Skip to content

Commit 72cb257

Browse files
xxuamclaude
andauthored
feat(capacity-provider): add capacity-provider resource (Journey 1) (#2030)
* feat(capacity-provider): add capacity-provider resource (Journey 1) Add the CapacityProviderPrimitive (create/remove) for a customer-managed EC2 compute pool that runtimes can run on. Includes the Zod schema, primitive with CLI flags, interactive TUI add wizard and remove picker, telemetry (add/remove command-run + OperatingSystem enum), and deploy/status plumbing: CloudFormation output parsing, deployed-state schema, preflight empty-project guard, and ResourceGraph rendering. Docs (AGENTS.md, README, commands) and integ tests included. The @aws/agentcore-cdk pin stays at 0.1.0-alpha.45 until the L3 construct publishes a new alpha; a follow-up bumps it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(capacity-provider): address review — TagsSchema, ARN partitions, deployable-resource preflight, volume parsing, TUI parity, optional operator role Address PR #2030 review feedback: - tags: use shared TagsSchema (mirrors the CDK schema) instead of a plain string record - ARN patterns (operator-role, instance-profile, KMS): partition-agnostic arn:[^:]+: per AGENTS.md; drop now-unneeded eslint-disable directives - preflight: replace the hand-maintained hasX teardown chain with a typed hasDeployableResources(spec) + DEPLOYABLE_RESOURCE_KEYS, fixing latent drift where a project containing only configBundles or onlineEvalConfigs was misclassified as empty - --volume parse: require exactly name:sizeGiB (segment count + digit regex), rejecting data:20:gp3, hex (0x14), and exponent (2e1) that Number() silently accepted - TUI: remove-all confirmation now enumerates capacity providers; add screen shows "Capacity Provider [preview]" to match the remove screen - operator role: --operator-role-arn is now optional (auto-created at deploy when omitted, matching the CDK construct); supplying an ARN is a bring-your-own override. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 88a02ed commit 72cb257

44 files changed

Lines changed: 2240 additions & 42 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ These options are available on all commands:
3232

3333
- `create` - Create new AgentCore project
3434
- `add` - Add resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target, policy-engine,
35-
policy, payment-manager, payment-connector)
35+
policy, payment-manager, payment-connector, capacity-provider)
3636
- `remove` - Remove resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target,
37-
policy-engine, policy, payment-manager, payment-connector, all)
37+
policy-engine, policy, payment-manager, payment-connector, capacity-provider, all)
3838
- `deploy` - Deploy infrastructure to AWS
3939
- `status` - Check deployment status
4040
- `dev` - Local development server (CodeZip: uvicorn with hot-reload; Container: Docker build + run with volume mount)
@@ -90,6 +90,7 @@ Current primitives:
9090
- `PolicyPrimitive` — Cedar policy creation/removal within policy engines
9191
- `PaymentManagerPrimitive` — payment manager creation/removal with agent code wiring
9292
- `PaymentConnectorPrimitive` — payment connector creation/removal with credential management
93+
- `CapacityProviderPrimitive` — capacity provider creation/removal (customer-managed EC2 compute pool for runtimes)
9394

9495
Singletons are created in `registry.ts` and wired into CLI commands via `cli.ts`. See `src/cli/AGENTS.md` for details on
9596
adding new primitives.

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ agentcore invoke
9191

9292
### Resource Management
9393

94-
| Command | Description |
95-
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96-
| `add` | Add harnesses, agents, memory, credentials, gateways and gateway-targets, evaluators, online evals, online insights, knowledge bases, config bundles, datasets, policy engines and policies, payment managers and payment connectors, runtime endpoints |
97-
| `remove` | Remove any of the above resources from the project |
94+
| Command | Description |
95+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96+
| `add` | Add harnesses, agents, memory, credentials, gateways and gateway-targets, evaluators, online evals, online insights, knowledge bases, config bundles, datasets, policy engines and policies, payment managers and payment connectors, capacity providers, runtime endpoints |
97+
| `remove` | Remove any of the above resources from the project |
9898

9999
> **Note**: Run `agentcore deploy` after `add` or `remove` to update resources in AWS.
100100
@@ -264,8 +264,8 @@ my-project/
264264
Projects use JSON schema files in the `agentcore/` directory:
265265

266266
- `agentcore.json` - Project resources (agents, memory, credentials, gateways, evaluators, online evals/insights,
267-
knowledge bases, harnesses, policy engines and policies, payment managers and connectors, config bundles, datasets,
268-
runtime endpoints)
267+
knowledge bases, harnesses, policy engines and policies, payment managers and connectors, capacity providers, config
268+
bundles, datasets, runtime endpoints)
269269
- `deployed-state.json` - Runtime state in agentcore/.cli/ (auto-managed)
270270
- `aws-targets.json` - Deployment targets (account, region)
271271

