Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
387862d
feat: add agentcore import command for starter toolkit migration
jesseturner21 Mar 19, 2026
7ecddbc
refactor: require existing project for import, fix memory support
jesseturner21 Mar 19, 2026
5681efa
fix: set up Python venv after import and fix setuptools auto-discovery
jesseturner21 Mar 19, 2026
3367bd2
feat: import credential providers from starter toolkit YAML
jesseturner21 Mar 19, 2026
de0bab8
fix: guard memory mode check with typeof string to prevent empty YAML…
jesseturner21 Mar 19, 2026
80e1409
fix: use Array.isArray guard for VPC networkConfig subnets/securityGr…
jesseturner21 Mar 19, 2026
e61888b
fix: make import idempotent by only importing newly-added resources
jesseturner21 Mar 19, 2026
a8621ea
fix: defer target resolution for undeployed starter toolkit imports
jesseturner21 Mar 19, 2026
da35072
test: add Test Group 1 unit tests for no-memory agent import path
jesseturner21 Mar 19, 2026
faf9534
test: add 30 unit tests for multi-agent import scenarios
jesseturner21 Mar 19, 2026
cd435cb
Revert "test: add 30 unit tests for multi-agent import scenarios"
jesseturner21 Mar 19, 2026
ce11f04
test: add merge-logic unit tests for CLI-native create then import sc…
jesseturner21 Mar 19, 2026
f8f54d0
test: add merge-logic unit tests for CLI-native create then import sc…
jesseturner21 Mar 19, 2026
6e1a14a
Fix import for container builds and multi-agent stacks
jesseturner21 Mar 20, 2026
562757c
fix(import): map OAuth providers correctly and fix region mismatch
jesseturner21 Mar 22, 2026
1762f1e
fix(import): handle symlinks and excluded dirs in source copy, warn o…
jesseturner21 Mar 23, 2026
9fcf323
fix(import): fix multi-agent memory import and skip Python setup for …
jesseturner21 Mar 23, 2026
6e655e0
fix(build): add jsx: automatic to esbuild config to fix TUI crash
jesseturner21 Mar 24, 2026
101040d
fix(import): warn about memory env var mismatch after import
jesseturner21 Mar 24, 2026
0c2277c
fix(import): warn about memory env var mismatch with diff-style hint
jesseturner21 Mar 24, 2026
dfed717
fix(tui): hide import command from interactive TUI menu
jesseturner21 Mar 24, 2026
201f298
fix(import): auto-bootstrap CDK environment before asset publishing
jesseturner21 Mar 24, 2026
c46f4a4
fix(import): resolve lint and format issues in import files
jesseturner21 Mar 24, 2026
9fdedb1
fix(tests): update tests for optional OAuth discoveryUrl schema change
jesseturner21 Mar 24, 2026
82dc367
fix(tests): hoist bootstrap mocks in idempotency test to survive clea…
jesseturner21 Mar 24, 2026
1c2489e
logs
jesseturner21 Mar 26, 2026
5f5aae0
feat(import): extract and pass through executionRoleArn from starter …
jesseturner21 Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ await esbuild.build({
platform: 'node',
format: 'esm',
minify: true,
jsx: 'automatic',
// Inject require shim for ESM compatibility with CommonJS dependencies
banner: {
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
Expand Down
2 changes: 2 additions & 0 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { registerDev } from './commands/dev';
import { registerEval } from './commands/eval';
import { registerFetch } from './commands/fetch';
import { registerHelp } from './commands/help';
import { registerImport } from './commands/import';
import { registerInvoke } from './commands/invoke';
import { registerLogs } from './commands/logs';
import { registerPackage } from './commands/package';
Expand Down Expand Up @@ -138,6 +139,7 @@ export function registerCommands(program: Command) {
registerEval(program);
registerFetch(program);
registerHelp(program);
registerImport(program);
registerInvoke(program);
registerLogs(program);
registerPackage(program);
Expand Down
268 changes: 268 additions & 0 deletions src/cli/commands/import/__tests__/container-agent-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/**
* Test Group 6: Container (Docker) Agent Import
*/
import { RUNTIME_TYPE_MAP } from '../constants';
import { buildImportTemplate, filterCompanionOnlyTemplate } from '../template-utils';
import { parseStarterToolkitYaml } from '../yaml-parser';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

function writeTempYaml(content: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'test6-'));
const filePath = path.join(dir, '.bedrock_agentcore.yaml');
fs.writeFileSync(filePath, content, 'utf-8');
return filePath;
}

function cleanupTempFile(filePath: string): void {
try {
fs.unlinkSync(filePath);
fs.rmdirSync(path.dirname(filePath));
} catch {
/* noop */
}
}

const AGENT_YAML_TEMPLATE = (overrides: string) => `
default_agent: my_agent
agents:
my_agent:
name: my_agent
entrypoint: main.py
${overrides}
aws:
account: '111122223333'
region: us-east-1
network_configuration:
network_mode: PUBLIC
protocol_configuration:
server_protocol: HTTP
observability:
enabled: true
bedrock_agentcore:
agent_id: null
`;

describe('deployment_type mapping', () => {
const tempFiles: string[] = [];
afterEach(() => {
for (const f of tempFiles) cleanupTempFile(f);
tempFiles.length = 0;
});

it('container -> Container', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: PYTHON_3_12'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('Container');
});

it('direct_code_deploy -> CodeZip', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: direct_code_deploy\n runtime_type: PYTHON_3_12'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('CodeZip');
});

it('missing -> Container (default)', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('runtime_type: PYTHON_3_12'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('Container');
});
});

