Skip to content

Commit de54554

Browse files
feat(project): vend a thin CDK app built on transformAgentCoreJson (#2249)
* feat(project): vend a thin CDK app built on transformAgentCoreJson 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEWrUu52DmcWE3sx4VEhKL * 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEWrUu52DmcWE3sx4VEhKL --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent bfaeeb9 commit de54554

14 files changed

Lines changed: 149 additions & 459 deletions

File tree

‎README.md‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ agentcore # interactive TUI
138138
├── project # manage an AgentCore project (scaffold → deploy)
139139
│ ├── create # create a project: a managed harness by default,
140140
│ │ # or scaffolded runtime code via --template;
141-
│ │ # bare `project create` opens an interactive wizard
141+
│ │ # bare `project create` opens an interactive wizard.
142+
│ │ # agentcore/cdk/ holds a two-file CDK app on
143+
│ │ # @aws/agentcore-cdk (bin/cdk.ts, lib/cdk-stack.ts)
142144
│ ├── add # add a resource to the project (runtime, harness, memory, …)
143145
│ ├── export
144146
│ │ └── harness # convert a harness into an editable Strands runtime agent
@@ -475,6 +477,38 @@ Source-aware values: any field flag documented as such accepts the value inline,
475477
`file://` convention). A command reads stdin from at most one flag. For example,
476478
`--instructions file://order-quality.txt` or `--instructions -`.
477479

480+
### Extending the CDK app
481+
482+
`agentcore/cdk/` is a CDK app of two files. `bin/cdk.ts` reads the project once
483+
(`readAgentCoreProject`), makes one stack per deployment target
484+
(`resolveTargetStacks`) and turns `agentcore.json` into the application's props
485+
(`transformAgentCoreJson`) — all three come from `@aws/agentcore-cdk`, so how
486+
`agentcore.json` is interpreted changes with the library version, not with code
487+
on your disk. `lib/cdk-stack.ts` instantiates one `AgentCoreApplication`; it is
488+
the file you edit. Add your own resources after the application and wire them to
489+
a runtime or harness through the application's accessors: runtimes and harnesses
490+
implement `iam.IGrantable`, so any AWS L2 grant accepts them, and they expose
491+
`grantRead` / `grantWrite` / `grantReadWrite` (DynamoDB tables, S3 buckets,
492+
Secrets Manager secrets) and `addEnvironmentVariable`:
493+
494+
```ts
495+
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
496+
497+
const orders = new dynamodb.Table(this, "Orders", {
498+
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
499+
});
500+
const checkout = this.application.runtime("checkout"); // or this.application.harness('support')
501+
checkout.grantReadWrite(orders);
502+
checkout.addEnvironmentVariable("ORDERS_TABLE", orders.tableName);
503+
orders.grantReadData(this.application.harness("support")); // any AWS L2 grant works too
504+
```
505+
506+
Redeploy with `agentcore project deploy`. An unknown name fails at synth and lists
507+
the names that exist; a runtime or harness configured with an `executionRoleArn`
508+
warns at synth about every grant CDK could not attach to the imported role. Note
509+
that `agentcore project status` reports only the resources `agentcore.json`
510+
declares, not the ones you add in the stack.
511+
478512
### Invoke a Gateway
479513

480514
Gateway Invoke is a project-independent HTTP request command with headless and
@@ -1100,8 +1134,10 @@ npm i -g ./aws-agentcore-0.28.1.tgz
11001134
output while text is selected (the title bar shows `Select`). Press `Esc`.
11011135
Windows Terminal does not do this.
11021136
- **`project create` refuses a long path**: Windows caps paths at 260 characters
1103-
unless `LongPathsEnabled` is set, and the CDK app's `node_modules` needs about
1104-
100 of them. Create the project higher in the tree or enable long paths.
1137+
unless `LongPathsEnabled` is set, and the CDK app's `node_modules` puts its
1138+
deepest file 155 characters below the project root (aws-cdk-lib's own shipped
1139+
fixtures), so the project root must be at most 104 characters. Create the
1140+
project higher in the tree or enable long paths.
11051141

11061142
# Build
11071143

‎src/assets/cdk/.prettierrc‎

Lines changed: 0 additions & 8 deletions
This file was deleted.

‎src/assets/cdk/README.md‎

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,49 @@
1-
# AgentCore CDK Project
1+
# AgentCore CDK app
22