docs/commands.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,57 @@ agentcore add config-bundle \
778778
| `--commit-message <text>` | Commit message for this version |
779779
| `--json` | JSON output |
780780

781+
### add capacity-provider
782+
783+
Add a capacity provider — a customer-managed pool of AWS-managed EC2 compute that agent runtimes can run on instead of
784+
the default managed fleet. Everything except the description and tags is immutable after creation.
785+
786+
**Operator role.** AgentCore assumes an IAM _operator role_ to create and manage the EC2 compute on your behalf. Omit
787+
`--operator-role-arn` and the CLI provisions one for you at deploy time — a role that trusts
788+
`bedrock-agentcore.amazonaws.com` (scoped to your account and region) and carries the AWS managed policy
789+
`BedrockAgentCoreRuntimeInstancesOperatorRolePolicy` (EC2/Auto Scaling/fleet management plus the
790+
`agentcore-lifecycle-events-*` EventBridge permissions the service needs). Pass `--operator-role-arn` only when you want
791+
to bring your own role; it must grant those same permissions, or capacity provider creation fails asynchronously
792+
(surfaced by CloudFormation as `NotStabilized`).
793+
794+
```bash
795+
# Minimal — operator role is created automatically
796+
agentcore add capacity-provider \
797+
--name MyCapacityProvider \
798+
--subnets subnet-0123456789abcdef0 \
799+
--security-groups sg-0123456789abcdef0 \
800+
--instance-types c6a.large
801+
802+
# With a named EBS volume, lifecycle limits, and ARM64 (and a bring-your-own operator role)
803+
agentcore add capacity-provider \
804+
--name MyCapacityProvider \
805+
--operator-role-arn arn:aws:iam::123456789012:role/MyOperatorRole \
806+
--subnets subnet-0123456789abcdef0,subnet-0fedcba9876543210 \
807+
--security-groups sg-0123456789abcdef0 \
808+
--os LINUX_ARM64 \
809+
--instance-types c7g.large,c7g.xlarge \
810+
--volume data:20 --volume-encrypted \
811+
--idle-instance-timeout 3600 \
812+
--max-lifetime 28800
813+
```
814+
815+
| Flag | Description |
816+
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
817+
| `--name <name>` | Capacity provider name (required); immutable after creation |
818+
| `--operator-role-arn <arn>` | IAM role ARN AgentCore assumes to manage the capacity provider (optional — auto-created if omitted); immutable |
819+
| `--description <desc>` | Description (the only mutable field besides tags) |
820+
| `--subnets <subnets>` | Comma-separated subnet IDs, 1–16 (required) |
821+
| `--security-groups <groups>` | Comma-separated security group IDs, 1–16 (required) |
822+
| `--os <os>` | `LINUX_X86_64` (default) or `LINUX_ARM64` |
823+
| `--instance-types <types>` | Comma-separated allowed EC2 instance types, 1–30 (required) |
824+
| `--volume <name:sizeGiB>` | Named EBS volume as `name:sizeGiB` (repeatable, max 5) |
825+
| `--volume-encrypted` | Encrypt EBS volumes |
826+
| `--volume-kms-key <arn>` | KMS key ARN for EBS volume encryption |
827+
| `--instance-profile-arn <arn>` | IAM instance profile ARN for launched instances |
828+
| `--idle-instance-timeout <secs>` | Idle instance timeout in seconds (60–1209600) |
829+
| `--max-lifetime <secs>` | Maximum instance lifetime in seconds (60–1209600) |
830+
| `--json` | JSON output |
831+
781832
### remove
782833

783834
Remove resources from project.
@@ -797,6 +848,7 @@ agentcore remove dataset --name MyDataset
797848
agentcore remove config-bundle --name MyBundle
798849
agentcore remove payment-manager --name MyManager -y
799850
agentcore remove payment-connector --name MyCDPConnector --manager MyManager -y
851+
agentcore remove capacity-provider --name MyCapacityProvider -y
800852

