Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 0 additions & 8 deletions src/assets/cdk/.prettierrc

This file was deleted.

58 changes: 39 additions & 19 deletions src/assets/cdk/README.md
Original file line number Diff line number Diff line change
@@ -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

<!-- TODO: revisit these commands once the project CLI surface is final —
they may need a project prefix (e.g. --project / cwd) to disambiguate. -->
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`).
194 changes: 21 additions & 173 deletions src/assets/cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, unknown>> {
const connectorParametersByFile: Record<string, Record<string, unknown>> = {};
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<string, unknown> | 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<string, unknown>)?.targets as
Record<string, Record<string, unknown>> | undefined;
const targetResources = target
? (targetState?.[target.name]?.resources as Record<string, unknown> | undefined)
: undefined;
const credentials = targetResources?.credentials as
Record<string, { credentialProviderArn: string; clientSecretArn?: string }> | 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);
});
}
9 changes: 0 additions & 9 deletions src/assets/cdk/jest.config.js

This file was deleted.

Loading
Loading