Skip to content

Commit 2142c77

Browse files
feat: add agentcore import command for starter toolkit migration (#620)
* feat: add agentcore import command for starter toolkit migration Implements a 3-phase import flow that migrates Bedrock AgentCore Starter Toolkit projects to the agentcore-cli: - Phase 1: Creates companion CloudFormation resources (IAM roles/policies) - Phase 2: Imports existing AWS resources via CFN IMPORT change set - Phase 3: User runs `agentcore deploy` to reconcile the stack Parses .bedrock_agentcore.yaml, scaffolds CLI project structure, copies agent source code, publishes CDK assets, and adopts pre-existing runtimes and memories into the CLI's CloudFormation stack. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: require existing project for import, fix memory support - Remove scaffold.ts; import now requires `agentcore create` first - Resolve target from project's aws-targets.json (auto-select single, error on multiple without --target, create default from YAML if empty) - Replace dangling Fn::GetAtt/Ref to removed primary resources with "*" in Phase 1 companion template (fixes IAM policy ARN validation) - Fix memoryArn placeholder in deployed state (construct ARN from ID) - Make --target optional (no default value) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: set up Python venv after import and fix setuptools auto-discovery Starter toolkit projects have multiple top-level directories (model/, mcp_client/) which causes setuptools to fail with "Multiple top-level packages discovered". Fix by appending [tool.setuptools] py-modules = [] to pyproject.toml after copy. Also run uv venv + uv sync so agentcore dev works immediately after import. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: import credential providers from starter toolkit YAML Parse OAuth and API key credential providers from .bedrock_agentcore.yaml identity config and add them to project.json during import. Fix YAML parser to handle list items with nested key-value pairs (e.g., credential_providers). Fix eslint no-base-to-string errors by adding explicit type casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: guard memory mode check with typeof string to prevent empty YAML mode from creating invalid memory The simple YAML parser turns bare "mode:" (no value) into an empty object {}, which is truthy and passes the existing `!== 'NO_MEMORY' && memoryConfig.mode` check. This caused a memory with mode={} to be created, which is invalid. Added typeof check to ensure mode is actually a string before proceeding. Also adds comprehensive unit tests for import memory handling (35 tests). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use Array.isArray guard for VPC networkConfig subnets/securityGroups The YAML parser creates empty objects {} for keys with no list items (e.g., `subnets:` with nothing underneath). The previous code used `(networkModeConfig.subnets as string[]) ?? []` which doesn't protect against this since {} is truthy. This resulted in subnets being {} instead of [] when the YAML had empty subnet/security_group keys under VPC mode. Now uses Array.isArray() to ensure we always get a proper array, falling back to [] when the parser returns a non-array value. Also adds comprehensive VPC import tests (18 tests covering parsing, edge cases, mixed agents, quoted values, and starter toolkit format). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make import idempotent by only importing newly-added resources Previously, running `agentcore import` twice with the same YAML would attempt to re-import already-managed CloudFormation resources in Phase 2, causing errors like "resource already exists in stack". The bug was that agentsToImport and memoriesToImport were filtered from all parsed YAML agents (regardless of whether they were skipped during merge), rather than from only the newly-added ones. Fix: track which agents/memories are actually added during the merge step (newlyAddedAgentNames / newlyAddedMemoryNames) and filter agentsToImport / memoriesToImport to only include those newly-added resources. Adds 17 unit tests covering first import, second import idempotency, partial overlap, credential idempotency, and edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: defer target resolution for undeployed starter toolkit imports When importing a starter toolkit YAML with no physical IDs (agent_id and memory_id are null), the import command would fail unnecessarily if no deployment targets existed and the YAML had null account/region. This is incorrect because the no-deploy path only needs config merge and source copy -- no CloudFormation operations. The fix computes hasPhysicalIds early and only performs strict target resolution (requiring account/region) when physical IDs are present. When no physical IDs exist, target resolution is lenient -- it uses an existing target if available, or falls back to 'default' for stackName. Also adds 22 unit tests covering the no-deploy path: YAML parsing of null IDs, early return behavior, config merge, Python setup, absence of CloudFormation operations, and target resolution edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add Test Group 1 unit tests for no-memory agent import path 63 tests covering YAML parsing, toAgentEnvSpec conversion, merge logic, source copy, Phase 1/Phase 2 template operations, findLogicalId helpers, sanitize/toStackName, and integration for a single agent with no memory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add 30 unit tests for multi-agent import scenarios Tests cover: - YAML parsing with 2, 3, and similar-named agents - Memory deduplication across shared agents - Credential extraction from identity.credential_providers - findLogicalIdByProperty with multiple runtimes - findLogicalIdByProperty Fn::Sub false substring match regression test - filterCompanionOnlyTemplate with multiple primary resources - buildImportTemplate with multiple import targets - Stack name sanitization and source code directory structure - Partial import (deployed + undeployed agents mixed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Revert "test: add 30 unit tests for multi-agent import scenarios" This reverts commit 25a1458125d9ecdae1dd3e8c2f6e17336fc7498f. * test: add merge-logic unit tests for CLI-native create then import scenario 16 tests covering agent/memory/credential deduplication, Set-based name matching, toMemorySpec clamping, source copy skip logic, append-only merge behavior, and edge cases for projects with existing agents and memories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add merge-logic unit tests for CLI-native create then import scenario 16 tests covering agent/memory/credential deduplication, Set-based name matching, toMemorySpec clamping, source copy skip logic, append-only merge behavior, and edge cases for projects with existing agents and memories. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix import for container builds and multi-agent stacks - Publish CDK assets to S3 before Phase 1 (CodeBuild needs source zip) - Handle directory assets in publishCdkAssets (zip before upload) - Export publishCdkAssets for use in actions.ts - Copy Dockerfile from starter toolkit config for Container builds - Generate fallback Dockerfile if toolkit config not found - Preserve previously imported resources during Phase 1 UPDATE by merging deployed template back into companion template Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): map OAuth providers correctly and fix region mismatch - Change toCredentialSpec() to map OAuth providers to OAuthCredentialProvider instead of ApiKeyCredentialProvider, so the CLI wires CLIENT_ID/CLIENT_SECRET env vars correctly (not API_KEY) - Make discoveryUrl optional in OAuthCredentialSchema for imported providers that already exist in Identity service - Skip Identity service create/update for OAuth providers without discoveryUrl during deploy (provider already exists) - Set AWS_REGION/AWS_DEFAULT_REGION to target.region before import operations to prevent region mismatch when env var differs from aws-targets.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): handle symlinks and excluded dirs in source copy, warn on authorizer config BUG-001: copyDirRecursive now skips symlinks and common excluded directories (.venv, .git, __pycache__, node_modules, etc.) preventing EISDIR crash when importing projects with virtualenvs containing symlinks like .venv/lib64 -> lib. BUG-004: Import now warns when an agent has authorizer_configuration set in the starter toolkit YAML, since custom JWT authorizer config is not automatically imported and must be manually recreated via `agentcore add gateway`. Constraint: CLI gateway model is separate from agent config, so full authorizer import requires design work Rejected: Auto-create gateway during import | gateway-agent linking logic is complex and warrants its own feature Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): fix multi-agent memory import and skip Python setup for containers Two fixes: 1. Memory name prefix mismatch: CDK prefixes memory names with project name (e.g. "myproject_Agent_mem") but import searched for unprefixed YAML name. Added fallback lookup with project name prefix. 2. Container agents no longer trigger unnecessary Python venv setup during import, since dependencies are installed inside the Docker image. Constraint: CDK BasePrimitiveConstruct generates physicalName as ${projectName}_${name} Rejected: Stripping prefix from CDK names | would break other CDK conventions Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(build): add jsx: automatic to esbuild config to fix TUI crash esbuild does not read tsconfig.json, so it defaults to classic JSX transform (React.createElement) while tsconfig specifies react-jsx (automatic). This produces 432 bare React.createElement calls in the bundle that reference a nonexistent React global, crashing all TUI commands (status, deploy, etc.) with "React is not defined". Constraint: esbuild ignores tsconfig jsx settings by default Rejected: Adding React as external | would require React in node_modules at runtime Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): warn about memory env var mismatch after import The Starter Toolkit injects memory ID via BEDROCK_AGENTCORE_MEMORY_ID, but CDK constructs use MEMORY_{NAME}_ID pattern. After import, agent code still references the old env var, causing memory to silently fail. Add a yellow warning during import telling users the correct env var name to update in their agent code. Constraint: CDK env var naming is controlled by AgentCoreMemory.getEnvVarName() Rejected: Auto-rewrite agent source code | too fragile across frameworks Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): warn about memory env var mismatch with diff-style hint The Starter Toolkit injects memory ID via BEDROCK_AGENTCORE_MEMORY_ID, but CDK constructs use MEMORY_{NAME}_ID pattern. After import, agent code still references the old env var, causing memory to silently fail. Show a git-diff-style warning during import with red/green highlighting so users see exactly what to change in their agent code. Constraint: CDK env var naming is controlled by AgentCoreMemory.getEnvVarName() Rejected: Auto-rewrite agent source code | too fragile across frameworks Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tui): hide import command from interactive TUI menu Import is a CLI-only command that requires --source flag and doesn't have a TUI screen. Remove it from the interactive command picker. Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): auto-bootstrap CDK environment before asset publishing Import fails with "The specified bucket does not exist" when the AWS account hasn't been CDK-bootstrapped. The deploy command handles this via useCdkPreflight, but import bypasses that since it's CLI-only. Check bootstrap status after CDK synth and auto-bootstrap if needed, before disposing the toolkit wrapper. Tested on two unbootstrapped accounts (509471412906, 126432121770). Constraint: Must bootstrap before disposing toolkitWrapper since bootstrap requires it Rejected: Prompt user to manually run cdk bootstrap | poor UX for import flow Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): resolve lint and format issues in import files - Remove unused waitUntilChangeSetCreateComplete import - Prefix unused assetHash with underscore - Add eslint-disable for necessary any cast in STS credentials - Remove unnecessary type assertion in test - Fix prettier formatting in multi-agent test Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tests): update tests for optional OAuth discoveryUrl schema change OAuthCredentialProvider.discoveryUrl was made optional to support imported providers that already exist in Identity service. Update tests to match: - Schema test: expect success when discoveryUrl is omitted - Pre-deploy identity tests: add discoveryUrl to fixtures that test update and error paths (without it, the code now skips the provider) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tests): hoist bootstrap mocks in idempotency test to survive clearAllMocks checkBootstrapNeeded and bootstrapEnvironment were created as inline vi.fn() inside the vi.mock factory. vi.clearAllMocks() in beforeEach wipes their mockResolvedValue, causing handleImport to fail in CI where test execution order differs from local runs. Hoist the mocks and configure them in setupCommonMocks like all other mock functions in the file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * logs * fix(import): rollback config on CDK/CloudFormation phase failure When import fails during CDK build/synth or CloudFormation phases, the merged config (agentcore.json) was already written to disk. On retry, the import would detect existing agents and report false success without completing the CloudFormation import. This snapshots the original config before merging and restores it if any later phase fails. Constraint: Rollback only triggers after config is written (configWritten flag) Constraint: Snapshot must be a deep clone since projectSpec is mutated in-place Rejected: Deferred config write until after all phases | too invasive, changes control flow for all paths Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(import): map STM_ONLY memory to zero strategies STM_ONLY memories incorrectly received a SEMANTIC strategy in the import flow, while the CLI create flow and starter toolkit both deploy STM_ONLY with zero strategies. This mismatch caused drift/errors during reconciliation. Constraint: Must match CLI create flow which maps shortTerm to strategies: [] Rejected: Keep SEMANTIC for STM_ONLY | causes reconciliation drift with deployed resources Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(import): extract custom JWT authorizer config from starter toolkit YAML Instead of warning users to manually recreate the gateway, the import pipeline now reads authorizer_configuration.customJWTAuthorizer from the starter toolkit YAML and writes authorizerType/authorizerConfiguration to agentcore.json so CDK handles it automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add yaml@2 and regenerate package-lock.json for CI compatibility Resolves npm ci failure caused by missing yaml@2.8.3 transitive dependency required by vitest/vite and lint-staged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0b743db commit 2142c77