801853
# Reset everything
802854
agentcore remove all -y
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js';
2+
import type { TestProject } from '../src/test-utils/index.js';
3+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
4+
5+
const OPERATOR_ROLE_ARN = 'arn:aws:iam::123456789012:role/MyOperatorRole';
6+
7+
describe('integration: add and remove capacity providers', () => {
8+
let project: TestProject;
9+
10+
beforeAll(async () => {
11+
project = await createTestProject({ noAgent: true });
12+
});
13+
14+
afterAll(async () => {
15+
await project.cleanup();
16+
});
17+
18+
describe('capacity provider lifecycle', () => {
19+
const cpName = `IntegCp${Date.now().toString().slice(-6)}`;
20+
21+
it('adds a capacity provider', async () => {
22+
const result = await runCLI(
23+
[
24+
'add',
25+
'capacity-provider',
26+
'--name',
27+
cpName,
28+
'--operator-role-arn',
29+
OPERATOR_ROLE_ARN,
30+
'--subnets',
31+
'subnet-0123456789abcdef0',
32+
'--security-groups',
33+
'sg-0123456789abcdef0',
34+
'--os',
35+
'LINUX_X86_64',
36+
'--instance-types',
37+
'c6a.large',
38+
'--json',
39+
],
40+
project.projectPath
41+
);
42+
43+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
44+
const json = JSON.parse(result.stdout);
45+
expect(json.success).toBe(true);
46+
expect(json.capacityProviderName).toBe(cpName);
47+
48+
const config = await readProjectConfig(project.projectPath);
49+
const cp = config.capacityProviders?.find((c: Record<string, unknown>) => c.name === cpName);
50+
expect(cp, `Capacity provider "${cpName}" should be in config`).toBeTruthy();
51+
expect(cp!.operatorRoleArn).toBe(OPERATOR_ROLE_ARN);
52+
const ec2 = (cp as any).computeConfiguration.ec2Configuration;
53+
expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_X86_64');
54+
expect(ec2.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes).toEqual([
55+
'c6a.large',
56+
]);
57+
expect(ec2.vpcConfiguration.subnets).toEqual(['subnet-0123456789abcdef0']);
58+
expect(ec2.vpcConfiguration.securityGroups).toEqual(['sg-0123456789abcdef0']);
59+
});
60+
61+
it('adds a capacity provider with volumes, lifecycle, and description', async () => {
62+
const richName = `${cpName}Rich`;
63+
const result = await runCLI(
64+
[
65+
'add',
66+
'capacity-provider',
67+
'--name',
68+
richName,
69+
'--operator-role-arn',
70+
OPERATOR_ROLE_ARN,
71+
'--description',
72+
'my rich capacity provider',
73+
'--subnets',
74+
'subnet-0123456789abcdef0,subnet-0fedcba9876543210',
75+
'--security-groups',
76+
'sg-0123456789abcdef0',
77+
'--os',
78+
'LINUX_ARM64',
79+
'--instance-types',
80+
'c7g.large,c7g.xlarge',
81+
'--volume',
82+
'data:20',
83+
'--volume-encrypted',
84+
'--idle-instance-timeout',
85+
'3600',
86+
'--max-lifetime',
87+
'28800',
88+
'--json',
89+
],
90+
project.projectPath
91+
);
92+
93+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
94+
expect(JSON.parse(result.stdout).success).toBe(true);
95+
96+
const config = await readProjectConfig(project.projectPath);
97+
const cp = config.capacityProviders?.find((c: Record<string, unknown>) => c.name === richName);
98+
expect(cp).toBeTruthy();
99+
expect(cp!.description).toBe('my rich capacity provider');
100+
const ec2 = (cp as any).computeConfiguration.ec2Configuration;
101+
expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_ARM64');
102+
expect(ec2.vpcConfiguration.subnets).toHaveLength(2);
103+
expect(ec2.volumes).toEqual([{ ebsConfiguration: { name: 'data', sizeGiB: 20, encrypted: true } }]);
104+
expect(ec2.lifecycleConfiguration).toEqual({ idleInstanceTimeout: 3600, maxLifetime: 28800 });
105+
106+
await runCLI(['remove', 'capacity-provider', '--name', richName, '--yes'], project.projectPath);
107+
});
108+
109+
it('rejects a duplicate capacity provider name', async () => {
110+
const result = await runCLI(
111+
[
112+
'add',
113+
'capacity-provider',
114+
'--name',
115+
cpName,
116+
'--operator-role-arn',
117+
OPERATOR_ROLE_ARN,
118+
'--subnets',
119+
'subnet-0123456789abcdef0',
120+
'--security-groups',
121+
'sg-0123456789abcdef0',
122+
'--instance-types',
123+
'c6a.large',
124+
'--json',
125+
],
126+
project.projectPath
127+
);
128+
129+
expect(result.exitCode).toBe(1);
130+
const json = JSON.parse(result.stdout);
131+
expect(json.success).toBe(false);
132+
expect(json.error).toContain('already exists');
133+
});
134+
135+
it('removes the capacity provider', async () => {
136+
const result = await runCLI(
137+
['remove', 'capacity-provider', '--name', cpName, '--yes', '--json'],
138+
project.projectPath
139+
);
140+
141+
expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0);
142+
const json = JSON.parse(result.stdout);
143+
expect(json.success).toBe(true);
144+
145+
const config = await readProjectConfig(project.projectPath);
146+
const found = config.capacityProviders?.some((c: Record<string, unknown>) => c.name === cpName);
147+
expect(found, `Capacity provider "${cpName}" should be removed`).toBeFalsy();
148+
});
149+
});
150+
151+
describe('validation', () => {
152+
it('rejects a missing required option', async () => {
153+
const result = await runCLI(['add', 'capacity-provider', '--name', 'noRole', '--json'], project.projectPath);
154+
expect(result.exitCode).toBe(1);
155+
const json = JSON.parse(result.stdout);
156+
expect(json.success).toBe(false);
157+
});
158+
159+
it('rejects an unsupported operating system', async () => {
160+
const result = await runCLI(
161+
[
162+
'add',
163+
'capacity-provider',
164+
'--name',
165+
'badOs',
166+
'--operator-role-arn',
167+
OPERATOR_ROLE_ARN,
168+
'--subnets',
169+
'subnet-0123456789abcdef0',
170+
'--security-groups',
171+
'sg-0123456789abcdef0',
172+
'--os',
173+
'WINDOWS_X86_64',
174+
'--instance-types',
175+
'c6a.large',
176+
'--json',
177+
],
178+
project.projectPath
179+
);
180+
expect(result.exitCode).toBe(1);
181+
});
182+
183+
it('rejects a malformed operator role ARN', async () => {
184+
const result = await runCLI(
185+
[
186+
'add',
187+
'capacity-provider',
188+
'--name',
189+
'badArn',
190+
'--operator-role-arn',
191+
'not-an-arn',
192+
'--subnets',
193+
'subnet-0123456789abcdef0',
194+
'--security-groups',
195+
'sg-0123456789abcdef0',
196+
'--instance-types',
197+
'c6a.large',
198+
'--json',
199+
],
200+
project.projectPath
201+
);
202+
expect(result.exitCode).toBe(1);
203+
});
204+
205+
it('passes agentcore validate after add/remove lifecycle', async () => {
206+
const result = await runCLI(['validate'], project.projectPath);
207+
expect(result.exitCode).toBe(0);
208+
});
209+
});
210+
});

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7511,7 +7511,7 @@ file maps to a JSON config file and includes validation constraints as comments
75117511
75127512
### Key Types
75137513
7514-
- **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, and payment managers
7514+
- **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, payment managers, and capacity providers
75157515
- **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode)
75167516
- **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry
75177517
- **Credential**: API key or OAuth credential provider
@@ -7622,7 +7622,7 @@ Run \`agentcore --help\` or \`agentcore <command> --help\` for full flags. Commo
76227622
76237623
| Command | Description |
76247624
| --- | --- |
7625-
| \`agentcore add <resource>\` | Add agent, memory, credential, gateway, gateway-target, evaluator, online-eval, online-insights, knowledge-base, harness, policy-engine, policy, payment-manager, payment-connector, config-bundle, dataset, runtime-endpoint |
7625+
| \`agentcore add <resource>\` | Add agent, memory, credential, gateway, gateway-target, evaluator, online-eval, online-insights, knowledge-base, harness, policy-engine, policy, payment-manager, payment-connector, capacity-provider, config-bundle, dataset, runtime-endpoint |
76267626
| \`agentcore remove <resource>\` | Remove any resource |
76277627
| \`agentcore export harness\` | Export a harness to a Strands runtime agent under \`app/<agentName>/\` |
76287628

src/assets/agents/AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ file maps to a JSON config file and includes validation constraints as comments
5656

5757
### Key Types
5858

59-
- **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, and payment managers
59+
- **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, payment managers, and capacity providers
6060
- **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode)
6161
- **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry
6262
- **Credential**: API key or OAuth credential provider
@@ -167,7 +167,7 @@ Run `agentcore --help` or `agentcore <command> --help` for full flags. Commonly
167167

168168
| Command | Description |
169169
| --- | --- |
170-
| `agentcore add <resource>` | Add agent, memory, credential, gateway, gateway-target, evaluator, online-eval, online-insights, knowledge-base, harness, policy-engine, policy, payment-manager, payment-connector, config-bundle, dataset, runtime-endpoint |
170+
| `agentcore add <resource>` | Add agent, memory, credential, gateway, gateway-target, evaluator, online-eval, online-insights, knowledge-base, harness, policy-engine, policy, payment-manager, payment-connector, capacity-provider, config-bundle, dataset, runtime-endpoint |
171171
| `agentcore remove <resource>` | Remove any resource |
172172
| `agentcore export harness` | Export a harness to a Strands runtime agent under `app/<agentName>/` |
173173

0 commit comments

Comments
 (0)