3-
This CDK project is managed by the AgentCore CLI. It deploys your agent infrastructure into AWS using the `@aws/agentcore-cdk` L3 constructs.
3+
This CDK app is managed by the AgentCore CLI. It deploys everything declared in `agentcore/agentcore.json` into AWS
4+
through the `@aws/agentcore-cdk` constructs. It is two files:
45

5-
## Structure
6+
- `bin/cdk.ts` — the entry point. It reads the project once (`readAgentCoreProject`), creates one stack per deployment
7+
target (`resolveTargetStacks`), and turns `agentcore.json` into the application's props (`transformAgentCoreJson`).
8+
Everything about how `agentcore.json` is interpreted lives in the library, so it changes with the library version, not
9+
with this file.
10+
- `lib/cdk-stack.ts` — `AgentCoreStack`, which instantiates one `AgentCoreApplication`. This is the file you edit.
611

7-
- `bin/cdk.ts` — Entry point. Reads project configuration from `agentcore/` and creates a stack per deployment target.
8-
- `lib/cdk-stack.ts` — Defines `AgentCoreStack`, which wraps the `AgentCoreApplication` L3 construct.
9-
- `test/cdk.test.ts` — Unit tests for stack synthesis.
12+
## The CLI runs it for you
1013

11-
## Useful commands
14+
You normally do not run this app directly:
1215

13-
- `npm run build` compile TypeScript to JavaScript
14-
- `npm run test` run unit tests
15-
- `npx cdk synth` emit the synthesized CloudFormation template
16-
- `npx cdk deploy` deploy this stack to your default AWS account/region
17-
- `npx cdk diff` compare deployed stack with current state
16+
```bash
17+
agentcore project build # synthesizes the CloudFormation templates into agentcore/cdk/cdk.out
18+
agentcore project deploy # synthesizes, then deploys the stack for the selected target
19+
agentcore project status # reports the resources agentcore.json declares
20+
```
1821

19-
## Usage
22+
`npm run build` compiles the app, and `npx cdk synth` / `npx cdk diff` work from this directory too.
2023

21-
You typically don't need to interact with this directory directly. The AgentCore CLI handles synthesis and deployment:
24+
## Extending the stack
2225

23-
<!-- TODO: revisit these commands once the project CLI surface is final —
24-
they may need a project prefix (e.g. --project / cwd) to disambiguate. -->
26+
Add your own AWS resources in `lib/cdk-stack.ts` after the application and wire them to a runtime or harness through the
27+
application's accessors. Runtimes and harnesses implement `iam.IGrantable`, so any AWS L2 grant accepts them, and they
28+
expose `grantRead` / `grantWrite` / `grantReadWrite` for DynamoDB tables, S3 buckets and Secrets Manager secrets plus
29+
`addEnvironmentVariable`:
2530