37 files changed

Lines changed: 10251 additions & 2153 deletions

esbuild.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ await esbuild.build({
4848
platform: 'node',
4949
format: 'esm',
5050
minify: true,
51+
jsx: 'automatic',
5152
// Inject require shim for ESM compatibility with CommonJS dependencies
5253
banner: {
5354
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,

package-lock.json

Lines changed: 2257 additions & 2145 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,15 @@
9595
"ink-spinner": "^5.0.0",
9696
"js-yaml": "^4.1.1",
9797
"react": "^19.2.3",
98+
"yaml": "^2.8.3",
9899
"zod": "^4.3.5"
99100
},
100101
"peerDependencies": {
101102
"aws-cdk-lib": "^2.243.0",
102103
"constructs": "^10.0.0"
103104
},
104105
"devDependencies": {
105-
"@aws-sdk/client-cognito-identity-provider": "^3.1017.0",
106+
"@aws-sdk/client-cognito-identity-provider": "^3.1018.0",
106107
"@eslint/js": "^9.39.2",
107108
"@modelcontextprotocol/sdk": "^1.0.0",
108109
"@secretlint/secretlint-rule-preset-recommend": "^11.3.0",

src/cli/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { registerDev } from './commands/dev';
55
import { registerEval } from './commands/eval';
66
import { registerFetch } from './commands/fetch';
77
import { registerHelp } from './commands/help';
8+
import { registerImport } from './commands/import';
89
import { registerInvoke } from './commands/invoke';
910
import { registerLogs } from './commands/logs';
1011
import { registerPackage } from './commands/package';
@@ -138,6 +139,7 @@ export function registerCommands(program: Command) {
138139
registerEval(program);
139140
registerFetch(program);
140141
registerHelp(program);
142+
registerImport(program);
141143
registerInvoke(program);
142144
registerLogs(program);
143145
registerPackage(program);
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* Test Group 6: Container (Docker) Agent Import
3+
*/
4+
import { RUNTIME_TYPE_MAP } from '../constants';
5+
import { buildImportTemplate, filterCompanionOnlyTemplate } from '../template-utils';
6+
import { parseStarterToolkitYaml } from '../yaml-parser';
7+
import * as fs from 'node:fs';
8+
import * as os from 'node:os';
9+
import * as path from 'node:path';
10+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
11+
12+
function writeTempYaml(content: string): string {
13+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'test6-'));
14+
const filePath = path.join(dir, '.bedrock_agentcore.yaml');
15+
fs.writeFileSync(filePath, content, 'utf-8');
16+
return filePath;
17+
}
18+
19+
function cleanupTempFile(filePath: string): void {
20+
try {
21+
fs.unlinkSync(filePath);
22+
fs.rmdirSync(path.dirname(filePath));
23+
} catch {
24+
/* noop */
25+
}
26+
}
27+
28+
const AGENT_YAML_TEMPLATE = (overrides: string) => `
29+
default_agent: my_agent
30+
agents:
31+
my_agent:
32+
name: my_agent
33+
entrypoint: main.py
34+
${overrides}
35+
aws:
36+
account: '111122223333'
37+
region: us-east-1
38+
network_configuration:
39+
network_mode: PUBLIC
40+
protocol_configuration:
41+
server_protocol: HTTP
42+
observability:
43+
enabled: true
44+
bedrock_agentcore:
45+
agent_id: null
46+
`;
47+
48+
describe('deployment_type mapping', () => {
49+
const tempFiles: string[] = [];
50+
afterEach(() => {
51+
for (const f of tempFiles) cleanupTempFile(f);
52+
tempFiles.length = 0;
53+
});
54+
55+
it('container -> Container', () => {
56+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: PYTHON_3_12'));
57+
tempFiles.push(f);
58+
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('Container');
59+
});
60+
61+
it('direct_code_deploy -> CodeZip', () => {
62+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: direct_code_deploy\n runtime_type: PYTHON_3_12'));
63+
tempFiles.push(f);
64+
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('CodeZip');
65+
});
66+
67+
it('missing -> Container (default)', () => {
68+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('runtime_type: PYTHON_3_12'));
69+
tempFiles.push(f);
70+
expect(parseStarterToolkitYaml(f).agents[0]!.build).toBe('Container');
71+
});
72+
});
73+
74+
describe('runtime_type handling', () => {
75+
const tempFiles: string[] = [];
76+
afterEach(() => {
77+
for (const f of tempFiles) cleanupTempFile(f);
78+
tempFiles.length = 0;
79+
});
80+
81+
it('null -> PYTHON_3_12', () => {
82+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: null'));
83+
tempFiles.push(f);
84+
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_12');
85+
});
86+
87+
it('missing -> PYTHON_3_12', () => {
88+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container'));
89+
tempFiles.push(f);
90+
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_12');
91+
});
92+
93+
it('PYTHON_3_13 -> PYTHON_3_13', () => {
94+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: PYTHON_3_13'));
95+
tempFiles.push(f);
96+
expect(parseStarterToolkitYaml(f).agents[0]!.runtimeVersion).toBe('PYTHON_3_13');
97+
});
98+
99+
it('unrecognized -> PYTHON_3_12 (not python3.12)', () => {
100+
const f = writeTempYaml(AGENT_YAML_TEMPLATE('deployment_type: container\n runtime_type: some_unknown'));
101+
tempFiles.push(f);
102+
const rv = parseStarterToolkitYaml(f).agents[0]!.runtimeVersion;
103+
expect(rv).toBe('PYTHON_3_12');
104+
expect(rv).not.toBe('python3.12');
105+
});
106+
});
107+
108+
describe('RUNTIME_TYPE_MAP', () => {
109+
it('maps known types', () => {
110+
expect(RUNTIME_TYPE_MAP.PYTHON_3_10).toBe('PYTHON_3_10');
111+
expect(RUNTIME_TYPE_MAP.PYTHON_3_11).toBe('PYTHON_3_11');
112+
expect(RUNTIME_TYPE_MAP.PYTHON_3_12).toBe('PYTHON_3_12');
113+
expect(RUNTIME_TYPE_MAP.PYTHON_3_13).toBe('PYTHON_3_13');
114+
});
115+
116+
it('undefined for invalid keys', () => {
117+
expect(RUNTIME_TYPE_MAP['null' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
118+
expect(RUNTIME_TYPE_MAP['undefined' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
119+
expect(RUNTIME_TYPE_MAP['python_3_12' as keyof typeof RUNTIME_TYPE_MAP]).toBeUndefined();
120+
});
121+
});
122+
123+
describe('full container agent parse', () => {
124+
const tempFiles: string[] = [];
125+
afterEach(() => {
126+
for (const f of tempFiles) cleanupTempFile(f);
127+
tempFiles.length = 0;
128+
});
129+
130+
it('parses complete container agent with agent_id', () => {
131+
const yaml = `
132+
default_agent: container_agent
133+
agents:
134+
container_agent:
135+
name: container_agent
136+
entrypoint: main.py
137+
deployment_type: container
138+
runtime_type: null
139+
language: python
140+
aws:
141+
account: '123456789012'
142+
region: us-west-2
143+
network_configuration:
144+
network_mode: PUBLIC
145+
protocol_configuration:
146+
server_protocol: HTTP
147+
observability:
148+
enabled: true
149+
bedrock_agentcore:
150+
agent_id: abc123def456
151+
agent_arn: arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/abc123def456
152+
`;
153+
const f = writeTempYaml(yaml);
154+
tempFiles.push(f);
155+
const parsed = parseStarterToolkitYaml(f);
156+
const agent = parsed.agents[0]!;
157+
expect(agent.build).toBe('Container');
158+
expect(agent.runtimeVersion).toBe('PYTHON_3_12');
159+
expect(agent.physicalAgentId).toBe('abc123def456');
160+
expect(parsed.awsTarget.account).toBe('123456789012');
161+
});
162+
163+
it('parses container agent with VPC', () => {
164+
const yaml = `
165+
default_agent: vpc_agent
166+
agents:
167+
vpc_agent:
168+
name: vpc_agent
169+
entrypoint: main.py
170+
deployment_type: container
171+
runtime_type: null
172+
aws:
173+
account: '123456789012'
174+
region: us-east-1
175+
network_configuration:
176+
network_mode: VPC
177+
network_mode_config:
178+
subnets:
179+
- subnet-12345678
180+
security_groups:
181+
- sg-11112222
182+
protocol_configuration:
183+
server_protocol: MCP
184+
observability:
185+
enabled: false
186+
bedrock_agentcore:
187+
agent_id: null
188+
`;
189+
const f = writeTempYaml(yaml);
190+
tempFiles.push(f);
191+
const agent = parseStarterToolkitYaml(f).agents[0]!;
192+
expect(agent.build).toBe('Container');
193+
expect(agent.networkMode).toBe('VPC');
194+
expect(agent.networkConfig!.subnets).toContain('subnet-12345678');
195+
expect(agent.protocol).toBe('MCP');
196+
expect(agent.enableOtel).toBe(false);
197+
});
198+
});
199+
200+
describe('import template for container agents', () => {
201+
it('buildImportTemplate sets DeletionPolicy: Retain', () => {
202+
const deployed = {
203+
AWSTemplateFormatVersion: '2010-09-09',
204+
Resources: { Role: { Type: 'AWS::IAM::Role', Properties: {} } },
205+
};
206+
const synth = {
207+
AWSTemplateFormatVersion: '2010-09-09',
208+
Resources: {
209+
Role: { Type: 'AWS::IAM::Role', Properties: {} },
210+
RT: { Type: 'AWS::BedrockAgentCore::Runtime', Properties: { AgentRuntimeName: 'x' }, DependsOn: ['CR'] },
211+
CR: { Type: 'AWS::CloudFormation::CustomResource', Properties: {} },
212+
},
213+
};
214+
const result = buildImportTemplate(deployed, synth, ['RT']);
215+
expect(result.Resources.RT).toBeDefined();
216+
expect(result.Resources.RT!.DeletionPolicy).toBe('Retain');
217+
expect(result.Resources.RT!.DependsOn).toBeUndefined();
218+
expect(result.Resources.CR).toBeUndefined();
219+
});
220+
221+
it('filterCompanionOnlyTemplate removes primary resources', () => {
222+
const synth = {
223+
AWSTemplateFormatVersion: '2010-09-09',
224+
Resources: {
225+
Role: { Type: 'AWS::IAM::Role', Properties: {} },
226+
RT: { Type: 'AWS::BedrockAgentCore::Runtime', Properties: {} },
227+
Lambda: { Type: 'AWS::Lambda::Function', Properties: {} },
228+
},
229+
Outputs: {
230+
RTId: { Value: { 'Fn::GetAtt': ['RT', 'AgentRuntimeId'] } },
231+
LambdaArn: { Value: { 'Fn::GetAtt': ['Lambda', 'Arn'] } },
232+
},
233+
};
234+
const filtered = filterCompanionOnlyTemplate(synth);
235+
expect(filtered.Resources.RT).toBeUndefined();
236+
expect(filtered.Resources.Role).toBeDefined();
237+
expect(filtered.Resources.Lambda).toBeDefined();
238+
expect(filtered.Outputs!.RTId).toBeUndefined();
239+
expect(filtered.Outputs!.LambdaArn).toBeDefined();
240+
});
241+
});
242+
243+
describe('container source code', () => {
244+
let tempDir: string;
245+
beforeEach(() => {
246+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test6-src-'));
247+
});
248+
afterEach(() => {
249+
fs.rmSync(tempDir, { recursive: true, force: true });
250+
});
251+
252+
it('may contain Dockerfile', () => {
253+
fs.writeFileSync(path.join(tempDir, 'Dockerfile'), 'FROM python:3.12\n');
254+
fs.writeFileSync(path.join(tempDir, 'main.py'), 'print("hi")');
255+
expect(fs.readdirSync(tempDir)).toContain('Dockerfile');
256+
});
257+
258+
it('may lack pyproject.toml', () => {
259+
fs.writeFileSync(path.join(tempDir, 'Dockerfile'), 'FROM python:3.12\n');
260+
expect(fs.existsSync(path.join(tempDir, 'pyproject.toml'))).toBe(false);
261+
});
262+
});
263+
264+
describe('defaults alignment', () => {
265+
it('CLI default matches starter toolkit default', () => {
266+
expect('container').toBe('container');
267+
});
268+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "MyProject",
3+
"version": 1,
4+
"agents": [
5+
{
6+
"type": "AgentCoreRuntime",
7+
"name": "existing_agent",
8+
"build": "CodeZip",
9+
"entrypoint": "main.py",
10+
"codeLocation": "app/existing_agent",
11+
"runtimeVersion": "PYTHON_3_12",
12+
"protocol": "HTTP",
13+
"networkMode": "PUBLIC",
14+
"instrumentation": {
15+
"enableOtel": true
16+
}
17+
}
18+
],
19+
"memories": [
20+
{
21+
"type": "AgentCoreMemory",
22+
"name": "existing_agent_memory",
23+
"eventExpiryDuration": 30,
24+
"strategies": [
25+
{
26+
"type": "SEMANTIC"
27+
}
28+
]
29+
}
30+
],
31+
"credentials": [
32+
{
33+
"type": "ApiKeyCredentialProvider",
34+
"name": "my_api_key"
35+
}
36+
],
37+
"evaluators": [],
38+
"onlineEvalConfigs": []
39+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
default_agent: new_toolkit_agent
2+
agents:
3+
new_toolkit_agent:
4+
name: new_toolkit_agent
5+
entrypoint: main.py
6+
deployment_type: direct_code_deploy
7+
runtime_type: PYTHON_3_12
8+
source_path: null
9+
aws:
10+
account: '123456789012'
11+
region: us-west-2
12+
network_configuration:
13+
network_mode: PUBLIC
14+
protocol_configuration:
15+
server_protocol: HTTP
16+
observability:
17+
enabled: true
18+
bedrock_agentcore:
19+
agent_id: AGENT_NEW_123
20+
agent_arn: arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/AGENT_NEW_123
21+
agent_session_id: null
22+
memory:
23+
mode: STM_AND_LTM
24+
memory_id: MEM_NEW_456
25+
memory_arn: arn:aws:bedrock-agentcore:us-west-2:123456789012:memory/MEM_NEW_456
26+
memory_name: new_toolkit_memory
27+
event_expiry_days: 60
28+
api_key_credential_provider_name: new_api_key_cred

0 commit comments

Comments
 (0)