From 6033149af5af59adb8c4eb799ea5a1b615e806db Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Wed, 9 Sep 2026 01:30:21 +0000 Subject: [PATCH 1/2] feat(project): vend a thin CDK app built on transformAgentCoreJson MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CDK app `project create` writes into agentcore/cdk/ shrinks to two files. bin/cdk.ts calls readAgentCoreProject, resolveTargetStacks and transformAgentCoreJson from @aws/agentcore-cdk and instantiates one AgentCoreStack per target; lib/cdk-stack.ts instantiates one AgentCoreApplication (construct id 'Application' unchanged) and is the file a customer edits to add resources and grant runtimes or harnesses access to them. Everything the old 181-line bin/cdk.ts did — reading the spec, every harness.json and system-prompt.md, connector files and deployed-state.json, stack naming and tagging — now lives in the library, so it changes with the library version rather than on customers' disks. No `as any`: the types come from the pinned library. The unused StackNameOutput output is dropped. test/cdk.test.ts, jest.config.js, .prettierrc and npmignore.template are removed, and jest, ts-jest, @types/jest and prettier leave the vended package.json with the test and format scripts: the library's vitest suite and this repository's tests cover synthesis, and a customer who wants tests in their CDK app adds them. The pin moves to 0.1.0-alpha.53, the first library release carrying transformAgentCoreJson; dependencies["@aws/agentcore-cdk"] stays a plain exact version string for scripts/sync-vended-cdk.ts. The manager snapshot drops the four removed files. The observability and CDK-backend comments that cited src/assets/cdk now point at the library helper and construct that own the rule. pathLimit: a fresh install puts its deepest file 155 characters below the project root (aws-cdk-lib's shipped fixtures, not jest), so the Windows limit is derived from that measurement (104-character project root) instead of the old "about 100" guess. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KEWrUu52DmcWE3sx4VEhKL --- src/assets/cdk/.prettierrc | 8 - src/assets/cdk/README.md | 58 ++++-- src/assets/cdk/bin/cdk.ts | 194 ++---------------- src/assets/cdk/jest.config.js | 9 - src/assets/cdk/lib/cdk-stack.ts | 98 ++------- src/assets/cdk/npmignore.template | 6 - src/assets/cdk/package.json | 11 +- src/assets/cdk/test/cdk.test.ts | 120 ----------- src/assets/cdk/tsconfig.json | 2 +- src/core/observability.ts | 7 +- .../__snapshots__/manager.test.ts.snap | 20 -- src/core/project/backends/cdk.ts | 14 +- src/handlers/project/create/pathLimit.test.ts | 10 + src/handlers/project/create/pathLimit.ts | 18 +- 14 files changed, 115 insertions(+), 460 deletions(-) delete mode 100644 src/assets/cdk/.prettierrc delete mode 100644 src/assets/cdk/jest.config.js delete mode 100644 src/assets/cdk/npmignore.template delete mode 100644 src/assets/cdk/test/cdk.test.ts diff --git a/src/assets/cdk/.prettierrc b/src/assets/cdk/.prettierrc deleted file mode 100644 index 5563802ee..000000000 --- a/src/assets/cdk/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "trailingComma": "es5", - "printWidth": 120, - "tabWidth": 2, - "semi": true, - "singleQuote": true, - "arrowParens": "avoid" -} diff --git a/src/assets/cdk/README.md b/src/assets/cdk/README.md index be548731f..e2a65ea3d 100644 --- a/src/assets/cdk/README.md +++ b/src/assets/cdk/README.md @@ -1,29 +1,49 @@ -# AgentCore CDK Project +# AgentCore CDK app -This CDK project is managed by the AgentCore CLI. It deploys your agent infrastructure into AWS using the `@aws/agentcore-cdk` L3 constructs. +This CDK app is managed by the AgentCore CLI. It deploys everything declared in `agentcore/agentcore.json` into AWS +through the `@aws/agentcore-cdk` constructs. It is two files: -## Structure +- `bin/cdk.ts` — the entry point. It reads the project once (`readAgentCoreProject`), creates one stack per deployment + target (`resolveTargetStacks`), and turns `agentcore.json` into the application's props (`transformAgentCoreJson`). + Everything about how `agentcore.json` is interpreted lives in the library, so it changes with the library version, not + with this file. +- `lib/cdk-stack.ts` — `AgentCoreStack`, which instantiates one `AgentCoreApplication`. This is the file you edit. -- `bin/cdk.ts` — Entry point. Reads project configuration from `agentcore/` and creates a stack per deployment target. -- `lib/cdk-stack.ts` — Defines `AgentCoreStack`, which wraps the `AgentCoreApplication` L3 construct. -- `test/cdk.test.ts` — Unit tests for stack synthesis. +## The CLI runs it for you -## Useful commands +You normally do not run this app directly: -- `npm run build` compile TypeScript to JavaScript -- `npm run test` run unit tests -- `npx cdk synth` emit the synthesized CloudFormation template -- `npx cdk deploy` deploy this stack to your default AWS account/region -- `npx cdk diff` compare deployed stack with current state +```bash +agentcore project build # synthesizes the CloudFormation templates into agentcore/cdk/cdk.out +agentcore project deploy # synthesizes, then deploys the stack for the selected target +agentcore project status # reports the resources agentcore.json declares +``` -## Usage +`npm run build` compiles the app, and `npx cdk synth` / `npx cdk diff` work from this directory too. -You typically don't need to interact with this directory directly. The AgentCore CLI handles synthesis and deployment: +## Extending the stack - +Add your own AWS resources in `lib/cdk-stack.ts` after the application and wire them to a runtime or harness through the +application's accessors. Runtimes and harnesses implement `iam.IGrantable`, so any AWS L2 grant accepts them, and they +expose `grantRead` / `grantWrite` / `grantReadWrite` for DynamoDB tables, S3 buckets and Secrets Manager secrets plus +`addEnvironmentVariable`: -```bash -agentcore deploy # synthesizes and deploys via CDK -agentcore status # checks deployment status +```ts +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; + +const orders = new dynamodb.Table(this, 'Orders', { + partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING }, +}); +const checkout = this.application.runtime('checkout'); // or this.application.harness('support') +checkout.grantReadWrite(orders); // updates the runtime's execution role +checkout.addEnvironmentVariable('ORDERS_TABLE', orders.tableName); +orders.grantReadData(this.application.harness('support')); // any AWS L2 grant works too ``` + +Then run `agentcore project deploy` again. An unknown name fails at synth and lists the names that exist. + +If a runtime or harness is configured with an `executionRoleArn`, CDK cannot modify that imported role: every grant +emits a synth-time warning listing the permissions that were not attached, and the role must already carry them. + +`agentcore project status` reports only the resources `agentcore.json` declares; resources you add here are visible +through CloudFormation (`aws cloudformation describe-stack-resources`). diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 9e308d1de..d30e8c505 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -1,181 +1,29 @@ #!/usr/bin/env node -import { AgentCoreStack, type HarnessConfig } from '../lib/cdk-stack'; -import { ConfigIO, HarnessSpecSchema, type AwsDeploymentTarget } from '@aws/agentcore-cdk'; -import { App, type Environment } from 'aws-cdk-lib'; -import * as path from 'path'; -import * as fs from 'fs'; - -function toEnvironment(target: AwsDeploymentTarget): Environment { - return { - account: target.account, - region: target.region, - }; -} - -function sanitize(name: string): string { - return name.replace(/_/g, '-'); -} - -function toStackName(projectName: string, targetName: string): string { - return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`; -} - -// The vended CDK project compiles against the published @aws/agentcore-cdk schema -// type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments, harnesses, -// gateway fields). This alias documents each read of those not-yet-published fields. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type SpecWithLatestFields = any; - -// Extract MCP configuration from the project spec. Gateway fields are stored in -// agentcore.json but may not yet be on the published spec type, so they are read -// off the loosened alias. -function resolveMcpSpec(spec: SpecWithLatestFields) { - return spec.agentCoreGateways?.length - ? { - agentCoreGateways: spec.agentCoreGateways, - mcpRuntimeTools: spec.mcpRuntimeTools, - unassignedTargets: spec.unassignedTargets, - } - : undefined; -} - -// Read non-S3 KB connector-config files and return their parsed contents keyed by -// the data source's connectorConfigFile path. The L3 does not read files; it -// expects these parsed connectorParameters verbatim. -function resolveConnectorParametersByFile( - spec: SpecWithLatestFields, - projectRoot: string -): Record> { - const connectorParametersByFile: Record> = {}; - for (const kb of spec.knowledgeBases ?? []) { - for (const ds of kb.dataSources ?? []) { - if (ds.type !== 'S3' && ds.connectorConfigFile) { - const abs = path.resolve(projectRoot, ds.connectorConfigFile); - try { - connectorParametersByFile[ds.connectorConfigFile] = JSON.parse(fs.readFileSync(abs, 'utf-8')); - } catch (err) { - throw new Error( - `Could not read connector config '${ds.connectorConfigFile}' for knowledge base '${kb.name}' at ${abs}: ${err instanceof Error ? err.message : err}` - ); - } - } - } - } - return connectorParametersByFile; -} - -// Synthesize a HarnessConfig for each harness entry in the spec. The full validated -// spec drives the AWS::BedrockAgentCore::Harness CFN resource; the role-scoped -// fields drive the IAM role + container build. -function resolveHarnessConfigs(spec: SpecWithLatestFields, projectRoot: string): HarnessConfig[] { - const harnessConfigs: HarnessConfig[] = []; - for (const entry of spec.harnesses ?? []) { - const harnessDir = path.resolve(projectRoot, entry.path); - const harnessPath = path.resolve(harnessDir, 'harness.json'); - try { - const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8'))); - harnessConfigs.push({ - name: entry.name, - executionRoleArn: harnessSpec.executionRoleArn, - // Only an `existing` memory ref carries a name to wire IAM against; managed memory is - // owned by the harness (no sibling) and disabled has none — both resolve to undefined. - memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined, - containerUri: harnessSpec.containerUri, - hasDockerfile: !!harnessSpec.dockerfile, - dockerfile: harnessSpec.dockerfile, - codeLocation: harnessSpec.dockerfile ? harnessDir : undefined, - tools: harnessSpec.tools, - skills: harnessSpec.skills, - apiKeyArn: harnessSpec.model?.apiKeyArn, - efsAccessPoints: harnessSpec.efsAccessPoints, - s3AccessPoints: harnessSpec.s3AccessPoints, - apiFormat: harnessSpec.model?.apiFormat, - // Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource. - spec: harnessSpec, - harnessDir, - }); - } catch (err) { - throw new Error( - `Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}` - ); - } - } - return harnessConfigs; -} - -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(); - - // `project build` runs before a project has anywhere to deploy, so an empty target - // list is not an error: it synthesizes a single environment-agnostic stack, which is - // enough to typecheck the app and produce a template. Only a stack synthesized for a - // real target is a deploy candidate; the target tag below is what marks one. - const stackTargets: (AwsDeploymentTarget | undefined)[] = targets.length > 0 ? targets : [undefined]; - - const specAny: SpecWithLatestFields = spec; - const projectRoot = path.resolve(configRoot, '..'); - - const mcpSpec = resolveMcpSpec(specAny); - const connectorParametersByFile = resolveConnectorParametersByFile(specAny, projectRoot); - const harnessConfigs = resolveHarnessConfigs(specAny, projectRoot); - - // Read deployed state for credential ARNs (populated by pre-deploy identity setup). - // Under agentcore/.cli/ to match the released CLI's location. - let deployedState: Record | undefined; - try { - deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')); - } catch (err) { - // A missing file is the normal first-deploy case. A malformed one is not: - // surface it rather than silently synthesizing without the credential ARNs - // it holds (which would drop them from the stack). - if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err; - } +import { App } from 'aws-cdk-lib'; +import { readAgentCoreProject, resolveTargetStacks, transformAgentCoreJson } from '@aws/agentcore-cdk'; +import { AgentCoreStack } from '../lib/cdk-stack'; +try { + // The AgentCore CLI runs this app from agentcore/cdk/; readAgentCoreProject walks up to agentcore/. + const project = readAgentCoreProject(); const app = new App(); - - for (const target of stackTargets) { - // An environment-agnostic stack resolves its account and region from CloudFormation - // pseudo-parameters at deploy time instead of pinning them at synth time. - const env = target ? toEnvironment(target) : undefined; - const stackName = target ? toStackName(spec.name, target.name) : `AgentCore-${sanitize(spec.name)}`; - - // Extract credentials from deployed state for this target - const targetState = (deployedState as Record)?.targets as - Record> | undefined; - const targetResources = target - ? (targetState?.[target.name]?.resources as Record | undefined) - : undefined; - const credentials = targetResources?.credentials as - Record | undefined; - - new AgentCoreStack(app, stackName, { - spec, - mcpSpec, - credentials, - connectorParametersByFile, - harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined, - env, - description: target - ? `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})` - : `AgentCore stack for ${spec.name} (no deployment target configured)`, - // Only a stack synthesized for a real target carries the target tag, which is - // how deploy selects the stack to ship. - tags: { - 'agentcore:project-name': spec.name, - ...(target ? { 'agentcore:target-name': target.name } : {}), - }, + for (const stack of resolveTargetStacks({ + projectName: project.projectName, + targets: project.targets, + deployedState: project.deployedState, + })) { + new AgentCoreStack(app, stack.stackName, { + env: stack.env, + tags: stack.tags, + description: stack.description, + application: transformAgentCoreJson(project.agentCoreJson, { + projectRoot: project.projectRoot, + credentials: stack.credentials, + }), }); } - app.synth(); -} - -main().catch((error: unknown) => { +} catch (error) { console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error); process.exit(1); -}); +} diff --git a/src/assets/cdk/jest.config.js b/src/assets/cdk/jest.config.js deleted file mode 100644 index 0077a6547..000000000 --- a/src/assets/cdk/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - testEnvironment: 'node', - roots: ['/test'], - testMatch: ['**/*.test.ts'], - transform: { - '^.+\\.tsx?$': 'ts-jest', - }, - setupFilesAfterEnv: ['aws-cdk-lib/testhelpers/jest-autoclean'], -}; diff --git a/src/assets/cdk/lib/cdk-stack.ts b/src/assets/cdk/lib/cdk-stack.ts index 9592561d2..fd4a2a54d 100644 --- a/src/assets/cdk/lib/cdk-stack.ts +++ b/src/assets/cdk/lib/cdk-stack.ts @@ -1,94 +1,32 @@ -import { - AgentCoreApplication, - AgentCoreMcp, - AgentCorePayments, - type AgentCoreProjectSpec, - type AgentCoreMcpSpec, - type HarnessDeploymentConfig, -} from '@aws/agentcore-cdk'; -import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib'; +import { AgentCoreApplication, type AgentCoreApplicationProps } from '@aws/agentcore-cdk'; +import { Stack, type StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; -/** - * Harness deployment config: role-scoped fields (for IAM role + container build) - * plus the full validated spec + its config directory so the L3 construct can - * synthesize the AWS::BedrockAgentCore::Harness resource. - */ -export type HarnessConfig = HarnessDeploymentConfig; - export interface AgentCoreStackProps extends StackProps { - /** - * The AgentCore project specification containing agents, memories, and credentials. - */ - spec: AgentCoreProjectSpec; - /** - * The MCP specification containing gateways and servers. - */ - mcpSpec?: AgentCoreMcpSpec; - /** - * Credential provider ARNs from deployed state, keyed by credential name. - */ - credentials?: Record; - /** - * Harness role configurations. - */ - harnesses?: HarnessConfig[]; - /** - * Parsed connectorParameters for non-S3 KB data sources, keyed by - * connectorConfigFile path. Forwarded to AgentCoreApplication. - */ - connectorParametersByFile?: Record>; + /** Props for the AgentCore application, produced by transformAgentCoreJson from agentcore/agentcore.json. */ + application: AgentCoreApplicationProps; } /** - * CDK Stack that deploys AgentCore infrastructure. - * - * This is a thin wrapper that instantiates L3 constructs. - * All resource logic and outputs are contained within the L3 constructs. + * The stack the AgentCore CLI deploys. Everything declared in agentcore/agentcore.json is + * created by the AgentCoreApplication construct. Add your own resources below it and wire + * them to your runtimes and harnesses through the application's accessors. */ export class AgentCoreStack extends Stack { - /** The AgentCore application containing all agent environments */ public readonly application: AgentCoreApplication; constructor(scope: Construct, id: string, props: AgentCoreStackProps) { super(scope, id, props); - - const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile } = props; - - // Create AgentCoreApplication with all agents and harness roles - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const appProps: Record = { spec }; - if (harnesses?.length) { - appProps.harnesses = harnesses; - } - if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) { - appProps.connectorParametersByFile = connectorParametersByFile; - } - if (credentials) { - appProps.credentials = credentials; - } - this.application = new AgentCoreApplication(this, 'Application', appProps as any); - new AgentCorePayments(this, 'Payments', { - spec, - credentials, - agentCoreApplication: this.application, - }); - - // Create AgentCoreMcp if there are gateways configured - if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) { - new AgentCoreMcp(this, 'Mcp', { - projectName: spec.name, - mcpSpec, - agentCoreApplication: this.application, - credentials, - projectTags: spec.tags, - }); - } - - // Stack-level output - new CfnOutput(this, 'StackNameOutput', { - description: 'Name of the CloudFormation Stack', - value: this.stackName, - }); + this.application = new AgentCoreApplication(this, 'Application', props.application); + + // Example: give a runtime a DynamoDB table. + // + // import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; + // const orders = new dynamodb.Table(this, 'Orders', { + // partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING }, + // }); + // const checkout = this.application.runtime('checkout'); // or .harness('support') + // checkout.grantReadWrite(orders); + // checkout.addEnvironmentVariable('ORDERS_TABLE', orders.tableName); } } diff --git a/src/assets/cdk/npmignore.template b/src/assets/cdk/npmignore.template deleted file mode 100644 index c1d6d45dc..000000000 --- a/src/assets/cdk/npmignore.template +++ /dev/null @@ -1,6 +0,0 @@ -*.ts -!*.d.ts - -# CDK asset staging directory -.cdk.staging -cdk.out diff --git a/src/assets/cdk/package.json b/src/assets/cdk/package.json index 8fc72c2de..489427d66 100644 --- a/src/assets/cdk/package.json +++ b/src/assets/cdk/package.json @@ -7,23 +7,16 @@ "scripts": { "build": "tsc", "watch": "tsc -w", - "test": "jest", "cdk": "npm run build && cdk", - "clean": "rm -rf dist", - "format": "prettier --write .", - "format:check": "prettier --check ." + "clean": "rm -rf dist" }, "devDependencies": { - "@types/jest": "~29.5.14", "@types/node": "~24.13.3", - "jest": "~29.7.0", - "ts-jest": "~29.4.11", "aws-cdk": "~2.1126.0", - "prettier": "~3.9.5", "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "0.1.0-alpha.52", + "@aws/agentcore-cdk": "0.1.0-alpha.53", "aws-cdk-lib": "~2.266.0", "constructs": "~10.7.0" } diff --git a/src/assets/cdk/test/cdk.test.ts b/src/assets/cdk/test/cdk.test.ts deleted file mode 100644 index c9cd8eb80..000000000 --- a/src/assets/cdk/test/cdk.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import * as cdk from 'aws-cdk-lib'; -import { Match, Template } from 'aws-cdk-lib/assertions'; - -const originalCwd = process.cwd(); -const originalInitCwd = process.env.INIT_CWD; -const testRoot = mkdtempSync(join(tmpdir(), 'agentcore-cdk-test-')); -const testConfigDir = join(testRoot, 'agentcore'); -let AgentCoreStack: typeof import('../lib/cdk-stack').AgentCoreStack; - -beforeAll(async () => { - process.chdir(testRoot); - process.env.INIT_CWD = testRoot; - mkdirSync(testConfigDir, { recursive: true }); - writeFileSync(join(testConfigDir, 'agentcore.json'), '{}'); - ({ AgentCoreStack } = await import('../lib/cdk-stack')); -}); - -afterAll(() => { - process.chdir(originalCwd); - if (originalInitCwd === undefined) delete process.env.INIT_CWD; - else process.env.INIT_CWD = originalInitCwd; - rmSync(testRoot, { recursive: true, force: true }); -}); - -test('AgentCoreStack synthesizes with empty spec', () => { - const app = new cdk.App(); - const stack = new AgentCoreStack(app, 'TestStack', { - spec: { - name: 'testproject', - version: 1, - managedBy: 'CDK' as const, - runtimes: [], - memories: [], - credentials: [], - evaluators: [], - onlineEvalConfigs: [], - configBundles: [], - policyEngines: [], - payments: [], - agentCoreGateways: [], - mcpRuntimeTools: [], - unassignedTargets: [], - datasets: [], - knowledgeBases: [], - }, - }); - const template = Template.fromStack(stack); - template.hasOutput('StackNameOutput', { - Description: 'Name of the CloudFormation Stack', - }); -}); - -test('AgentCoreStack synthesizes manual and Quick Create payment connectors', () => { - const app = new cdk.App(); - const stack = new AgentCoreStack(app, 'TestStack', { - spec: { - name: 'testproject', - version: 1, - managedBy: 'CDK' as const, - runtimes: [], - memories: [], - credentials: [ - { - authorizerType: 'PaymentCredentialProvider', - name: 'coinbase', - provider: 'CoinbaseCDP', - }, - ], - evaluators: [], - onlineEvalConfigs: [], - configBundles: [], - policyEngines: [], - payments: [ - { - name: 'Payments', - authorizerType: 'AWS_IAM', - connectors: [ - { - name: 'Manual', - provider: 'CoinbaseCDP', - credentialName: 'coinbase', - }, - { - name: 'Quick', - provider: 'CoinbaseCDP', - provisionMode: 'QUICK_CREATE', - }, - ], - }, - ], - agentCoreGateways: [], - mcpRuntimeTools: [], - unassignedTargets: [], - datasets: [], - knowledgeBases: [], - }, - credentials: { - coinbase: { - credentialProviderArn: - 'arn:aws:bedrock-agentcore:us-east-1:123456789012:token-vault/default/paymentcredentialprovider/coinbase', - }, - }, - }); - const template = Template.fromStack(stack); - - template.resourceCountIs('AWS::BedrockAgentCore::PaymentConnector', 2); - template.hasResourceProperties('AWS::BedrockAgentCore::PaymentConnector', { - ConnectorName: 'Manual', - ProvisionMode: Match.absent(), - }); - template.hasResourceProperties('AWS::BedrockAgentCore::PaymentConnector', { - ConnectorName: 'Quick', - ConnectorType: 'CoinbaseCDP', - ProvisionMode: 'QUICK_CREATE', - CredentialProviderConfigurations: [], - }); -}); diff --git a/src/assets/cdk/tsconfig.json b/src/assets/cdk/tsconfig.json index c70b0d444..6ff8be70a 100644 --- a/src/assets/cdk/tsconfig.json +++ b/src/assets/cdk/tsconfig.json @@ -23,6 +23,6 @@ "rootDir": ".", "outDir": "dist" }, - "include": ["bin/**/*", "lib/**/*", "test/**/*"], + "include": ["bin/**/*", "lib/**/*"], "exclude": ["node_modules", "cdk.out", "dist"] } diff --git a/src/core/observability.ts b/src/core/observability.ts index f014ee400..a96bb6bd6 100644 --- a/src/core/observability.ts +++ b/src/core/observability.ts @@ -212,9 +212,10 @@ const describeStackOutputsWithSdk: DescribeStackOutputs = async (stackName, regi }; // The vended CDK app names project stacks `AgentCore--` with -// underscores sanitized to hyphens (see src/assets/cdk/bin/cdk.ts). Deriving it -// here lets deployed state be read live from CloudFormation without a local -// state file. +// underscores sanitized to hyphens; the rule lives in @aws/agentcore-cdk's +// resolveTargetStacks (src/cdk/project/target-stacks.ts), which the vended +// bin/cdk.ts calls. Deriving it here lets deployed state be read live from +// CloudFormation without a local state file. function targetStackName(projectName: string, targetName: string): string { const sanitize = (name: string) => name.replace(/_/g, "-"); return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`; diff --git a/src/core/project/__snapshots__/manager.test.ts.snap b/src/core/project/__snapshots__/manager.test.ts.snap index a079f027d..368eeb7a3 100644 --- a/src/core/project/__snapshots__/manager.test.ts.snap +++ b/src/core/project/__snapshots__/manager.test.ts.snap @@ -7,15 +7,11 @@ exports[`FsProjectManager.create scaffolds the expected file tree into a fresh d "agentcore/agentcore.json", "agentcore/aws-targets.json", "agentcore/cdk/.gitignore", - "agentcore/cdk/.npmignore", - "agentcore/cdk/.prettierrc", "agentcore/cdk/README.md", "agentcore/cdk/bin/cdk.ts", "agentcore/cdk/cdk.json", - "agentcore/cdk/jest.config.js", "agentcore/cdk/lib/cdk-stack.ts", "agentcore/cdk/package.json", - "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", "app/agent_python_minimal/README.md", "app/agent_python_minimal/main.py", @@ -31,15 +27,11 @@ exports[`FsProjectManager.create snapshots the Strands project manifest and runt "agentcore/agentcore.json", "agentcore/aws-targets.json", "agentcore/cdk/.gitignore", - "agentcore/cdk/.npmignore", - "agentcore/cdk/.prettierrc", "agentcore/cdk/README.md", "agentcore/cdk/bin/cdk.ts", "agentcore/cdk/cdk.json", - "agentcore/cdk/jest.config.js", "agentcore/cdk/lib/cdk-stack.ts", "agentcore/cdk/package.json", - "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", "app/agent_python_strands/.gitignore", "app/agent_python_strands/README.md", @@ -106,15 +98,11 @@ exports[`FsProjectManager.create snapshots the Strands TypeScript project manife "agentcore/agentcore.json", "agentcore/aws-targets.json", "agentcore/cdk/.gitignore", - "agentcore/cdk/.npmignore", - "agentcore/cdk/.prettierrc", "agentcore/cdk/README.md", "agentcore/cdk/bin/cdk.ts", "agentcore/cdk/cdk.json", - "agentcore/cdk/jest.config.js", "agentcore/cdk/lib/cdk-stack.ts", "agentcore/cdk/package.json", - "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", "app/agent_typescript_strands/.gitignore", "app/agent_typescript_strands/README.md", @@ -181,15 +169,11 @@ exports[`FsProjectManager.create snapshots the Strands A2A project manifest and "agentcore/agentcore.json", "agentcore/aws-targets.json", "agentcore/cdk/.gitignore", - "agentcore/cdk/.npmignore", - "agentcore/cdk/.prettierrc", "agentcore/cdk/README.md", "agentcore/cdk/bin/cdk.ts", "agentcore/cdk/cdk.json", - "agentcore/cdk/jest.config.js", "agentcore/cdk/lib/cdk-stack.ts", "agentcore/cdk/package.json", - "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", "app/a2a_python_strands/.gitignore", "app/a2a_python_strands/README.md", @@ -256,15 +240,11 @@ exports[`FsProjectManager.create snapshots the LangChain project manifest and ru "agentcore/agentcore.json", "agentcore/aws-targets.json", "agentcore/cdk/.gitignore", - "agentcore/cdk/.npmignore", - "agentcore/cdk/.prettierrc", "agentcore/cdk/README.md", "agentcore/cdk/bin/cdk.ts", "agentcore/cdk/cdk.json", - "agentcore/cdk/jest.config.js", "agentcore/cdk/lib/cdk-stack.ts", "agentcore/cdk/package.json", - "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", "app/agent_python_langchain/.gitignore", "app/agent_python_langchain/README.md", diff --git a/src/core/project/backends/cdk.ts b/src/core/project/backends/cdk.ts index 3d0f4d51d..a977ddc1e 100644 --- a/src/core/project/backends/cdk.ts +++ b/src/core/project/backends/cdk.ts @@ -76,7 +76,7 @@ type StackDescriber = typeof describeStack; */ const MAX_ERROR_OUTPUT_LINES = 20; -// Payment logical ids drop underscores the same way the template's toCdkId does. +// Payment output keys drop underscores the same way AgentCorePayments' toCdkId does. function cdkId(name: string): string { return name.replace(/_/g, ""); } @@ -492,14 +492,14 @@ export class CdkBackend implements ProjectBackend { case "config-bundle": return byExportName("ConfigBundle", name, "Arn"); case "payment-manager": - // The CLI template writes the payment outputs. It does not set an - // exportName on them. Therefore match on the OutputKey. The template - // makes that key from the manager name. - // See src/assets/cdk/lib/cdk-stack.ts + // @aws/agentcore-cdk's AgentCorePayments construct writes the payment + // outputs at stack scope without an exportName, under keys built from + // the manager name with underscores removed (its toCdkId). Therefore + // match on the OutputKey. return byOutputKey(`Payment${cdkId(name)}ManagerArn`); case "payment-connector": - // The same template does not set an exportName. Therefore match on the - // OutputKey. The template writes only a connector id, and never an ARN. + // The same construct writes only a connector id, never an ARN, again + // without an exportName. Therefore match on the OutputKey. return byOutputKey(`Payment${cdkId(owner ?? "")}${cdkId(name)}ConnectorId`); case "credential": // The CLI creates credential providers imperatively. The stack does not diff --git a/src/handlers/project/create/pathLimit.test.ts b/src/handlers/project/create/pathLimit.test.ts index 187583bb7..4ea8f65fe 100644 --- a/src/handlers/project/create/pathLimit.test.ts +++ b/src/handlers/project/create/pathLimit.test.ts @@ -13,3 +13,13 @@ test.each([ if (throws) expect(check).toThrow(InputValidationError); else expect(check).not.toThrow(); }); + +// The deepest file npm installs under agentcore/cdk is 155 characters below the project root, +// so a 104-character root is the longest that still fits under Windows' 260-character MAX_PATH. +test("allows a 104-character project root and refuses 105 on Windows", () => { + const root = (length: number) => "C:\\" + "x".repeat(length - 3 - 5); // leaves room for "\\Demo" + expect(() => assertProjectPathFits("Demo", "win32", { cwd: root(104) })).not.toThrow(); + expect(() => assertProjectPathFits("Demo", "win32", { cwd: root(105) })).toThrow( + /105 characters/, + ); +}); diff --git a/src/handlers/project/create/pathLimit.ts b/src/handlers/project/create/pathLimit.ts index 5d3fc26fe..df11c3cdf 100644 --- a/src/handlers/project/create/pathLimit.ts +++ b/src/handlers/project/create/pathLimit.ts @@ -1,14 +1,22 @@ import { join } from "node:path"; import { InputValidationError } from "../../../errors"; -const MAX_WINDOWS_PROJECT_PATH = 150; +/** + Deepest file a fresh `npm install` under agentcore/cdk writes, measured from the project root: + `agentcore/cdk/node_modules/aws-cdk-lib/product-stack-snapshots/nested/<...>.v1.product.template.json` + is 155 characters (aws-cdk-lib ~2.266 with @aws/agentcore-cdk 0.1.0-alpha.53). The depth comes + from aws-cdk-lib's own shipped fixtures, so it does not move when the vended app changes. +**/ +const DEEPEST_INSTALLED_PATH = 155; /** - Windows caps paths at 260 characters unless long paths are enabled, and npm - install under the CDK app needs about 100 of them, so a deep project root - fails half way through scaffolding. Refusing up front leaves nothing behind. - `alternative` names a way out the caller offers besides a shorter directory. + Windows caps paths at 260 characters unless long paths are enabled, so a project root longer + than 260 - 1 (separator) - DEEPEST_INSTALLED_PATH fails half way through scaffolding. Refusing + up front leaves nothing behind. `alternative` names a way out the caller offers besides a + shorter directory. **/ +const MAX_WINDOWS_PROJECT_PATH = 260 - 1 - DEEPEST_INSTALLED_PATH; + export function assertProjectPathFits( name: string, platform: NodeJS.Platform, From 9deed5d89d560e359716ef57db812a40c7f0194e Mon Sep 17 00:00:00 2001 From: Alexander Richey Date: Wed, 9 Sep 2026 01:30:22 +0000 Subject: [PATCH 2/2] docs(project): describe extending the vended CDK stack The project create entry in the command tree says what agentcore/cdk contains now, a new "Extending the CDK app" subsection walks through lib/cdk-stack.ts, the accessors, grants, environment variables and redeploying (noting that project status reports only the resources agentcore.json declares), and the Windows path note carries the measured depth. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KEWrUu52DmcWE3sx4VEhKL --- README.md | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 79822cc32..ef8240f29 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,9 @@ agentcore # interactive TUI ├── project # manage an AgentCore project (scaffold → deploy) │ ├── create # create a project: a managed harness by default, │ │ # or scaffolded runtime code via --template; -│ │ # bare `project create` opens an interactive wizard +│ │ # bare `project create` opens an interactive wizard. +│ │ # agentcore/cdk/ holds a two-file CDK app on +│ │ # @aws/agentcore-cdk (bin/cdk.ts, lib/cdk-stack.ts) │ ├── add # add a resource to the project (runtime, harness, memory, …) │ ├── export │ │ └── harness # convert a harness into an editable Strands runtime agent @@ -342,6 +344,38 @@ Source-aware values: any field flag documented as such accepts the value inline, `file://` convention). A command reads stdin from at most one flag. For example, `--instructions file://order-quality.txt` or `--instructions -`. +### Extending the CDK app + +`agentcore/cdk/` is a CDK app of two files. `bin/cdk.ts` reads the project once +(`readAgentCoreProject`), makes one stack per deployment target +(`resolveTargetStacks`) and turns `agentcore.json` into the application's props +(`transformAgentCoreJson`) — all three come from `@aws/agentcore-cdk`, so how +`agentcore.json` is interpreted changes with the library version, not with code +on your disk. `lib/cdk-stack.ts` instantiates one `AgentCoreApplication`; it is +the file you edit. Add your own resources after the application and wire them to +a runtime or harness through the application's accessors: runtimes and harnesses +implement `iam.IGrantable`, so any AWS L2 grant accepts them, and they expose +`grantRead` / `grantWrite` / `grantReadWrite` (DynamoDB tables, S3 buckets, +Secrets Manager secrets) and `addEnvironmentVariable`: + +```ts +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; + +const orders = new dynamodb.Table(this, "Orders", { + partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING }, +}); +const checkout = this.application.runtime("checkout"); // or this.application.harness('support') +checkout.grantReadWrite(orders); +checkout.addEnvironmentVariable("ORDERS_TABLE", orders.tableName); +orders.grantReadData(this.application.harness("support")); // any AWS L2 grant works too +``` + +Redeploy with `agentcore project deploy`. An unknown name fails at synth and lists +the names that exist; a runtime or harness configured with an `executionRoleArn` +warns at synth about every grant CDK could not attach to the imported role. Note +that `agentcore project status` reports only the resources `agentcore.json` +declares, not the ones you add in the stack. + ### Invoke a Gateway Gateway Invoke is a project-independent HTTP request command with headless and @@ -967,8 +1001,10 @@ npm i -g ./aws-agentcore-0.28.1.tgz output while text is selected (the title bar shows `Select`). Press `Esc`. Windows Terminal does not do this. - **`project create` refuses a long path**: Windows caps paths at 260 characters - unless `LongPathsEnabled` is set, and the CDK app's `node_modules` needs about - 100 of them. Create the project higher in the tree or enable long paths. + unless `LongPathsEnabled` is set, and the CDK app's `node_modules` puts its + deepest file 155 characters below the project root (aws-cdk-lib's own shipped + fixtures), so the project root must be at most 104 characters. Create the + project higher in the tree or enable long paths. # Build