diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 520b4a57d..c1070bf22 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -45,11 +45,15 @@ agentcore status # checks deployment status exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/bin/cdk.ts should match snapshot 1`] = ` "#!/usr/bin/env node import { AgentCoreStack } from '../lib/cdk-stack'; -import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk'; +import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '@aws/agentcore-cdk'; import { App, type Environment } from 'aws-cdk-lib'; import * as path from 'path'; import * as fs from 'fs'; +// CDK CLI's autoSynth uses process.on('beforeExit') which fires before async +// functions resolve. All reads must be synchronous to ensure stacks are +// registered on the App before the CDK CLI reads the cloud assembly. + function toEnvironment(target: AwsDeploymentTarget): Environment { return { account: target.account, @@ -65,115 +69,111 @@ function toStackName(projectName: string, targetName: string): string { return \`AgentCore-\${sanitize(projectName)}-\${sanitize(targetName)}\`; } -async function main() { - // Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. - const configRoot = path.resolve(process.cwd(), '..'); - const configIO = new ConfigIO({ baseDir: configRoot }); - - const spec = await configIO.readProjectSpec(); - const targets = await configIO.readAWSDeploymentTargets(); - - // Extract MCP configuration from project spec. - // Gateway fields are stored in agentcore.json but may not yet be on the - // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them - // dynamically and cast the resulting object. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const specAny = spec as any; - const mcpSpec = specAny.agentCoreGateways?.length - ? { - agentCoreGateways: specAny.agentCoreGateways, - mcpRuntimeTools: specAny.mcpRuntimeTools, - unassignedTargets: specAny.unassignedTargets, - } - : undefined; - - // Read deployed state for credential ARNs (populated by pre-deploy identity setup) - let deployedState: Record | undefined; - try { - deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); - } catch { - // Deployed state may not exist on first deploy - } - - if (targets.length === 0) { - throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); - } - - // Read harness configs for role creation. - // Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, - // so we read them dynamically via specAny (same pattern as gateways above). - // Harness paths in agentcore.json are relative to the project root (parent of agentcore/). - const projectRoot = path.resolve(configRoot, '..'); - const harnessConfigs: { - name: string; - executionRoleArn?: string; - memoryName?: string; - containerUri?: string; - hasDockerfile?: boolean; - dockerfile?: string; - codeLocation?: string; - tools?: { type: string; name: string }[]; - apiKeyArn?: string; - }[] = []; - for (const entry of specAny.harnesses ?? []) { - const harnessDir = path.resolve(projectRoot, entry.path); - const harnessPath = path.resolve(harnessDir, 'harness.json'); - try { - const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); - harnessConfigs.push({ - name: entry.name, - executionRoleArn: harnessSpec.executionRoleArn, - memoryName: harnessSpec.memory?.name, - containerUri: harnessSpec.containerUri, - hasDockerfile: !!harnessSpec.dockerfile, - dockerfile: harnessSpec.dockerfile, - codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, - tools: harnessSpec.tools, - apiKeyArn: harnessSpec.model?.apiKeyArn, - }); - } catch (err) { - throw new Error( - \`Could not read harness.json for "\${entry.name}" at \${harnessPath}: \${err instanceof Error ? err.message : err}\` - ); +// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. +const configRoot = path.resolve(process.cwd(), '..'); +const projectRoot = path.resolve(configRoot, '..'); + +const spec: AgentCoreProjectSpec = JSON.parse( + fs.readFileSync(path.join(configRoot, 'agentcore.json'), 'utf-8') +); +const targets: AwsDeploymentTarget[] = JSON.parse( + fs.readFileSync(path.join(configRoot, 'aws-targets.json'), 'utf-8') +); + +// Extract MCP configuration from project spec. +// Gateway fields are stored in agentcore.json but may not yet be on the +// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them +// dynamically and cast the resulting object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const specAny = spec as any; +const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, } - } + : undefined; + +// Read deployed state for credential ARNs (populated by pre-deploy identity setup) +let deployedState: Record | undefined; +try { + deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); +} catch { + // Deployed state may not exist on first deploy +} - const app = new App(); - - for (const target of targets) { - const env = toEnvironment(target); - const stackName = toStackName(spec.name, target.name); - - // Extract credentials from deployed state for this target - const targetState = (deployedState as Record)?.targets as - | Record> - | undefined; - const targetResources = targetState?.[target.name]?.resources as Record | undefined; - const credentials = targetResources?.credentials as - | Record - | undefined; - - new AgentCoreStack(app, stackName, { - spec, - mcpSpec, - credentials, - harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, - env, - description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`, - tags: { - 'agentcore:project-name': spec.name, - 'agentcore:target-name': target.name, - }, +if (targets.length === 0) { + throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); +} + +// Read harness configs for role creation. +// Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, +// so we read them dynamically via specAny (same pattern as gateways above). +// Harness paths in agentcore.json are relative to the project root (parent of agentcore/). +const harnessConfigs: { + name: string; + executionRoleArn?: string; + memoryName?: string; + containerUri?: string; + hasDockerfile?: boolean; + dockerfile?: string; + codeLocation?: string; + tools?: { type: string; name: string }[]; + apiKeyArn?: string; +}[] = []; +for (const entry of specAny.harnesses ?? []) { + const harnessDir = path.resolve(projectRoot, entry.path); + const harnessPath = path.resolve(harnessDir, 'harness.json'); + try { + const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); + harnessConfigs.push({ + name: entry.name, + executionRoleArn: harnessSpec.executionRoleArn, + memoryName: harnessSpec.memory?.name, + containerUri: harnessSpec.containerUri, + hasDockerfile: !!harnessSpec.dockerfile, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, + tools: harnessSpec.tools, + apiKeyArn: harnessSpec.model?.apiKeyArn, }); + } catch (err) { + throw new Error( + \`Could not read harness.json for "\${entry.name}" at \${harnessPath}: \${err instanceof Error ? err.message : err}\` + ); } +} - app.synth(); +const app = new App(); + +for (const target of targets) { + const env = toEnvironment(target); + const stackName = toStackName(spec.name, target.name); + + // Extract credentials from deployed state for this target + const targetState = (deployedState as Record)?.targets as + | Record> + | undefined; + const targetResources = targetState?.[target.name]?.resources as Record | undefined; + const credentials = targetResources?.credentials as + | Record + | undefined; + + new AgentCoreStack(app, stackName, { + spec, + mcpSpec, + credentials, + harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, + env, + description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`, + tags: { + 'agentcore:project-name': spec.name, + 'agentcore:target-name': target.name, + }, + }); } -main().catch((error: unknown) => { - console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error); - process.exitCode = 1; -}); +app.synth(); " `; diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 1c010e19b..9234861b7 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -1,10 +1,14 @@ #!/usr/bin/env node import { AgentCoreStack } from '../lib/cdk-stack'; -import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk'; +import type { AgentCoreProjectSpec, AwsDeploymentTarget } from '@aws/agentcore-cdk'; import { App, type Environment } from 'aws-cdk-lib'; import * as path from 'path'; import * as fs from 'fs'; +// CDK CLI's autoSynth uses process.on('beforeExit') which fires before async +// functions resolve. All reads must be synchronous to ensure stacks are +// registered on the App before the CDK CLI reads the cloud assembly. + function toEnvironment(target: AwsDeploymentTarget): Environment { return { account: target.account, @@ -20,112 +24,108 @@ function toStackName(projectName: string, targetName: string): string { return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`; } -async function main() { - // Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. - const configRoot = path.resolve(process.cwd(), '..'); - const configIO = new ConfigIO({ baseDir: configRoot }); +// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/. +const configRoot = path.resolve(process.cwd(), '..'); +const projectRoot = path.resolve(configRoot, '..'); - const spec = await configIO.readProjectSpec(); - const targets = await configIO.readAWSDeploymentTargets(); +const spec: AgentCoreProjectSpec = JSON.parse( + fs.readFileSync(path.join(configRoot, 'agentcore.json'), 'utf-8') +); +const targets: AwsDeploymentTarget[] = JSON.parse( + fs.readFileSync(path.join(configRoot, 'aws-targets.json'), 'utf-8') +); - // Extract MCP configuration from project spec. - // Gateway fields are stored in agentcore.json but may not yet be on the - // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them - // dynamically and cast the resulting object. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const specAny = spec as any; - const mcpSpec = specAny.agentCoreGateways?.length - ? { - agentCoreGateways: specAny.agentCoreGateways, - mcpRuntimeTools: specAny.mcpRuntimeTools, - unassignedTargets: specAny.unassignedTargets, - } - : undefined; +// Extract MCP configuration from project spec. +// Gateway fields are stored in agentcore.json but may not yet be on the +// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them +// dynamically and cast the resulting object. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const specAny = spec as any; +const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, + } + : undefined; - // Read deployed state for credential ARNs (populated by pre-deploy identity setup) - let deployedState: Record | undefined; - try { - deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); - } catch { - // Deployed state may not exist on first deploy - } +// Read deployed state for credential ARNs (populated by pre-deploy identity setup) +let deployedState: Record | undefined; +try { + deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); +} catch { + // Deployed state may not exist on first deploy +} - if (targets.length === 0) { - throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); - } +if (targets.length === 0) { + throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json'); +} - // Read harness configs for role creation. - // Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, - // so we read them dynamically via specAny (same pattern as gateways above). - // Harness paths in agentcore.json are relative to the project root (parent of agentcore/). - const projectRoot = path.resolve(configRoot, '..'); - const harnessConfigs: { - name: string; - executionRoleArn?: string; - memoryName?: string; - containerUri?: string; - hasDockerfile?: boolean; - dockerfile?: string; - codeLocation?: string; - tools?: { type: string; name: string }[]; - apiKeyArn?: string; - }[] = []; - for (const entry of specAny.harnesses ?? []) { - const harnessDir = path.resolve(projectRoot, entry.path); - const harnessPath = path.resolve(harnessDir, 'harness.json'); - try { - const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); - harnessConfigs.push({ - name: entry.name, - executionRoleArn: harnessSpec.executionRoleArn, - memoryName: harnessSpec.memory?.name, - containerUri: harnessSpec.containerUri, - hasDockerfile: !!harnessSpec.dockerfile, - dockerfile: harnessSpec.dockerfile, - codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, - tools: harnessSpec.tools, - apiKeyArn: harnessSpec.model?.apiKeyArn, - }); - } catch (err) { - throw new Error( - `Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}` - ); - } +// Read harness configs for role creation. +// Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk, +// so we read them dynamically via specAny (same pattern as gateways above). +// Harness paths in agentcore.json are relative to the project root (parent of agentcore/). +const harnessConfigs: { + name: string; + executionRoleArn?: string; + memoryName?: string; + containerUri?: string; + hasDockerfile?: boolean; + dockerfile?: string; + codeLocation?: string; + tools?: { type: string; name: string }[]; + apiKeyArn?: string; +}[] = []; +for (const entry of specAny.harnesses ?? []) { + const harnessDir = path.resolve(projectRoot, entry.path); + const harnessPath = path.resolve(harnessDir, 'harness.json'); + try { + const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8')); + harnessConfigs.push({ + name: entry.name, + executionRoleArn: harnessSpec.executionRoleArn, + memoryName: harnessSpec.memory?.name, + containerUri: harnessSpec.containerUri, + hasDockerfile: !!harnessSpec.dockerfile, + dockerfile: harnessSpec.dockerfile, + codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, + tools: harnessSpec.tools, + apiKeyArn: harnessSpec.model?.apiKeyArn, + }); + } catch (err) { + throw new Error( + `Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}` + ); } +} - const app = new App(); - - for (const target of targets) { - const env = toEnvironment(target); - const stackName = toStackName(spec.name, target.name); +const app = new App(); - // Extract credentials from deployed state for this target - const targetState = (deployedState as Record)?.targets as - | Record> - | undefined; - const targetResources = targetState?.[target.name]?.resources as Record | undefined; - const credentials = targetResources?.credentials as - | Record - | undefined; +for (const target of targets) { + const env = toEnvironment(target); + const stackName = toStackName(spec.name, target.name); - new AgentCoreStack(app, stackName, { - spec, - mcpSpec, - credentials, - harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, - env, - description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`, - tags: { - 'agentcore:project-name': spec.name, - 'agentcore:target-name': target.name, - }, - }); - } + // Extract credentials from deployed state for this target + const targetState = (deployedState as Record)?.targets as + | Record> + | undefined; + const targetResources = targetState?.[target.name]?.resources as Record | undefined; + const credentials = targetResources?.credentials as + | Record + | undefined; - app.synth(); + new AgentCoreStack(app, stackName, { + spec, + mcpSpec, + credentials, + harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, + env, + description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`, + tags: { + 'agentcore:project-name': spec.name, + 'agentcore:target-name': target.name, + }, + }); } -main().catch((error: unknown) => { - console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error); - process.exitCode = 1; -}); +app.synth();