describe('runtime_type handling', () => {
const tempFiles: string[] = [];
afterEach(() => {
for (const f of tempFiles) cleanupTempFile(f);
tempFiles.length = 0;
});

it('null -> PYTHON_3_12', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: null'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_12');
});

it('missing -> PYTHON_3_12', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_12');
});

it('PYTHON_3_13 -> PYTHON_3_13', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: PYTHON_3_13'));
tempFiles.push(f);
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_13');
});

it('unrecognized -> PYTHON_3_12 (not python3.12)', () => {
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: some_unknown'));
tempFiles.push(f);
const rv = parseStarterToolkitYaml(f).agents[0]!.runtimeVersion;
expect(rv).toBe('PYTHON_3_12');
expect(rv).not.toBe('python3.12');
});
});

describe('RUNTIME_TYPE_MAP', () => {
it('maps known types', () => {
expect(RUNTIME_TYPE_MAP.PYTHON_3_10).toBe('PYTHON_3_10');
expect(RUNTIME_TYPE_MAP.PYTHON_3_11).toBe('PYTHON_3_11');
expect(RUNTIME_TYPE_MAP.PYTHON_3_12).toBe('PYTHON_3_12');
expect(RUNTIME_TYPE_MAP.PYTHON_3_13).toBe('PYTHON_3_13');
});

it('undefined for invalid keys', () => {
expect(RUNTIME_TYPE_MAP['null' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
expect(RUNTIME_TYPE_MAP['undefined' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
expect(RUNTIME_TYPE_MAP['python_3_12' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
});
});

describe('full container agent parse', () => {
const tempFiles: string[] = [];
afterEach(() => {
for (const f of tempFiles) cleanupTempFile(f);
tempFiles.length = 0;
});

it('parses complete container agent with agent_id', () => {
const yaml = `
default_agent: container_agent
agents:
container_agent:
name: container_agent
entrypoint: main.py
deployment_type: container
runtime_type: null
language: python
aws:
account: '123456789012'
region: us-west-2
network_configuration:
network_mode: PUBLIC
protocol_configuration:
server_protocol: HTTP
observability:
enabled: true
bedrock_agentcore:
agent_id: abc123def456
agent_arn: arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/abc123def456
`;
const f = writeTempYaml(yaml);
tempFiles.push(f);
const parsed = parseStarterToolkitYaml(f);
const agent = parsed.agents[0]!;
expect(agent.build).toBe('Container');
expect(agent.runtimeVersion).toBe('PYTHON_3_12');
expect(agent.physicalAgentId).toBe('abc123def456');
expect(parsed.awsTarget.account).toBe('123456789012');
});

it('parses container agent with VPC', () => {
const yaml = `
default_agent: vpc_agent
agents:
vpc_agent:
name: vpc_agent
entrypoint: main.py
deployment_type: container
runtime_type: null
aws:
account: '123456789012'
region: us-east-1
network_configuration:
network_mode: VPC
network_mode_config:
subnets:
- subnet-12345678
security_groups:
- sg-11112222
protocol_configuration:
server_protocol: MCP
observability:
enabled: false
bedrock_agentcore:
agent_id: null
`;
const f = writeTempYaml(yaml);
tempFiles.push(f);
const agent = parseStarterToolkitYaml(f).agents[0]!;
expect(agent.build).toBe('Container');
expect(agent.networkMode).toBe('VPC');
expect(agent.networkConfig!.subnets).toContain('subnet-12345678');
expect(agent.protocol).toBe('MCP');
expect(agent.enableOtel).toBe(false);
});
});

describe('import template for container agents', () => {
it('buildImportTemplate sets DeletionPolicy: Retain', () => {
const deployed = {
AWSTemplateFormatVersion: '2010-09-09',
Resources: { Role: { Type: 'AWS::IAM::Role', Properties: {} } },
};
const synth = {
AWSTemplateFormatVersion: '2010-09-09',
Resources: {
Role: { Type: 'AWS::IAM::Role', Properties: {} },
RT: { Type: 'AWS::BedrockAgentCore::Runtime', Properties: { AgentRuntimeName: 'x' }, DependsOn: ['CR'] },
CR: { Type: 'AWS::CloudFormation::CustomResource', Properties: {} },
},
};
const result = buildImportTemplate(deployed, synth, ['RT']);
expect(result.Resources.RT).toBeDefined();
expect(result.Resources.RT!.DeletionPolicy).toBe('Retain');
expect(result.Resources.RT!.DependsOn).toBeUndefined();
expect(result.Resources.CR).toBeUndefined();
});

it('filterCompanionOnlyTemplate removes primary resources', () => {
const synth = {
AWSTemplateFormatVersion: '2010-09-09',
Resources: {
Role: { Type: 'AWS::IAM::Role', Properties: {} },
RT: { Type: 'AWS::BedrockAgentCore::Runtime', Properties: {} },
Lambda: { Type: 'AWS::Lambda::Function', Properties: {} },
},
Outputs: {
RTId: { Value: { 'Fn::GetAtt': ['RT', 'AgentRuntimeId'] } },
LambdaArn: { Value: { 'Fn::GetAtt': ['Lambda', 'Arn'] } },
},
};
const filtered = filterCompanionOnlyTemplate(synth);
expect(filtered.Resources.RT).toBeUndefined();
expect(filtered.Resources.Role).toBeDefined();
expect(filtered.Resources.Lambda).toBeDefined();
expect(filtered.Outputs!.RTId).toBeUndefined();
expect(filtered.Outputs!.LambdaArn).toBeDefined();
});
});

describe('container source code', () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test6-src-'));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('may contain Dockerfile', () => {
fs.writeFileSync(path.join(tempDir, 'Dockerfile'), 'FROM python:3.12\n');
fs.writeFileSync(path.join(tempDir, 'main.py'), 'print("hi")');
expect(fs.readdirSync(tempDir)).toContain('Dockerfile');
});

it('may lack pyproject.toml', () => {
fs.writeFileSync(path.join(tempDir, 'Dockerfile'), 'FROM python:3.12\n');
expect(fs.existsSync(path.join(tempDir, 'pyproject.toml'))).toBe(false);
});
});

describe('defaults alignment', () => {
it('CLI default matches starter toolkit default', () => {
expect('container').toBe('container');
});
});
63 changes: 63 additions & 0 deletions src/cli/commands/import/__tests__/execution-role-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Tests for execution role import from starter toolkit YAML.
*/
import type { AgentEnvSpec } from '../../../../schema/schemas/agent-env';
import type { ParsedStarterToolkitConfig } from '../types';
import { parseStarterToolkitYaml } from '../yaml-parser';
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';

const APP_DIR = 'app';

function toAgentEnvSpec(agent: ParsedStarterToolkitConfig['agents'][0]): AgentEnvSpec {
const codeLocation = path.join(APP_DIR, agent.name);
const entrypoint = path.basename(agent.entrypoint);
const spec: AgentEnvSpec = {
type: 'AgentCoreRuntime',
name: agent.name,
build: agent.build,
entrypoint: entrypoint as AgentEnvSpec['entrypoint'],
codeLocation: codeLocation as AgentEnvSpec['codeLocation'],
runtimeVersion: (agent.runtimeVersion ?? 'PYTHON_3_12') as AgentEnvSpec['runtimeVersion'],
protocol: agent.protocol,
networkMode: agent.networkMode,
instrumentation: { enableOtel: agent.enableOtel },
};
if (agent.networkMode === 'VPC' && agent.networkConfig) {
spec.networkConfig = agent.networkConfig;
}
if (agent.executionRoleArn) {
spec.executionRoleArn = agent.executionRoleArn;
}
return spec;
}

const FIXTURE = path.join(__dirname, 'fixtures', 'agent-with-execution-role.yaml');
const FIXTURE_NO_ROLE = path.join(__dirname, 'fixtures', 'different-agent.yaml');

describe('parseStarterToolkitYaml: executionRoleArn', () => {
it('extracts executionRoleArn from YAML with execution_role', () => {
const parsed = parseStarterToolkitYaml(FIXTURE);
expect(parsed.agents).toHaveLength(1);
expect(parsed.agents[0]!.executionRoleArn).toBe('arn:aws:iam::123456789012:role/StarterToolkitExecutionRole');
});

it('returns undefined executionRoleArn when execution_role is absent', () => {
const parsed = parseStarterToolkitYaml(FIXTURE_NO_ROLE);
expect(parsed.agents[0]!.executionRoleArn).toBeUndefined();
});
});

describe('toAgentEnvSpec: executionRoleArn', () => {
it('includes executionRoleArn in spec when present', () => {
const parsed = parseStarterToolkitYaml(FIXTURE);
const spec = toAgentEnvSpec(parsed.agents[0]!);
expect(spec.executionRoleArn).toBe('arn:aws:iam::123456789012:role/StarterToolkitExecutionRole');
});

it('omits executionRoleArn from spec when absent', () => {
const parsed = parseStarterToolkitYaml(FIXTURE_NO_ROLE);
const spec = toAgentEnvSpec(parsed.agents[0]!);
expect(spec.executionRoleArn).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
default_agent: my_agent
agents:
my_agent:
name: my_agent
entrypoint: main.py
deployment_type: direct_code_deploy
runtime_type: PYTHON_3_12
source_path: null
aws:
account: '123456789012'
region: us-west-2
execution_role: arn:aws:iam::123456789012:role/StarterToolkitExecutionRole
network_configuration:
network_mode: PUBLIC
protocol_configuration:
server_protocol: HTTP
observability:
enabled: true
bedrock_agentcore:
agent_id: AGENT_ROLE_123
agent_arn: arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/AGENT_ROLE_123
memory:
mode: NO_MEMORY
Loading
Loading