26-
```bash
27-
agentcore deploy # synthesizes and deploys via CDK
28-
agentcore status # checks deployment status
31+
```ts
32+
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
33+
34+
const orders = new dynamodb.Table(this, 'Orders', {
35+
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
36+
});
37+
const checkout = this.application.runtime('checkout'); // or this.application.harness('support')
38+
checkout.grantReadWrite(orders); // updates the runtime's execution role
39+
checkout.addEnvironmentVariable('ORDERS_TABLE', orders.tableName);
40+
orders.grantReadData(this.application.harness('support')); // any AWS L2 grant works too
2941
```
42+
43+
Then run `agentcore project deploy` again. An unknown name fails at synth and lists the names that exist.
44+
45+
If a runtime or harness is configured with an `executionRoleArn`, CDK cannot modify that imported role: every grant
46+
emits a synth-time warning listing the permissions that were not attached, and the role must already carry them.
47+
48+
`agentcore project status` reports only the resources `agentcore.json` declares; resources you add here are visible
49+
through CloudFormation (`aws cloudformation describe-stack-resources`).

‎src/assets/cdk/bin/cdk.ts‎

Lines changed: 21 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,29 @@
11
#!/usr/bin/env node
2-
import { AgentCoreStack, type HarnessConfig } from '../lib/cdk-stack';
3-
import { ConfigIO, HarnessSpecSchema, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
4-
import { App, type Environment } from 'aws-cdk-lib';
5-
import * as path from 'path';
6-
import * as fs from 'fs';
7-
8-
function toEnvironment(target: AwsDeploymentTarget): Environment {
9-
return {
10-
account: target.account,
11-
region: target.region,
12-
};
13-
}
14-
15-
function sanitize(name: string): string {
16-
return name.replace(/_/g, '-');
17-
}
18-
19-
function toStackName(projectName: string, targetName: string): string {
20-
return `AgentCore-${sanitize(projectName)}-${sanitize(targetName)}`;
21-
}
22-
23-
// The vended CDK project compiles against the published @aws/agentcore-cdk schema
24-
// type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments, harnesses,
25-
// gateway fields). This alias documents each read of those not-yet-published fields.
26-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27-
type SpecWithLatestFields = any;
28-
29-
// Extract MCP configuration from the project spec. Gateway fields are stored in
30-
// agentcore.json but may not yet be on the published spec type, so they are read
31-
// off the loosened alias.
32-
function resolveMcpSpec(spec: SpecWithLatestFields) {
33-
return spec.agentCoreGateways?.length
34-
? {
35-
agentCoreGateways: spec.agentCoreGateways,
36-
toolRuntimes: spec.toolRuntimes,
37-
unassignedTargets: spec.unassignedTargets,
38-
}
39-
: undefined;
40-
}
41-
42-
// Read non-S3 KB connector-config files and return their parsed contents keyed by
43-
// the data source's connectorConfigFile path. The L3 does not read files; it
44-
// expects these parsed connectorParameters verbatim.
45-
function resolveConnectorParametersByFile(
46-
spec: SpecWithLatestFields,
47-
projectRoot: string
48-
): Record<string, Record<string, unknown>> {
49-
const connectorParametersByFile: Record<string, Record<string, unknown>> = {};
50-
for (const kb of spec.knowledgeBases ?? []) {
51-
for (const ds of kb.dataSources ?? []) {
52-
if (ds.type !== 'S3' && ds.connectorConfigFile) {
53-
const abs = path.resolve(projectRoot, ds.connectorConfigFile);
54-
try {
55-
connectorParametersByFile[ds.connectorConfigFile] = JSON.parse(fs.readFileSync(abs, 'utf-8'));
56-
} catch (err) {
57-
throw new Error(
58-
`Could not read connector config '${ds.connectorConfigFile}' for knowledge base '${kb.name}' at ${abs}: ${err instanceof Error ? err.message : err}`
59-
);
60-
}
61-
}
62-
}
63-
}
64-
return connectorParametersByFile;
65-
}
66-
67-
// Synthesize a HarnessConfig for each harness entry in the spec. The full validated
68-
// spec drives the AWS::BedrockAgentCore::Harness CFN resource; the role-scoped
69-
// fields drive the IAM role + container build.
70-
function resolveHarnessConfigs(spec: SpecWithLatestFields, projectRoot: string): HarnessConfig[] {
71-
const harnessConfigs: HarnessConfig[] = [];
72-
for (const entry of spec.harnesses ?? []) {
73-
const harnessDir = path.resolve(projectRoot, entry.path);
74-
const harnessPath = path.resolve(harnessDir, 'harness.json');
75-
try {
76-
const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
77-
harnessConfigs.push({
78-
name: entry.name,
79-
executionRoleArn: harnessSpec.executionRoleArn,
80-
// Only an `existing` memory ref carries a name to wire IAM against; managed memory is
81-
// owned by the harness (no sibling) and disabled has none — both resolve to undefined.
82-
memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined,
83-
containerUri: harnessSpec.containerUri,
84-
hasDockerfile: !!harnessSpec.dockerfile,
85-
dockerfile: harnessSpec.dockerfile,
86-
codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
87-
tools: harnessSpec.tools,
88-
skills: harnessSpec.skills,
89-
apiKeyArn: harnessSpec.model?.apiKeyArn,
90-
efsAccessPoints: harnessSpec.efsAccessPoints,
91-
s3AccessPoints: harnessSpec.s3AccessPoints,
92-
apiFormat: harnessSpec.model?.apiFormat,
93-
// Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
94-
spec: harnessSpec,
95-
harnessDir,
96-
});
97-
} catch (err) {
98-
throw new Error(
99-
`Could not read harness.json for "${entry.name}" at ${harnessPath}: ${err instanceof Error ? err.message : err}`
100-
);
101-
}
102-
}
103-
return harnessConfigs;
104-
}
105-
106-
async function main() {
107-
// Config root is parent of cdk/ directory. The CLI sets process.cwd() to agentcore/cdk/.
108-
const configRoot = path.resolve(process.cwd(), '..');
109-
const configIO = new ConfigIO({ baseDir: configRoot });
110-
111-
const spec = await configIO.readProjectSpec();
112-
const targets = await configIO.readAWSDeploymentTargets();
113-
114-
// `project build` runs before a project has anywhere to deploy, so an empty target
115-
// list is not an error: it synthesizes a single environment-agnostic stack, which is
116-
// enough to typecheck the app and produce a template. Only a stack synthesized for a
117-
// real target is a deploy candidate; the target tag below is what marks one.
118-
const stackTargets: (AwsDeploymentTarget | undefined)[] = targets.length > 0 ? targets : [undefined];
119-
120-
const specAny: SpecWithLatestFields = spec;
121-
const projectRoot = path.resolve(configRoot, '..');
122-
123-
const mcpSpec = resolveMcpSpec(specAny);
124-
const connectorParametersByFile = resolveConnectorParametersByFile(specAny, projectRoot);
125-
const harnessConfigs = resolveHarnessConfigs(specAny, projectRoot);
126-
127-
// Read deployed state for credential ARNs (populated by pre-deploy identity setup).
128-
// Under agentcore/.cli/ to match the released CLI's location.
129-
let deployedState: Record<string, unknown> | undefined;
130-
try {
131-
deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8'));
132-
} catch (err) {
133-
// A missing file is the normal first-deploy case. A malformed one is not:
134-
// surface it rather than silently synthesizing without the credential ARNs
135-
// it holds (which would drop them from the stack).
136-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
137-
}
2+
import { App } from 'aws-cdk-lib';
3+
import { readAgentCoreProject, resolveTargetStacks, transformAgentCoreJson } from '@aws/agentcore-cdk';
4+
import { AgentCoreStack } from '../lib/cdk-stack';
1385

6+
try {
7+
// The AgentCore CLI runs this app from agentcore/cdk/; readAgentCoreProject walks up to agentcore/.
8+
const project = readAgentCoreProject();
1399
const app = new App();
140-
141-
for (const target of stackTargets) {
142-
// An environment-agnostic stack resolves its account and region from CloudFormation
143-
// pseudo-parameters at deploy time instead of pinning them at synth time.
144-
const env = target ? toEnvironment(target) : undefined;
145-
const stackName = target ? toStackName(spec.name, target.name) : `AgentCore-${sanitize(spec.name)}`;
146-
147-
// Extract credentials from deployed state for this target
148-
const targetState = (deployedState as Record<string, unknown>)?.targets as
149-
Record<string, Record<string, unknown>> | undefined;
150-
const targetResources = target
151-
? (targetState?.[target.name]?.resources as Record<string, unknown> | undefined)
152-
: undefined;
153-
const credentials = targetResources?.credentials as
154-
Record<string, { credentialProviderArn: string; clientSecretArn?: string }> | undefined;
155-
156-
new AgentCoreStack(app, stackName, {
157-
spec,
158-
mcpSpec,
159-
credentials,
160-
connectorParametersByFile,
161-
harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
162-
env,
163-
description: target
164-
? `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`
165-
: `AgentCore stack for ${spec.name} (no deployment target configured)`,
166-
// Only a stack synthesized for a real target carries the target tag, which is
167-
// how deploy selects the stack to ship.
168-
tags: {
169-
'agentcore:project-name': spec.name,
170-
...(target ? { 'agentcore:target-name': target.name } : {}),
171-
},
10+
for (const stack of resolveTargetStacks({
11+
projectName: project.projectName,
12+
targets: project.targets,
13+
deployedState: project.deployedState,
14+
})) {
15+
new AgentCoreStack(app, stack.stackName, {
16+
env: stack.env,
17+
tags: stack.tags,
18+
description: stack.description,
19+
application: transformAgentCoreJson(project.agentCoreJson, {
20+
projectRoot: project.projectRoot,
21+
credentials: stack.credentials,
22+
}),
17223
});
17324
}
174-
17525
app.synth();
176-
}
177-
178-
main().catch((error: unknown) => {
26+
} catch (error) {
17927
console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
18028
process.exit(1);
181-
});
29+
}

‎src/assets/cdk/jest.config.js‎

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)