From e036b837e25f97455c1e830c0020162c5f75dfc1 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 19 Aug 2026 19:54:01 +0000 Subject: [PATCH] revert: remove capacity-provider Journey 1 --- AGENTS.md | 5 +- README.md | 12 +- docs/commands.md | 52 --- .../add-remove-capacity-provider.test.ts | 210 ------------- .../assets.snapshot.test.ts.snap | 4 +- src/assets/agents/AGENTS.md | 4 +- .../outputs-capacity-provider.test.ts | 51 --- src/cli/cloudformation/outputs.ts | 39 --- src/cli/commands/deploy/actions.ts | 6 - src/cli/commands/remove/command.tsx | 2 - src/cli/commands/remove/types.ts | 3 +- .../commands/status/__tests__/action.test.ts | 54 ---- src/cli/commands/status/action.ts | 13 +- src/cli/commands/status/command.tsx | 1 - src/cli/logging/remove-logger.ts | 3 +- .../agent/generate/write-agent-to-project.ts | 1 - .../deploy/__tests__/preflight.test.ts | 70 ----- src/cli/operations/deploy/preflight.ts | 66 ++-- .../primitives/CapacityProviderPrimitive.ts | 274 ---------------- .../CapacityProviderPrimitive.test.ts | 296 ------------------ src/cli/primitives/index.ts | 3 - src/cli/primitives/registry.ts | 3 - src/cli/project.ts | 1 - src/cli/telemetry/schemas/command-run.ts | 12 - src/cli/telemetry/schemas/common-shapes.ts | 2 - src/cli/tui/components/ResourceGraph.tsx | 25 -- src/cli/tui/hooks/useRemove.ts | 20 -- src/cli/tui/screens/add/AddFlow.tsx | 20 -- src/cli/tui/screens/add/AddScreen.tsx | 8 +- .../screens/add/__tests__/AddScreen.test.tsx | 6 - .../AddCapacityProviderFlow.tsx | 106 ------- .../AddCapacityProviderScreen.tsx | 262 ---------------- .../tui/screens/capacity-provider/index.ts | 3 - .../remove/RemoveCapacityProviderScreen.tsx | 30 -- src/cli/tui/screens/remove/RemoveFlow.tsx | 114 +------ src/cli/tui/screens/remove/RemoveScreen.tsx | 12 - .../remove/__tests__/RemoveScreen.test.tsx | 59 ---- src/cli/tui/screens/remove/useRemoveFlow.ts | 5 - src/schema/llm-compacted/agentcore.ts | 52 --- src/schema/schemas/agentcore-project.ts | 21 -- src/schema/schemas/deployed-state.ts | 12 - .../__tests__/capacity-provider.test.ts | 98 ------ .../schemas/primitives/capacity-provider.ts | 213 ------------- src/schema/schemas/primitives/index.ts | 29 -- 44 files changed, 42 insertions(+), 2240 deletions(-) delete mode 100644 integ-tests/add-remove-capacity-provider.test.ts delete mode 100644 src/cli/cloudformation/__tests__/outputs-capacity-provider.test.ts delete mode 100644 src/cli/primitives/CapacityProviderPrimitive.ts delete mode 100644 src/cli/primitives/__tests__/CapacityProviderPrimitive.test.ts delete mode 100644 src/cli/tui/screens/capacity-provider/AddCapacityProviderFlow.tsx delete mode 100644 src/cli/tui/screens/capacity-provider/AddCapacityProviderScreen.tsx delete mode 100644 src/cli/tui/screens/capacity-provider/index.ts delete mode 100644 src/cli/tui/screens/remove/RemoveCapacityProviderScreen.tsx delete mode 100644 src/schema/schemas/primitives/__tests__/capacity-provider.test.ts delete mode 100644 src/schema/schemas/primitives/capacity-provider.ts diff --git a/AGENTS.md b/AGENTS.md index 6f8e92036..4d4627610 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,9 +32,9 @@ These options are available on all commands: - `create` - Create new AgentCore project - `add` - Add resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target, policy-engine, - policy, payment-manager, payment-connector, capacity-provider) + policy, payment-manager, payment-connector) - `remove` - Remove resources (agent, memory, credential, evaluator, online-eval, gateway, gateway-target, - policy-engine, policy, payment-manager, payment-connector, capacity-provider, all) + policy-engine, policy, payment-manager, payment-connector, all) - `deploy` - Deploy infrastructure to AWS - `status` - Check deployment status - `dev` - Local development server (CodeZip: uvicorn with hot-reload; Container: Docker build + run with volume mount) @@ -90,7 +90,6 @@ Current primitives: - `PolicyPrimitive` — Cedar policy creation/removal within policy engines - `PaymentManagerPrimitive` — payment manager creation/removal with agent code wiring - `PaymentConnectorPrimitive` — payment connector creation/removal with credential management -- `CapacityProviderPrimitive` — capacity provider creation/removal (customer-managed EC2 compute pool for runtimes) Singletons are created in `registry.ts` and wired into CLI commands via `cli.ts`. See `src/cli/AGENTS.md` for details on adding new primitives. diff --git a/README.md b/README.md index a6184931b..b257fd1cf 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,10 @@ agentcore invoke ### Resource Management -| Command | Description | -| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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 | -| `remove` | Remove any of the above resources from the project | +| Command | Description | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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 | +| `remove` | Remove any of the above resources from the project | > **Note**: Run `agentcore deploy` after `add` or `remove` to update resources in AWS. @@ -264,8 +264,8 @@ my-project/ Projects use JSON schema files in the `agentcore/` directory: - `agentcore.json` - Project resources (agents, memory, credentials, gateways, evaluators, online evals/insights, - knowledge bases, harnesses, policy engines and policies, payment managers and connectors, capacity providers, config - bundles, datasets, runtime endpoints) + knowledge bases, harnesses, policy engines and policies, payment managers and connectors, config bundles, datasets, + runtime endpoints) - `deployed-state.json` - Runtime state in agentcore/.cli/ (auto-managed) - `aws-targets.json` - Deployment targets (account, region) diff --git a/docs/commands.md b/docs/commands.md index f1ad57055..0b12e95e3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -778,57 +778,6 @@ agentcore add config-bundle \ | `--commit-message ` | Commit message for this version | | `--json` | JSON output | -### add capacity-provider - -Add a capacity provider — a customer-managed pool of AWS-managed EC2 compute that agent runtimes can run on instead of -the default managed fleet. Everything except the description and tags is immutable after creation. - -**Operator role.** AgentCore assumes an IAM _operator role_ to create and manage the EC2 compute on your behalf. Omit -`--operator-role-arn` and the CLI provisions one for you at deploy time — a role that trusts -`bedrock-agentcore.amazonaws.com` (scoped to your account and region) and carries the AWS managed policy -`BedrockAgentCoreRuntimeInstancesOperatorRolePolicy` (EC2/Auto Scaling/fleet management plus the -`agentcore-lifecycle-events-*` EventBridge permissions the service needs). Pass `--operator-role-arn` only when you want -to bring your own role; it must grant those same permissions, or capacity provider creation fails asynchronously -(surfaced by CloudFormation as `NotStabilized`). - -```bash -# Minimal — operator role is created automatically -agentcore add capacity-provider \ - --name MyCapacityProvider \ - --subnets subnet-0123456789abcdef0 \ - --security-groups sg-0123456789abcdef0 \ - --instance-types c6a.large - -# With a named EBS volume, lifecycle limits, and ARM64 (and a bring-your-own operator role) -agentcore add capacity-provider \ - --name MyCapacityProvider \ - --operator-role-arn arn:aws:iam::123456789012:role/MyOperatorRole \ - --subnets subnet-0123456789abcdef0,subnet-0fedcba9876543210 \ - --security-groups sg-0123456789abcdef0 \ - --os LINUX_ARM64 \ - --instance-types c7g.large,c7g.xlarge \ - --volume data:20 --volume-encrypted \ - --idle-instance-timeout 3600 \ - --max-lifetime 28800 -``` - -| Flag | Description | -| -------------------------------- | -------------------------------------------------------------------------------------------------------------- | -| `--name ` | Capacity provider name (required); immutable after creation | -| `--operator-role-arn ` | IAM role ARN AgentCore assumes to manage the capacity provider (optional — auto-created if omitted); immutable | -| `--description ` | Description (the only mutable field besides tags) | -| `--subnets ` | Comma-separated subnet IDs, 1–16 (required) | -| `--security-groups ` | Comma-separated security group IDs, 1–16 (required) | -| `--os ` | `LINUX_X86_64` (default) or `LINUX_ARM64` | -| `--instance-types ` | Comma-separated allowed EC2 instance types, 1–30 (required) | -| `--volume ` | Named EBS volume as `name:sizeGiB` (repeatable, max 5) | -| `--volume-encrypted` | Encrypt EBS volumes | -| `--volume-kms-key ` | KMS key ARN for EBS volume encryption | -| `--instance-profile-arn ` | IAM instance profile ARN for launched instances | -| `--idle-instance-timeout ` | Idle instance timeout in seconds (60–1209600) | -| `--max-lifetime ` | Maximum instance lifetime in seconds (60–1209600) | -| `--json` | JSON output | - ### remove Remove resources from project. @@ -848,7 +797,6 @@ agentcore remove dataset --name MyDataset agentcore remove config-bundle --name MyBundle agentcore remove payment-manager --name MyManager -y agentcore remove payment-connector --name MyCDPConnector --manager MyManager -y -agentcore remove capacity-provider --name MyCapacityProvider -y # Reset everything agentcore remove all -y diff --git a/integ-tests/add-remove-capacity-provider.test.ts b/integ-tests/add-remove-capacity-provider.test.ts deleted file mode 100644 index df07a429f..000000000 --- a/integ-tests/add-remove-capacity-provider.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; -import type { TestProject } from '../src/test-utils/index.js'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -const OPERATOR_ROLE_ARN = 'arn:aws:iam::123456789012:role/MyOperatorRole'; - -describe('integration: add and remove capacity providers', () => { - let project: TestProject; - - beforeAll(async () => { - project = await createTestProject({ noAgent: true }); - }); - - afterAll(async () => { - await project.cleanup(); - }); - - describe('capacity provider lifecycle', () => { - const cpName = `IntegCp${Date.now().toString().slice(-6)}`; - - it('adds a capacity provider', async () => { - const result = await runCLI( - [ - 'add', - 'capacity-provider', - '--name', - cpName, - '--operator-role-arn', - OPERATOR_ROLE_ARN, - '--subnets', - 'subnet-0123456789abcdef0', - '--security-groups', - 'sg-0123456789abcdef0', - '--os', - 'LINUX_X86_64', - '--instance-types', - 'c6a.large', - '--json', - ], - project.projectPath - ); - - expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(true); - expect(json.capacityProviderName).toBe(cpName); - - const config = await readProjectConfig(project.projectPath); - const cp = config.capacityProviders?.find((c: Record) => c.name === cpName); - expect(cp, `Capacity provider "${cpName}" should be in config`).toBeTruthy(); - expect(cp!.operatorRoleArn).toBe(OPERATOR_ROLE_ARN); - const ec2 = (cp as any).computeConfiguration.ec2Configuration; - expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_X86_64'); - expect(ec2.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes).toEqual([ - 'c6a.large', - ]); - expect(ec2.vpcConfiguration.subnets).toEqual(['subnet-0123456789abcdef0']); - expect(ec2.vpcConfiguration.securityGroups).toEqual(['sg-0123456789abcdef0']); - }); - - it('adds a capacity provider with volumes, lifecycle, and description', async () => { - const richName = `${cpName}Rich`; - const result = await runCLI( - [ - 'add', - 'capacity-provider', - '--name', - richName, - '--operator-role-arn', - OPERATOR_ROLE_ARN, - '--description', - 'my rich capacity provider', - '--subnets', - 'subnet-0123456789abcdef0,subnet-0fedcba9876543210', - '--security-groups', - 'sg-0123456789abcdef0', - '--os', - 'LINUX_ARM64', - '--instance-types', - 'c7g.large,c7g.xlarge', - '--volume', - 'data:20', - '--volume-encrypted', - '--idle-instance-timeout', - '3600', - '--max-lifetime', - '28800', - '--json', - ], - project.projectPath - ); - - expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); - expect(JSON.parse(result.stdout).success).toBe(true); - - const config = await readProjectConfig(project.projectPath); - const cp = config.capacityProviders?.find((c: Record) => c.name === richName); - expect(cp).toBeTruthy(); - expect(cp!.description).toBe('my rich capacity provider'); - const ec2 = (cp as any).computeConfiguration.ec2Configuration; - expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_ARM64'); - expect(ec2.vpcConfiguration.subnets).toHaveLength(2); - expect(ec2.volumes).toEqual([{ ebsConfiguration: { name: 'data', sizeGiB: 20, encrypted: true } }]); - expect(ec2.lifecycleConfiguration).toEqual({ idleInstanceTimeout: 3600, maxLifetime: 28800 }); - - await runCLI(['remove', 'capacity-provider', '--name', richName, '--yes'], project.projectPath); - }); - - it('rejects a duplicate capacity provider name', async () => { - const result = await runCLI( - [ - 'add', - 'capacity-provider', - '--name', - cpName, - '--operator-role-arn', - OPERATOR_ROLE_ARN, - '--subnets', - 'subnet-0123456789abcdef0', - '--security-groups', - 'sg-0123456789abcdef0', - '--instance-types', - 'c6a.large', - '--json', - ], - project.projectPath - ); - - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - expect(json.error).toContain('already exists'); - }); - - it('removes the capacity provider', async () => { - const result = await runCLI( - ['remove', 'capacity-provider', '--name', cpName, '--yes', '--json'], - project.projectPath - ); - - expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(true); - - const config = await readProjectConfig(project.projectPath); - const found = config.capacityProviders?.some((c: Record) => c.name === cpName); - expect(found, `Capacity provider "${cpName}" should be removed`).toBeFalsy(); - }); - }); - - describe('validation', () => { - it('rejects a missing required option', async () => { - const result = await runCLI(['add', 'capacity-provider', '--name', 'noRole', '--json'], project.projectPath); - expect(result.exitCode).toBe(1); - const json = JSON.parse(result.stdout); - expect(json.success).toBe(false); - }); - - it('rejects an unsupported operating system', async () => { - const result = await runCLI( - [ - 'add', - 'capacity-provider', - '--name', - 'badOs', - '--operator-role-arn', - OPERATOR_ROLE_ARN, - '--subnets', - 'subnet-0123456789abcdef0', - '--security-groups', - 'sg-0123456789abcdef0', - '--os', - 'WINDOWS_X86_64', - '--instance-types', - 'c6a.large', - '--json', - ], - project.projectPath - ); - expect(result.exitCode).toBe(1); - }); - - it('rejects a malformed operator role ARN', async () => { - const result = await runCLI( - [ - 'add', - 'capacity-provider', - '--name', - 'badArn', - '--operator-role-arn', - 'not-an-arn', - '--subnets', - 'subnet-0123456789abcdef0', - '--security-groups', - 'sg-0123456789abcdef0', - '--instance-types', - 'c6a.large', - '--json', - ], - project.projectPath - ); - expect(result.exitCode).toBe(1); - }); - - it('passes agentcore validate after add/remove lifecycle', async () => { - const result = await runCLI(['validate'], project.projectPath); - expect(result.exitCode).toBe(0); - }); - }); -}); diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 26bfcab52..857f0dbb3 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -7511,7 +7511,7 @@ file maps to a JSON config file and includes validation constraints as comments ### Key Types -- **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 +- **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 - **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode) - **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry - **Credential**: API key or OAuth credential provider @@ -7622,7 +7622,7 @@ Run \`agentcore --help\` or \`agentcore --help\` for full flags. Commo | Command | Description | | --- | --- | -| \`agentcore add \` | 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 | +| \`agentcore add \` | 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 | | \`agentcore remove \` | Remove any resource | | \`agentcore export harness\` | Export a harness to a Strands runtime agent under \`app//\` | diff --git a/src/assets/agents/AGENTS.md b/src/assets/agents/AGENTS.md index 8a020757e..f81046d71 100644 --- a/src/assets/agents/AGENTS.md +++ b/src/assets/agents/AGENTS.md @@ -56,7 +56,7 @@ file maps to a JSON config file and includes validation constraints as comments ### Key Types -- **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 +- **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 - **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode) - **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry - **Credential**: API key or OAuth credential provider @@ -167,7 +167,7 @@ Run `agentcore --help` or `agentcore --help` for full flags. Commonly | Command | Description | | --- | --- | -| `agentcore add ` | 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 | +| `agentcore add ` | 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 | | `agentcore remove ` | Remove any resource | | `agentcore export harness` | Export a harness to a Strands runtime agent under `app//` | diff --git a/src/cli/cloudformation/__tests__/outputs-capacity-provider.test.ts b/src/cli/cloudformation/__tests__/outputs-capacity-provider.test.ts deleted file mode 100644 index c4b932015..000000000 --- a/src/cli/cloudformation/__tests__/outputs-capacity-provider.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { parseCapacityProviderOutputs } from '../outputs'; -import { describe, expect, it } from 'vitest'; - -describe('parseCapacityProviderOutputs', () => { - it('parses Id and Arn from stack outputs', () => { - const outputs = { - ApplicationCapacityProviderMyCpIdOutputABC123: 'MyCp-abc1234567', - ApplicationCapacityProviderMyCpArnOutputDEF456: - 'arn:aws:bedrock-agentcore:us-west-2:123456789012:capacity-provider/MyCp-abc1234567', - }; - - const result = parseCapacityProviderOutputs(outputs, ['MyCp']); - - expect(result).toEqual({ - MyCp: { - capacityProviderId: 'MyCp-abc1234567', - capacityProviderArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:capacity-provider/MyCp-abc1234567', - }, - }); - }); - - it('parses multiple capacity providers', () => { - const outputs = { - ApplicationCapacityProviderFirstIdOutputAAA: 'first-id', - ApplicationCapacityProviderFirstArnOutputBBB: 'arn:first', - ApplicationCapacityProviderSecondIdOutputCCC: 'second-id', - ApplicationCapacityProviderSecondArnOutputDDD: 'arn:second', - }; - - const result = parseCapacityProviderOutputs(outputs, ['First', 'Second']); - - expect(Object.keys(result)).toHaveLength(2); - expect(result.First!.capacityProviderId).toBe('first-id'); - expect(result.Second!.capacityProviderArn).toBe('arn:second'); - }); - - it('skips a capacity provider when the Id output is missing', () => { - const outputs = { - ApplicationCapacityProviderMyCpArnOutputDEF: 'arn:test', - }; - - const result = parseCapacityProviderOutputs(outputs, ['MyCp']); - - expect(result).toEqual({}); - }); - - it('returns empty for no names', () => { - const result = parseCapacityProviderOutputs({ ApplicationCapacityProviderMyCpIdOutputX: 'x' }, []); - expect(result).toEqual({}); - }); -}); diff --git a/src/cli/cloudformation/outputs.ts b/src/cli/cloudformation/outputs.ts index 6db7ec064..d8463a023 100644 --- a/src/cli/cloudformation/outputs.ts +++ b/src/cli/cloudformation/outputs.ts @@ -1,6 +1,5 @@ import type { AgentCoreDeployedState, - CapacityProviderDeployedState, ConfigBundleDeployedState, DatasetDeployedState, DeployedState, @@ -538,37 +537,6 @@ export function parseDatasetOutputs( return datasets; } -/** - * Parse stack outputs into deployed state for capacity providers. - * - * Output key pattern: ApplicationCapacityProvider{PascalName}(Id|Arn)Output{Hash} - */ -export function parseCapacityProviderOutputs( - outputs: StackOutputs, - capacityProviderNames: string[] -): Record { - const capacityProviders: Record = {}; - const outputKeys = Object.keys(outputs); - - for (const capacityProviderName of capacityProviderNames) { - const pascal = toPascalId('CapacityProvider', capacityProviderName); - const idPrefix = `Application${pascal}IdOutput`; - const arnPrefix = `Application${pascal}ArnOutput`; - - const idKey = outputKeys.find(k => k.startsWith(idPrefix)); - const arnKey = outputKeys.find(k => k.startsWith(arnPrefix)); - - if (idKey && arnKey) { - capacityProviders[capacityProviderName] = { - capacityProviderId: outputs[idKey]!, - capacityProviderArn: outputs[arnKey]!, - }; - } - } - - return capacityProviders; -} - /** * Parse CDK stack outputs for CFN-deployed harnesses into deployed-state records. * @@ -756,7 +724,6 @@ export interface BuildDeployedStateOptions { configBundles?: Record; knowledgeBases?: Record; payments?: Record; - capacityProviders?: Record; /** * Names of A/B tests currently declared in the project spec. AB test state is managed * post-deploy (not via CFN outputs) and carried forward across deploys; passing the @@ -791,7 +758,6 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta configBundles, knowledgeBases, payments, - capacityProviders, abTestNames, } = opts; const targetState: TargetDeployedState = { @@ -894,11 +860,6 @@ export function buildDeployedState(opts: BuildDeployedStateOptions): DeployedSta targetState.resources!.payments = payments; } - // Add capacity provider state from CFN outputs - if (capacityProviders && Object.keys(capacityProviders).length > 0) { - targetState.resources!.capacityProviders = capacityProviders; - } - return { targets: { ...existingState?.targets, diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index 484beaf1d..437f26898 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -17,7 +17,6 @@ import { buildDeployedState, getStackOutputs, parseAgentOutputs, - parseCapacityProviderOutputs, parseConfigBundleOutputs, parseDatasetOutputs, parseEvaluatorOutputs, @@ -708,10 +707,6 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise d.name); const datasets = parseDatasetOutputs(outputs, datasetNames); - // Parse capacity provider outputs - const capacityProviderNames = (context.projectSpec.capacityProviders ?? []).map(cp => cp.name); - const capacityProviders = parseCapacityProviderOutputs(outputs, capacityProviderNames); - // Parse config bundle outputs const configBundleNames = (context.projectSpec.configBundles ?? []).map(b => b.name); const configBundles = parseConfigBundleOutputs(outputs, configBundleNames); @@ -795,7 +790,6 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise t.name), }); diff --git a/src/cli/commands/remove/command.tsx b/src/cli/commands/remove/command.tsx index 803f45bc3..b5e298047 100644 --- a/src/cli/commands/remove/command.tsx +++ b/src/cli/commands/remove/command.tsx @@ -27,7 +27,6 @@ async function handleRemoveAll(options: RemoveAllOptions): Promise for (const e of current.evaluators ?? []) items.push(`evaluator: ${e.name}`); for (const g of current.agentCoreGateways ?? []) items.push(`gateway: ${g.name}`); for (const pe of current.policyEngines ?? []) items.push(`policy-engine: ${pe.name}`); - for (const cp of current.capacityProviders ?? []) items.push(`capacity-provider: ${cp.name}`); return { success: true, message: items.length > 0 ? `Would remove: ${items.join(', ')}` : 'Nothing to remove', @@ -85,7 +84,6 @@ async function handleRemoveAll(options: RemoveAllOptions): Promise harnesses: [], datasets: [], payments: [], - capacityProviders: [], }); // Preserve aws-targets.json and deployed-state.json so that diff --git a/src/cli/commands/remove/types.ts b/src/cli/commands/remove/types.ts index 0ec4140d4..2e3ad6346 100644 --- a/src/cli/commands/remove/types.ts +++ b/src/cli/commands/remove/types.ts @@ -17,8 +17,7 @@ export type ResourceType = | 'dataset' | 'knowledge-base' | 'payment-manager' - | 'payment-connector' - | 'capacity-provider'; + | 'payment-connector'; export interface RemoveOptions { resourceType: ResourceType; diff --git a/src/cli/commands/status/__tests__/action.test.ts b/src/cli/commands/status/__tests__/action.test.ts index 32979cc78..faceb2812 100644 --- a/src/cli/commands/status/__tests__/action.test.ts +++ b/src/cli/commands/status/__tests__/action.test.ts @@ -1338,58 +1338,4 @@ describe('handleProjectStatus — invocation URL enrichment', () => { expect(agentEntry!.deploymentState).toBe('pending-removal'); expect(agentEntry!.invocationUrl).toBeUndefined(); }); - - it('marks capacity provider deployed / local-only / pending-removal correctly', () => { - const project = { - ...baseProject, - capacityProviders: [ - { - name: 'my-cp', - operatorRoleArn: 'arn:aws:iam::123456789012:role/Op', - computeConfiguration: { - ec2Configuration: { - launchTemplateSource: { - launchParameters: { - operatingSystem: 'LINUX_X86_64', - instanceRequirements: { allowedInstanceTypes: ['c6a.large'] }, - }, - }, - vpcConfiguration: { subnets: ['subnet-0123456789abcdef0'], securityGroups: ['sg-0123456789abcdef0'] }, - }, - }, - }, - ], - } as unknown as AgentCoreProjectSpec; - - // Deployed - const deployed = computeResourceStatuses(project, { - capacityProviders: { - 'my-cp': { - capacityProviderId: 'my-cp-abc1234567', - capacityProviderArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:capacity-provider/my-cp-abc1234567', - }, - }, - }); - const deployedEntry = deployed.find(r => r.resourceType === 'capacity-provider' && r.name === 'my-cp'); - expect(deployedEntry).toBeDefined(); - expect(deployedEntry!.deploymentState).toBe('deployed'); - expect(deployedEntry!.identifier).toContain('capacity-provider/my-cp-abc1234567'); - - // Local-only - const local = computeResourceStatuses(project, undefined); - const localEntry = local.find(r => r.resourceType === 'capacity-provider' && r.name === 'my-cp'); - expect(localEntry!.deploymentState).toBe('local-only'); - - // Pending removal: deployed but not in local spec - const pending = computeResourceStatuses(baseProject, { - capacityProviders: { - 'gone-cp': { - capacityProviderId: 'gone-cp-abc1234567', - capacityProviderArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:capacity-provider/gone-cp-abc1234567', - }, - }, - }); - const pendingEntry = pending.find(r => r.resourceType === 'capacity-provider' && r.name === 'gone-cp'); - expect(pendingEntry!.deploymentState).toBe('pending-removal'); - }); }); diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index 91624aeaf..12bccd24d 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -33,8 +33,7 @@ export interface ResourceStatusEntry { | 'harness' | 'runtime-endpoint' | 'knowledge-base' - | 'payment' - | 'capacity-provider'; + | 'payment'; name: string; deploymentState: ResourceDeploymentState; identifier?: string; @@ -353,15 +352,6 @@ export function computeResourceStatuses( `${item.authorizerType} — auto-pay ${item.autoPayment ? 'on' : 'off'} (${item.connectors.length} connector(s))`, }); - const capacityProviders = diffResourceSet({ - resourceType: 'capacity-provider', - localItems: project.capacityProviders ?? [], - deployedRecord: resources?.capacityProviders ?? {}, - getIdentifier: deployed => deployed.capacityProviderArn, - getLocalDetail: item => - item.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters.operatingSystem, - }); - return [ ...agents, ...runtimeEndpoints, @@ -377,7 +367,6 @@ export function computeResourceStatuses( ...configBundles, ...harnesses, ...payments, - ...capacityProviders, ]; } diff --git a/src/cli/commands/status/command.tsx b/src/cli/commands/status/command.tsx index cadc18b96..bafccb023 100644 --- a/src/cli/commands/status/command.tsx +++ b/src/cli/commands/status/command.tsx @@ -27,7 +27,6 @@ const VALID_RESOURCE_TYPES = [ 'dataset', 'knowledge-base', 'harness', - 'capacity-provider', ] as const; const VALID_STATES = ['deployed', 'local-only', 'pending-removal'] as const; diff --git a/src/cli/logging/remove-logger.ts b/src/cli/logging/remove-logger.ts index 428d175b5..30cc5642b 100644 --- a/src/cli/logging/remove-logger.ts +++ b/src/cli/logging/remove-logger.ts @@ -24,8 +24,7 @@ export interface RemoveLoggerOptions { | 'dataset' | 'knowledge-base' | 'payment-manager' - | 'payment-connector' - | 'capacity-provider'; + | 'payment-connector'; /** Name of the resource being removed */ resourceName: string; } diff --git a/src/cli/operations/agent/generate/write-agent-to-project.ts b/src/cli/operations/agent/generate/write-agent-to-project.ts index a60f4404c..8163494f6 100644 --- a/src/cli/operations/agent/generate/write-agent-to-project.ts +++ b/src/cli/operations/agent/generate/write-agent-to-project.ts @@ -77,7 +77,6 @@ export async function writeAgentToProject(config: GenerateConfig, options?: Writ harnesses: [], datasets: [], payments: [], - capacityProviders: [], }; await configIO.writeProjectSpec(project); diff --git a/src/cli/operations/deploy/__tests__/preflight.test.ts b/src/cli/operations/deploy/__tests__/preflight.test.ts index 894e4eb30..f8e9a16b0 100644 --- a/src/cli/operations/deploy/__tests__/preflight.test.ts +++ b/src/cli/operations/deploy/__tests__/preflight.test.ts @@ -162,76 +162,6 @@ describe('validateProject', () => { expect(result.isTeardownDeploy).toBe(false); }); - it('allows deploy when only config bundles are defined (regression: previously misclassified as empty)', async () => { - mockRequireConfigRoot.mockReturnValue('/project/agentcore'); - mockValidate.mockReturnValue(undefined); - mockReadProjectSpec.mockResolvedValue({ - name: 'test-project', - runtimes: [], - agentCoreGateways: [], - configBundles: [{ name: 'bundle1' }], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockValidateAwsCredentials.mockResolvedValue(undefined); - - const result = await validateProject(); - - expect(result.projectSpec.name).toBe('test-project'); - expect(result.isTeardownDeploy).toBe(false); - }); - - it('allows deploy when only online eval configs are defined (regression: previously misclassified as empty)', async () => { - mockRequireConfigRoot.mockReturnValue('/project/agentcore'); - mockValidate.mockReturnValue(undefined); - mockReadProjectSpec.mockResolvedValue({ - name: 'test-project', - runtimes: [], - agentCoreGateways: [], - onlineEvalConfigs: [{ name: 'oec1' }], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockValidateAwsCredentials.mockResolvedValue(undefined); - - const result = await validateProject(); - - expect(result.projectSpec.name).toBe('test-project'); - expect(result.isTeardownDeploy).toBe(false); - }); - - it('allows deploy when only capacity providers are defined', async () => { - mockRequireConfigRoot.mockReturnValue('/project/agentcore'); - mockValidate.mockReturnValue(undefined); - mockReadProjectSpec.mockResolvedValue({ - name: 'test-project', - runtimes: [], - agentCoreGateways: [], - capacityProviders: [{ name: 'cp1' }], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockValidateAwsCredentials.mockResolvedValue(undefined); - - const result = await validateProject(); - - expect(result.projectSpec.name).toBe('test-project'); - expect(result.isTeardownDeploy).toBe(false); - }); - - it('treats an empty project as teardown when a deployed stack exists', async () => { - mockRequireConfigRoot.mockReturnValue('/project/agentcore'); - mockValidate.mockReturnValue(undefined); - mockReadProjectSpec.mockResolvedValue({ - name: 'test-project', - runtimes: [], - agentCoreGateways: [], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockReadDeployedState.mockResolvedValue({ targets: { default: {} } }); - - const result = await validateProject(); - - expect(result.isTeardownDeploy).toBe(true); - }); - it('allows deploy when both agents and gateways exist', async () => { mockRequireConfigRoot.mockReturnValue('/project/agentcore'); mockValidate.mockReturnValue(undefined); diff --git a/src/cli/operations/deploy/preflight.ts b/src/cli/operations/deploy/preflight.ts index 61cd8a196..75c34b41a 100644 --- a/src/cli/operations/deploy/preflight.ts +++ b/src/cli/operations/deploy/preflight.ts @@ -67,43 +67,6 @@ export function formatError(err: unknown): string { return String(err); } -/** - * Spec arrays whose presence means the project has something to deploy to CloudFormation. - * Keep in sync with the resource types that emit CFN outputs (see cloudformation/outputs.ts - * parse*Outputs). A project with none of these is empty: deploy either errors ("No resources - * defined") or, when a stack already exists, tears it down. - * - * This is the single source of truth for "is the project deployable" — a new deployable primitive - * adds its key here and every consumer stays correct. Previously this was a hand-maintained boolean - * chain that silently drifted (configBundles and onlineEvalConfigs were both missing from it, so a - * project containing only those was misclassified as empty). The `satisfies` clause makes a typo'd - * or renamed key a compile error. - */ -export const DEPLOYABLE_RESOURCE_KEYS = [ - 'runtimes', - 'agentCoreGateways', - 'memories', - 'knowledgeBases', - 'evaluators', - 'onlineEvalConfigs', - 'policyEngines', - 'configBundles', - 'datasets', - 'capacityProviders', - 'harnesses', - 'payments', -] as const satisfies readonly (keyof AgentCoreProjectSpec)[]; - -/** - * True when the project defines at least one resource that deploys to CloudFormation. - */ -export function hasDeployableResources(spec: AgentCoreProjectSpec): boolean { - return DEPLOYABLE_RESOURCE_KEYS.some(key => { - const value = spec[key]; - return Array.isArray(value) && value.length > 0; - }); -} - /** * Validates the CDK project and loads configuration. * Also validates AWS credentials are configured before proceeding. @@ -135,11 +98,32 @@ export async function validateProject(selectedTarget?: AwsDeploymentTarget): Pro // No deployed state file — no existing stack } - // Teardown detection: when no deployable resources remain but deployed-state.json records - // existing targets, the user has run `remove all` and wants to tear down AWS resources via deploy. + // Teardown detection: when agents is empty but deployed-state.json records existing + // targets, the user has run `remove all` and wants to tear down AWS resources via deploy. let isTeardownDeploy = false; - - if (!hasDeployableResources(projectSpec)) { + const hasAgents = projectSpec.runtimes && projectSpec.runtimes.length > 0; + const hasMemories = projectSpec.memories && projectSpec.memories.length > 0; + const hasKnowledgeBases = projectSpec.knowledgeBases && projectSpec.knowledgeBases.length > 0; + const hasEvaluators = projectSpec.evaluators && projectSpec.evaluators.length > 0; + const hasPolicyEngines = projectSpec.policyEngines && projectSpec.policyEngines.length > 0; + const hasHarnesses = projectSpec.harnesses && projectSpec.harnesses.length > 0; + const hasDatasets = projectSpec.datasets && projectSpec.datasets.length > 0; + + // Check for gateways in agentcore.json + const hasGateways = projectSpec.agentCoreGateways && projectSpec.agentCoreGateways.length > 0; + const hasPayments = projectSpec.payments && projectSpec.payments.length > 0; + + if ( + !hasAgents && + !hasGateways && + !hasMemories && + !hasKnowledgeBases && + !hasEvaluators && + !hasPolicyEngines && + !hasHarnesses && + !hasDatasets && + !hasPayments + ) { if (!hasExistingStack) { throw new ValidationError( 'No resources defined in project. Add at least one resource (agent, memory, knowledge base, evaluator, or gateway) before deploying.' diff --git a/src/cli/primitives/CapacityProviderPrimitive.ts b/src/cli/primitives/CapacityProviderPrimitive.ts deleted file mode 100644 index d823d94f6..000000000 --- a/src/cli/primitives/CapacityProviderPrimitive.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { ResourceNotFoundError, ValidationError, findConfigRoot, serializeResult, toError } from '../../lib'; -import type { Result } from '../../lib/result'; -import type { CapacityProvider } from '../../schema'; -import { CapacityProviderSchema } from '../../schema'; -import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; -import { runCliCommand } from '../telemetry/cli-command-run.js'; -import { OperatingSystem, standardize } from '../telemetry/schemas/common-shapes.js'; -import { BasePrimitive } from './BasePrimitive'; -import type { AddResult, AddScreenComponent, RemovableResource } from './types'; -import type { Command } from '@commander-js/extra-typings'; - -/** - * Options for adding a capacity provider resource (CLI-level). - */ -export interface AddCapacityProviderOptions { - name: string; - operatorRoleArn?: string; - description?: string; - subnets: string; - securityGroups: string; - os?: string; - instanceTypes: string; - volume?: string[]; - volumeEncrypted?: boolean; - volumeKmsKey?: string; - instanceProfileArn?: string; - idleInstanceTimeout?: string; - maxLifetime?: string; -} - -/** Split a comma-separated CLI value into a trimmed, non-empty string array. */ -function splitList(value: string): string[] { - return value - .split(',') - .map(s => s.trim()) - .filter(Boolean); -} - -/** - * CapacityProviderPrimitive handles capacity provider add/remove operations. - * - * A capacity provider is a declarative resource stored in agentcore.json and - * synthesized to an `AWS::BedrockAgentCore::CapacityProvider` CFN resource by - * the vended CDK project. Everything except Description/Tags is immutable after - * creation. - */ -export class CapacityProviderPrimitive extends BasePrimitive { - readonly kind = 'capacity-provider'; - readonly label = 'Capacity Provider'; - readonly primitiveSchema = CapacityProviderSchema; - - async add(options: AddCapacityProviderOptions): Promise> { - try { - const capacityProvider = this.buildCapacityProvider(options); - - const project = await this.readProjectSpec(); - this.checkDuplicate(project.capacityProviders ?? [], capacityProvider.name); - - project.capacityProviders = [...(project.capacityProviders ?? []), capacityProvider]; - await this.writeProjectSpec(project); - - return { success: true, capacityProviderName: capacityProvider.name }; - } catch (err) { - return { success: false, error: toError(err) }; - } - } - - async remove(name: string): Promise { - try { - const project = await this.readProjectSpec(); - const existing = project.capacityProviders ?? []; - - if (!existing.some(cp => cp.name === name)) { - return { success: false, error: new ResourceNotFoundError(`Capacity provider "${name}" not found.`) }; - } - - const remaining = existing.filter(cp => cp.name !== name); - await this.writeProjectSpec({ - ...project, - capacityProviders: remaining.length > 0 ? remaining : undefined, - }); - - return { success: true }; - } catch (err) { - return { success: false, error: toError(err) }; - } - } - - async previewRemove(name: string): Promise { - const project = await this.readProjectSpec(); - const existing = project.capacityProviders ?? []; - - if (!existing.some(cp => cp.name === name)) { - throw new Error(`Capacity provider "${name}" not found.`); - } - - const remaining = existing.filter(cp => cp.name !== name); - const schemaChanges: SchemaChange[] = [ - { - file: 'agentcore/agentcore.json', - before: project, - after: { ...project, capacityProviders: remaining.length > 0 ? remaining : undefined }, - }, - ]; - - return { - summary: [`Removing capacity provider: ${name}`], - directoriesToDelete: [], - schemaChanges, - }; - } - - async getRemovable(): Promise { - try { - const project = await this.readProjectSpec(); - return (project.capacityProviders ?? []).map(cp => ({ name: cp.name })); - } catch { - return []; - } - } - - /** Names of all capacity providers in the project (for duplicate checks in the TUI). */ - async getAllNames(): Promise { - try { - const project = await this.readProjectSpec(); - return (project.capacityProviders ?? []).map(cp => cp.name); - } catch { - return []; - } - } - - registerCommands(addCmd: Command, removeCmd: Command): void { - addCmd - .command('capacity-provider') - .description('Add a capacity provider (customer-managed EC2 compute pool for agent runtimes)') - .option('--name ', 'Capacity provider name [non-interactive]') - .option( - '--operator-role-arn ', - 'IAM role ARN AgentCore assumes to manage the capacity provider. Optional — omit to have one created automatically [non-interactive]' - ) - .option('--description ', 'Capacity provider description [non-interactive]') - .option('--subnets ', 'Comma-separated subnet IDs (1-16) [non-interactive]') - .option('--security-groups ', 'Comma-separated security group IDs (1-16) [non-interactive]') - .option('--os ', 'Operating system: LINUX_X86_64 or LINUX_ARM64 (default: LINUX_X86_64) [non-interactive]') - .option('--instance-types ', 'Comma-separated allowed EC2 instance types (1-30) [non-interactive]') - .option( - '--volume ', - 'Named EBS volume as name:sizeGiB (repeatable, max 5) [non-interactive]', - (val: string, prev: string[] = []) => [...prev, val] - ) - .option('--volume-encrypted', 'Encrypt EBS volumes [non-interactive]') - .option('--volume-kms-key ', 'KMS key ARN for EBS volume encryption [non-interactive]') - .option('--instance-profile-arn ', 'IAM instance profile ARN for launched instances [non-interactive]') - .option('--idle-instance-timeout ', 'Idle instance timeout in seconds (60-1209600) [non-interactive]') - .option('--max-lifetime ', 'Maximum instance lifetime in seconds (60-1209600) [non-interactive]') - .option('--json', 'Output as JSON [non-interactive]') - .action(async (rawOptions: Record) => { - const cliOptions = rawOptions as unknown as AddCapacityProviderOptions & { json?: boolean }; - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - await runCliCommand('add.capacity-provider', !!cliOptions.json, async () => { - this.validateRequiredOptions(cliOptions); - - const result = await this.add(cliOptions); - if (!result.success) { - throw result.error; - } - - if (cliOptions.json) { - console.log(JSON.stringify(serializeResult(result))); - } else { - console.log(`Added capacity provider '${result.capacityProviderName}'`); - } - - const built = this.buildCapacityProvider(cliOptions); - const ec2 = built.computeConfiguration.ec2Configuration; - return { - operating_system: standardize(OperatingSystem, ec2.launchTemplateSource.launchParameters.operatingSystem), - instance_type_count: - ec2.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes.length, - subnet_count: ec2.vpcConfiguration.subnets.length, - security_group_count: ec2.vpcConfiguration.securityGroups.length, - volume_count: ec2.volumes?.length ?? 0, - has_description: !!built.description, - }; - }); - }); - - this.registerRemoveSubcommand(removeCmd); - } - - addScreen(): AddScreenComponent { - return null; - } - - /** - * Validate that all required CLI flags are present, throwing ValidationError - * with an actionable message when they are not. - */ - private validateRequiredOptions(options: AddCapacityProviderOptions): void { - const missing: string[] = []; - if (!options.name) missing.push('--name'); - if (!options.subnets) missing.push('--subnets'); - if (!options.securityGroups) missing.push('--security-groups'); - if (!options.instanceTypes) missing.push('--instance-types'); - if (missing.length > 0) { - throw new ValidationError(`Missing required option(s): ${missing.join(', ')}`); - } - } - - /** - * Build a validated CapacityProvider config from CLI options. - * Zod validation (via CapacityProviderSchema.parse) rejects bad input here, - * at `add` time, rather than late at deploy/CFN time. - */ - private buildCapacityProvider(options: AddCapacityProviderOptions): CapacityProvider { - const volumes = (options.volume ?? []).map(entry => { - // Require exactly `name:sizeGiB`. Splitting without a segment count lets `data:20:gp3` - // silently drop the trailing segment, and Number() accepts hex/exponent (`0x14`, `2e1`) - // as 20 — so validate the size as literal digits instead of trusting Number(). - const segments = entry.split(':'); - const [volName, sizeRaw] = segments; - if (segments.length !== 2 || !volName || !sizeRaw || !/^[0-9]+$/.test(sizeRaw)) { - throw new ValidationError(`Invalid --volume "${entry}". Expected format name:sizeGiB (e.g. data:20).`); - } - const sizeGiB = Number(sizeRaw); - return { - ebsConfiguration: { - name: volName, - sizeGiB, - ...(options.volumeEncrypted !== undefined && { encrypted: options.volumeEncrypted }), - ...(options.volumeKmsKey && { kmsKeyId: options.volumeKmsKey }), - }, - }; - }); - - const lifecycle: { idleInstanceTimeout?: number; maxLifetime?: number } = {}; - if (options.idleInstanceTimeout !== undefined) { - lifecycle.idleInstanceTimeout = Number(options.idleInstanceTimeout); - } - if (options.maxLifetime !== undefined) { - lifecycle.maxLifetime = Number(options.maxLifetime); - } - - const candidate = { - name: options.name, - ...(options.description && { description: options.description }), - ...(options.operatorRoleArn && { operatorRoleArn: options.operatorRoleArn }), - computeConfiguration: { - ec2Configuration: { - launchTemplateSource: { - launchParameters: { - operatingSystem: options.os ?? 'LINUX_X86_64', - instanceRequirements: { - allowedInstanceTypes: splitList(options.instanceTypes), - }, - ...(options.instanceProfileArn && { instanceProfileArn: options.instanceProfileArn }), - }, - }, - vpcConfiguration: { - subnets: splitList(options.subnets), - securityGroups: splitList(options.securityGroups), - }, - ...(volumes.length > 0 && { volumes }), - ...(Object.keys(lifecycle).length > 0 && { lifecycleConfiguration: lifecycle }), - }, - }, - }; - - return CapacityProviderSchema.parse(candidate); - } -} diff --git a/src/cli/primitives/__tests__/CapacityProviderPrimitive.test.ts b/src/cli/primitives/__tests__/CapacityProviderPrimitive.test.ts deleted file mode 100644 index 09fcfb629..000000000 --- a/src/cli/primitives/__tests__/CapacityProviderPrimitive.test.ts +++ /dev/null @@ -1,296 +0,0 @@ -import type { AgentCoreProjectSpec, CapacityProvider } from '../../../schema'; -import type { AddCapacityProviderOptions } from '../CapacityProviderPrimitive'; -import { CapacityProviderPrimitive } from '../CapacityProviderPrimitive'; -import { afterEach, describe, expect, it, vi } from 'vitest'; - -const mockReadProjectSpec = vi.fn(); -const mockWriteProjectSpec = vi.fn(); - -vi.mock('../../../lib', async importOriginal => { - const actual = await importOriginal(); - return { - ...actual, - ConfigIO: class { - readProjectSpec = mockReadProjectSpec; - writeProjectSpec = mockWriteProjectSpec; - }, - findConfigRoot: vi.fn().mockReturnValue(null), - }; -}); - -function makeProject(overrides: Partial = {}): AgentCoreProjectSpec { - return { - name: 'TestProject', - version: 1, - managedBy: 'CDK' as const, - runtimes: [], - memories: [], - knowledgeBases: [], - credentials: [], - evaluators: [], - onlineEvalConfigs: [], - agentCoreGateways: [], - policyEngines: [], - configBundles: [], - abTests: [], - httpGateways: [], - harnesses: [], - datasets: [], - payments: [], - ...overrides, - }; -} - -const OPERATOR_ROLE_ARN = 'arn:aws:iam::123456789012:role/MyOperatorRole'; - -function baseOptions(overrides: Partial = {}): AddCapacityProviderOptions { - return { - name: 'myCp', - operatorRoleArn: OPERATOR_ROLE_ARN, - subnets: 'subnet-0123456789abcdef0', - securityGroups: 'sg-0123456789abcdef0', - instanceTypes: 'c6a.large', - ...overrides, - }; -} - -function makeCapacityProvider(name: string): CapacityProvider { - return { - name, - operatorRoleArn: OPERATOR_ROLE_ARN, - computeConfiguration: { - ec2Configuration: { - launchTemplateSource: { - launchParameters: { - operatingSystem: 'LINUX_X86_64', - instanceRequirements: { allowedInstanceTypes: ['c6a.large'] }, - }, - }, - vpcConfiguration: { subnets: ['subnet-0123456789abcdef0'], securityGroups: ['sg-0123456789abcdef0'] }, - }, - }, - }; -} - -const primitive = new CapacityProviderPrimitive(); - -describe('CapacityProviderPrimitive', () => { - afterEach(() => vi.clearAllMocks()); - - describe('add()', () => { - it('happy path — adds a capacity provider to spec and returns success', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - mockWriteProjectSpec.mockResolvedValue(undefined); - - const result = await primitive.add(baseOptions()); - - expect(result.success).toBe(true); - expect(result).toHaveProperty('capacityProviderName', 'myCp'); - - const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; - expect(written.capacityProviders).toHaveLength(1); - const cp = written.capacityProviders![0]!; - expect(cp.name).toBe('myCp'); - expect(cp.operatorRoleArn).toBe(OPERATOR_ROLE_ARN); - const ec2 = cp.computeConfiguration.ec2Configuration; - expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_X86_64'); - expect(ec2.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes).toEqual([ - 'c6a.large', - ]); - expect(ec2.vpcConfiguration.subnets).toEqual(['subnet-0123456789abcdef0']); - expect(ec2.vpcConfiguration.securityGroups).toEqual(['sg-0123456789abcdef0']); - }); - - it('omitting the operator role ARN succeeds — the role is created at deploy time', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - mockWriteProjectSpec.mockResolvedValue(undefined); - - const result = await primitive.add(baseOptions({ operatorRoleArn: undefined })); - - expect(result.success).toBe(true); - const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; - const cp = written.capacityProviders![0]!; - // The field is omitted entirely (not written as undefined) so the construct auto-creates the role. - expect(cp).not.toHaveProperty('operatorRoleArn'); - }); - - it('parses multi-value flags, volumes, and lifecycle', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - mockWriteProjectSpec.mockResolvedValue(undefined); - - await primitive.add( - baseOptions({ - os: 'LINUX_ARM64', - description: 'my cp', - instanceTypes: 'c7g.large, c7g.xlarge', - subnets: 'subnet-0123456789abcdef0,subnet-0fedcba9876543210', - securityGroups: 'sg-0123456789abcdef0', - volume: ['data:20'], - volumeEncrypted: true, - idleInstanceTimeout: '3600', - maxLifetime: '28800', - }) - ); - - const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; - const ec2 = written.capacityProviders![0]!.computeConfiguration.ec2Configuration; - expect(ec2.launchTemplateSource.launchParameters.operatingSystem).toBe('LINUX_ARM64'); - expect(ec2.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes).toEqual([ - 'c7g.large', - 'c7g.xlarge', - ]); - expect(ec2.vpcConfiguration.subnets).toHaveLength(2); - expect(ec2.volumes).toEqual([{ ebsConfiguration: { name: 'data', sizeGiB: 20, encrypted: true } }]); - expect(ec2.lifecycleConfiguration).toEqual({ idleInstanceTimeout: 3600, maxLifetime: 28800 }); - expect(written.capacityProviders![0]!.description).toBe('my cp'); - }); - - it('duplicate name — returns error without writing', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject({ capacityProviders: [makeCapacityProvider('myCp')] })); - - const result = await primitive.add(baseOptions()); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.message).toContain('myCp'); - expect(result.error.message).toContain('already exists'); - } - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - - it('rejects an operator role ARN with a malformed shape', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - - const result = await primitive.add(baseOptions({ operatorRoleArn: 'not-an-arn' })); - - expect(result.success).toBe(false); - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - - it('accepts an operator role ARN without an account id (account segment optional)', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - mockWriteProjectSpec.mockResolvedValue(undefined); - - const result = await primitive.add(baseOptions({ operatorRoleArn: 'arn:aws:iam:::role/MyRole' })); - - expect(result.success).toBe(true); - }); - - it('rejects an unsupported operating system value', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - - const result = await primitive.add(baseOptions({ os: 'WINDOWS_X86_64' })); - - expect(result.success).toBe(false); - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - - it('rejects a malformed --volume value', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - - const result = await primitive.add(baseOptions({ volume: ['data-no-size'] })); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.message).toContain('name:sizeGiB'); - } - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - - // A plain split(':') + Number() silently accepted all of these before the fix: - // extra segments were dropped, and hex/exponent notation coerced to a number. - it.each([ - ['extra segment', 'data:20:gp3'], - ['hex size', 'data:0x14'], - ['exponent size', 'data:2e1'], - ['decimal size', 'data:20.5'], - ['empty size', 'data:'], - ])('rejects a --volume with %s (%s)', async (_label, value) => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - - const result = await primitive.add(baseOptions({ volume: [value] })); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.message).toContain('name:sizeGiB'); - } - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - }); - - describe('remove()', () => { - it('removes a capacity provider from spec', async () => { - const project = makeProject({ - capacityProviders: [makeCapacityProvider('cpA'), makeCapacityProvider('cpB')], - }); - mockReadProjectSpec.mockResolvedValue(project); - mockWriteProjectSpec.mockResolvedValue(undefined); - - const result = await primitive.remove('cpA'); - - expect(result.success).toBe(true); - const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; - expect(written.capacityProviders).toHaveLength(1); - expect(written.capacityProviders![0]!.name).toBe('cpB'); - }); - - it('drops the array to undefined when removing the last capacity provider', async () => { - const project = makeProject({ capacityProviders: [makeCapacityProvider('only')] }); - mockReadProjectSpec.mockResolvedValue(project); - mockWriteProjectSpec.mockResolvedValue(undefined); - - await primitive.remove('only'); - - const written = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; - expect(written.capacityProviders).toBeUndefined(); - }); - - it('non-existent name — returns error without writing', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - - const result = await primitive.remove('missing'); - - expect(result.success).toBe(false); - if (!result.success) { - expect(result.error.message).toContain('missing'); - expect(result.error.message).toContain('not found'); - } - expect(mockWriteProjectSpec).not.toHaveBeenCalled(); - }); - }); - - describe('getRemovable()', () => { - it('returns capacity provider names from spec', async () => { - mockReadProjectSpec.mockResolvedValue( - makeProject({ capacityProviders: [makeCapacityProvider('alpha'), makeCapacityProvider('beta')] }) - ); - - expect(await primitive.getRemovable()).toEqual([{ name: 'alpha' }, { name: 'beta' }]); - }); - - it('returns empty array when none exist', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - expect(await primitive.getRemovable()).toEqual([]); - }); - }); - - describe('previewRemove()', () => { - it('returns summary and schema changes', async () => { - const project = makeProject({ capacityProviders: [makeCapacityProvider('previewCp')] }); - mockReadProjectSpec.mockResolvedValue(project); - - const preview = await primitive.previewRemove('previewCp'); - - expect(preview.summary[0]).toContain('previewCp'); - expect(preview.schemaChanges).toHaveLength(1); - expect(preview.schemaChanges[0]!.file).toBe('agentcore/agentcore.json'); - const after = preview.schemaChanges[0]!.after as AgentCoreProjectSpec; - expect(after.capacityProviders).toBeUndefined(); - }); - - it('throws when not found', async () => { - mockReadProjectSpec.mockResolvedValue(makeProject()); - await expect(primitive.previewRemove('missing')).rejects.toThrow('not found'); - }); - }); -}); diff --git a/src/cli/primitives/index.ts b/src/cli/primitives/index.ts index 105d40d8a..7e711ec6c 100644 --- a/src/cli/primitives/index.ts +++ b/src/cli/primitives/index.ts @@ -10,8 +10,6 @@ export { EvaluatorPrimitive } from './EvaluatorPrimitive'; export { OnlineEvalConfigPrimitive } from './OnlineEvalConfigPrimitive'; export { GatewayPrimitive } from './GatewayPrimitive'; export { GatewayTargetPrimitive } from './GatewayTargetPrimitive'; -export { CapacityProviderPrimitive } from './CapacityProviderPrimitive'; -export type { AddCapacityProviderOptions } from './CapacityProviderPrimitive'; export { RuntimeEndpointPrimitive } from './RuntimeEndpointPrimitive'; export type { AddRuntimeEndpointOptions, RemovableRuntimeEndpoint } from './RuntimeEndpointPrimitive'; export { @@ -25,7 +23,6 @@ export { onlineEvalConfigPrimitive, gatewayPrimitive, gatewayTargetPrimitive, - capacityProviderPrimitive, configBundlePrimitive, runtimeEndpointPrimitive, getPrimitive, diff --git a/src/cli/primitives/registry.ts b/src/cli/primitives/registry.ts index c6e26ea78..d578c4b6f 100644 --- a/src/cli/primitives/registry.ts +++ b/src/cli/primitives/registry.ts @@ -1,6 +1,5 @@ import { AgentPrimitive } from './AgentPrimitive'; import type { BasePrimitive } from './BasePrimitive'; -import { CapacityProviderPrimitive } from './CapacityProviderPrimitive'; import { ConfigBundlePrimitive } from './ConfigBundlePrimitive'; import { CredentialPrimitive } from './CredentialPrimitive'; import { DatasetPrimitive } from './DatasetPrimitive'; @@ -39,7 +38,6 @@ export const configBundlePrimitive = new ConfigBundlePrimitive(); export const runtimeEndpointPrimitive = new RuntimeEndpointPrimitive(); export const paymentManagerPrimitive = new PaymentManagerPrimitive(); export const paymentConnectorPrimitive = new PaymentConnectorPrimitive(); -export const capacityProviderPrimitive = new CapacityProviderPrimitive(); /** * All primitives in display order. @@ -62,7 +60,6 @@ export const ALL_PRIMITIVES: BasePrimitive[] = [ runtimeEndpointPrimitive, paymentManagerPrimitive, paymentConnectorPrimitive, - capacityProviderPrimitive, ]; /** diff --git a/src/cli/project.ts b/src/cli/project.ts index d5886ab66..3399042ab 100644 --- a/src/cli/project.ts +++ b/src/cli/project.ts @@ -24,7 +24,6 @@ export function createDefaultProjectSpec(projectName: string): AgentCoreProjectS abTests: [], datasets: [], payments: [], - capacityProviders: [], tags: { 'agentcore:created-by': 'agentcore-cli', 'agentcore:project-name': projectName, diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index ab091bd27..9e91b8357 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -25,7 +25,6 @@ import { Mode, ModelProvider, NetworkMode, - OperatingSystem, OutboundAuthType, PolicyAttrSourceType, PolicyEngineMode, @@ -114,15 +113,6 @@ const AddPolicyAttrs = safeSchema({ const AddSkillAttrs = safeSchema({ skill_source_type: SkillSourceType }); -const AddCapacityProviderAttrs = safeSchema({ - operating_system: OperatingSystem, - instance_type_count: Count, - subnet_count: Count, - security_group_count: Count, - volume_count: Count, - has_description: z.boolean(), -}); - const DeployAttrs = safeSchema({ runtime_count: Count, harness_count: Count, @@ -254,7 +244,6 @@ export const COMMAND_SCHEMAS = { 'add.knowledge-base': AddKnowledgeBaseAttrs, 'add.payment-manager': NoAttrs, 'add.payment-connector': NoAttrs, - 'add.capacity-provider': AddCapacityProviderAttrs, 'add.skill': AddSkillAttrs, deploy: DeployAttrs, @@ -318,7 +307,6 @@ export const COMMAND_SCHEMAS = { 'dataset.remove-version': NoAttrs, 'remove.payment-manager': NoAttrs, 'remove.payment-connector': NoAttrs, - 'remove.capacity-provider': NoAttrs, 'remove.skill': NoAttrs, 'telemetry.disable': NoAttrs, 'telemetry.enable': NoAttrs, diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 2c4064390..071df8956 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -55,7 +55,6 @@ export const FilterType = z.enum([ 'config-bundle', 'dataset', 'harness', - 'capacity-provider', 'none', ]); export const AgentEnvironment = z.enum(['harness', 'runtime']); @@ -90,7 +89,6 @@ export const MemoryType = z.enum(['none', 'shortterm', 'longandshortterm']); export const Mode = z.enum(['cli', 'tui']); export const ModelProvider = z.enum(['bedrock', 'anthropic', 'openai', 'gemini', 'lite_llm']); export const NetworkMode = z.enum(['public', 'vpc']); -export const OperatingSystem = z.enum(['linux_x86_64', 'linux_arm64']); export const OutboundAuthType = z.enum(['oauth', 'api-key', 'none']); export const PolicyEngineMode = z.enum(['log_only', 'enforce']); export const AgentProtocol = z.enum(['http', 'mcp', 'a2a', 'agui']); diff --git a/src/cli/tui/components/ResourceGraph.tsx b/src/cli/tui/components/ResourceGraph.tsx index 5e4bd80f8..f7f748035 100644 --- a/src/cli/tui/components/ResourceGraph.tsx +++ b/src/cli/tui/components/ResourceGraph.tsx @@ -26,7 +26,6 @@ const ICONS = { 'runtime-endpoint': '◉', 'knowledge-base': '✚', payment: '₿', - 'capacity-provider': '▦', } as const; interface ResourceGraphProps { @@ -139,7 +138,6 @@ export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: Res const configBundles = project.configBundles ?? []; const datasets = project.datasets ?? []; const payments = project.payments ?? []; - const capacityProviders = project.capacityProviders ?? []; const harnesses = project.harnesses ?? []; // Build lookup map and collect pending-removal resources in a single pass @@ -439,29 +437,6 @@ export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: Res )} - {/* Capacity Providers */} - {capacityProviders.length > 0 && ( - - Capacity Providers - {capacityProviders.map(cp => { - const rsEntry = statusMap.get(`capacity-provider:${cp.name}`); - const localDetail = - cp.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters.operatingSystem; - return ( - - ); - })} - - )} - {/* Removed locally — still deployed in AWS, will be torn down on next deploy */} {pendingRemovals.length > 0 && ( diff --git a/src/cli/tui/hooks/useRemove.ts b/src/cli/tui/hooks/useRemove.ts index 1ccaf9182..48eaca8b8 100644 --- a/src/cli/tui/hooks/useRemove.ts +++ b/src/cli/tui/hooks/useRemove.ts @@ -9,7 +9,6 @@ import type { RemovablePolicyResource } from '../../primitives/PolicyPrimitive'; import type { RemovableRuntimeEndpoint } from '../../primitives/RuntimeEndpointPrimitive'; import { agentPrimitive, - capacityProviderPrimitive, configBundlePrimitive, credentialPrimitive, datasetPrimitive, @@ -163,11 +162,6 @@ export function useRemovableDatasets() { return { datasets, ...rest }; } -export function useRemovableCapacityProviders() { - const { items: capacityProviders, ...rest } = useRemovableResources(() => capacityProviderPrimitive.getRemovable()); - return { capacityProviders, ...rest }; -} - export function useRemovableKnowledgeBases() { const { items: knowledgeBases, ...rest } = useRemovableResources(() => knowledgeBasePrimitive.getRemovable()); return { knowledgeBases, ...rest }; @@ -303,11 +297,6 @@ export function useRemovalPreview() { [loadPreview] ); - const loadCapacityProviderPreview = useCallback( - (name: string) => loadPreview(n => capacityProviderPrimitive.previewRemove(n), name), - [loadPreview] - ); - const reset = useCallback(() => { setState({ isLoading: false, preview: null, error: null }); }, []); @@ -328,7 +317,6 @@ export function useRemovalPreview() { loadPolicyPreview, loadConfigBundlePreview, loadRuntimeEndpointPreview, - loadCapacityProviderPreview, reset, }; } @@ -408,14 +396,6 @@ export function useRemoveDataset() { ); } -export function useRemoveCapacityProvider() { - return useRemoveResource( - (name: string) => capacityProviderPrimitive.remove(name), - 'capacity-provider', - name => name - ); -} - export function useRemoveKnowledgeBase() { return useRemoveResource( (name: string) => knowledgeBasePrimitive.remove(name), diff --git a/src/cli/tui/screens/add/AddFlow.tsx b/src/cli/tui/screens/add/AddFlow.tsx index c7b837f8e..dab2f1433 100644 --- a/src/cli/tui/screens/add/AddFlow.tsx +++ b/src/cli/tui/screens/add/AddFlow.tsx @@ -7,7 +7,6 @@ import { AddAgentFlow } from '../agent/AddAgentFlow'; import type { AddAgentConfig } from '../agent/types'; import { FRAMEWORK_OPTIONS } from '../agent/types'; import { useAddAgent } from '../agent/useAddAgent'; -import { AddCapacityProviderFlow } from '../capacity-provider'; import { AddConfigBundleFlow } from '../config-bundle'; import { AddDatasetFlow } from '../dataset'; import { AddEvaluatorFlow } from '../evaluator'; @@ -46,7 +45,6 @@ type FlowState = | { name: 'runtime-endpoint-wizard' } | { name: 'payment-manager-wizard' } | { name: 'payment-connector-wizard' } - | { name: 'capacity-provider-wizard' } | { name: 'agent-create-success'; agentName: string; @@ -222,8 +220,6 @@ function getInitialFlowState(resource?: AddResourceType): FlowState { return { name: 'payment-manager-wizard' }; case 'payment-connector': return { name: 'payment-connector-wizard' }; - case 'capacity-provider': - return { name: 'capacity-provider-wizard' }; default: return { name: 'select' }; } @@ -300,9 +296,6 @@ export function AddFlow(props: AddFlowProps) { case 'payment-connector': setFlow({ name: 'payment-connector-wizard' }); break; - case 'capacity-provider': - setFlow({ name: 'capacity-provider-wizard' }); - break; } }, []); @@ -647,19 +640,6 @@ export function AddFlow(props: AddFlowProps) { ); } - // Capacity provider wizard - if (flow.name === 'capacity-provider-wizard') { - return ( - setFlow({ name: 'select' })} - onDev={props.onDev} - onDeploy={props.onDeploy} - /> - ); - } - return ( { expect(lastFrame()).toContain('Payment Manager'); expect(lastFrame()).toContain('Payment Connector'); }); - - it('capacity provider is a top-level option', () => { - const { lastFrame } = render(); - - expect(lastFrame()).toContain('Capacity Provider'); - }); }); diff --git a/src/cli/tui/screens/capacity-provider/AddCapacityProviderFlow.tsx b/src/cli/tui/screens/capacity-provider/AddCapacityProviderFlow.tsx deleted file mode 100644 index 2fc056e79..000000000 --- a/src/cli/tui/screens/capacity-provider/AddCapacityProviderFlow.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { capacityProviderPrimitive } from '../../../primitives/registry'; -import { ErrorPrompt } from '../../components'; -import { AddSuccessScreen } from '../add/AddSuccessScreen'; -import type { AddCapacityProviderConfig } from './AddCapacityProviderScreen'; -import { AddCapacityProviderScreen } from './AddCapacityProviderScreen'; -import { Box, Text } from 'ink'; -import React, { useCallback, useEffect, useState } from 'react'; - -type FlowState = - | { name: 'create-wizard' } - | { name: 'create-success'; capacityProviderName: string; os: string; instanceTypes: string; description?: string } - | { name: 'error'; message: string }; - -interface AddCapacityProviderFlowProps { - isInteractive?: boolean; - onExit: () => void; - onBack: () => void; - onDev?: () => void; - onDeploy?: () => void; -} - -export function AddCapacityProviderFlow({ - isInteractive = true, - onExit, - onBack, - onDev, - onDeploy, -}: AddCapacityProviderFlowProps) { - const [flow, setFlow] = useState({ name: 'create-wizard' }); - const [existingNames, setExistingNames] = useState([]); - - useEffect(() => { - void capacityProviderPrimitive.getAllNames().then(setExistingNames); - }, []); - - // In non-interactive mode, exit after success - useEffect(() => { - if (!isInteractive && flow.name === 'create-success') { - onExit(); - } - }, [isInteractive, flow.name, onExit]); - - const handleCreateComplete = useCallback((config: AddCapacityProviderConfig) => { - void capacityProviderPrimitive - .add({ - name: config.name, - operatorRoleArn: config.operatorRoleArn, - description: config.description, - subnets: config.subnets, - securityGroups: config.securityGroups, - os: config.os, - instanceTypes: config.instanceTypes, - }) - .then(result => { - if (result.success) { - setFlow({ - name: 'create-success', - capacityProviderName: result.capacityProviderName, - os: config.os, - instanceTypes: config.instanceTypes, - description: config.description, - }); - return; - } - setFlow({ name: 'error', message: result.error.message }); - }); - }, []); - - if (flow.name === 'create-wizard') { - return ( - - ); - } - - if (flow.name === 'create-success') { - return ( - - OS: {flow.os} - Instance types: {flow.instanceTypes} - {flow.description && Desc: {flow.description}} - - } - onAddAnother={onBack} - onDev={onDev} - onDeploy={onDeploy} - onExit={onExit} - /> - ); - } - - return ( - { - setFlow({ name: 'create-wizard' }); - }} - onExit={onExit} - /> - ); -} diff --git a/src/cli/tui/screens/capacity-provider/AddCapacityProviderScreen.tsx b/src/cli/tui/screens/capacity-provider/AddCapacityProviderScreen.tsx deleted file mode 100644 index 4b238ae5f..000000000 --- a/src/cli/tui/screens/capacity-provider/AddCapacityProviderScreen.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import type { OperatingSystem } from '../../../../schema'; -import { CapacityProviderNameSchema, isValidOperatorRoleArn } from '../../../../schema'; -import { ConfirmReview, Panel, Screen, StepIndicator, TextInput, WizardSelect } from '../../components'; -import type { SelectableItem } from '../../components'; -import { HELP_TEXT } from '../../constants'; -import { useListNavigation } from '../../hooks'; -import { generateUniqueName } from '../../utils'; -import React, { useMemo, useState } from 'react'; - -const OS_OPTIONS: SelectableItem[] = [ - { id: 'LINUX_X86_64', title: 'Linux x86_64', description: 'Intel/AMD 64-bit Linux instances' }, - { id: 'LINUX_ARM64', title: 'Linux ARM64', description: 'Graviton/ARM 64-bit Linux instances' }, -]; - -export interface AddCapacityProviderConfig { - name: string; - operatorRoleArn?: string; - description?: string; - subnets: string; - securityGroups: string; - os: OperatingSystem; - instanceTypes: string; -} - -type Step = - | 'name' - | 'operator-role' - | 'subnets' - | 'security-groups' - | 'os' - | 'instance-types' - | 'description' - | 'confirm'; - -const STEP_LABELS: Record = { - name: 'Name', - 'operator-role': 'Operator Role', - subnets: 'Subnets', - 'security-groups': 'Security Groups', - os: 'OS', - 'instance-types': 'Instance Types', - description: 'Description', - confirm: 'Confirm', -}; - -const STEPS: Step[] = [ - 'name', - 'operator-role', - 'subnets', - 'security-groups', - 'os', - 'instance-types', - 'description', - 'confirm', -]; - -const SUBNET_PATTERN = /^subnet-[0-9a-zA-Z]{8,17}$/; -const SECURITY_GROUP_PATTERN = /^sg-[0-9a-zA-Z]{8,17}$/; - -function splitList(value: string): string[] { - return value - .split(',') - .map(s => s.trim()) - .filter(Boolean); -} - -interface AddCapacityProviderScreenProps { - onComplete: (config: AddCapacityProviderConfig) => void; - onExit: () => void; - existingNames: string[]; -} - -export function AddCapacityProviderScreen({ onComplete, onExit, existingNames }: AddCapacityProviderScreenProps) { - const [step, setStep] = useState('name'); - const [name, setName] = useState(''); - const [operatorRoleArn, setOperatorRoleArn] = useState(''); - const [subnets, setSubnets] = useState(''); - const [securityGroups, setSecurityGroups] = useState(''); - const [os, setOs] = useState('LINUX_X86_64'); - const [instanceTypes, setInstanceTypes] = useState(''); - const [description, setDescription] = useState(''); - - const isNameStep = step === 'name'; - const isOperatorRoleStep = step === 'operator-role'; - const isSubnetsStep = step === 'subnets'; - const isSecurityGroupsStep = step === 'security-groups'; - const isOsStep = step === 'os'; - const isInstanceTypesStep = step === 'instance-types'; - const isDescriptionStep = step === 'description'; - const isConfirmStep = step === 'confirm'; - - const osNav = useListNavigation({ - items: OS_OPTIONS, - isActive: isOsStep, - onSelect: (item: SelectableItem) => { - setOs(item.id as OperatingSystem); - setStep('instance-types'); - }, - onExit: () => setStep('security-groups'), - }); - - useListNavigation({ - items: [{ id: 'confirm', title: 'Confirm' }], - onSelect: () => - onComplete({ - name, - operatorRoleArn: operatorRoleArn || undefined, - subnets, - securityGroups, - os, - instanceTypes, - description: description || undefined, - }), - onExit: () => setStep('description'), - isActive: isConfirmStep, - }); - - const helpText = isOsStep - ? HELP_TEXT.NAVIGATE_SELECT - : isConfirmStep - ? HELP_TEXT.CONFIRM_CANCEL - : HELP_TEXT.TEXT_INPUT; - - const headerContent = ; - - const confirmFields = useMemo( - () => [ - { label: 'Name', value: name }, - { label: 'Operator Role ARN', value: operatorRoleArn || '(auto-created)' }, - { label: 'Subnets', value: splitList(subnets).join(', ') }, - { label: 'Security Groups', value: splitList(securityGroups).join(', ') }, - { label: 'OS', value: os }, - { label: 'Instance Types', value: splitList(instanceTypes).join(', ') }, - ...(description ? [{ label: 'Description', value: description }] : []), - ], - [name, operatorRoleArn, subnets, securityGroups, os, instanceTypes, description] - ); - - return ( - - - {isNameStep && ( - { - setName(value); - setStep('operator-role'); - }} - onCancel={onExit} - schema={CapacityProviderNameSchema} - customValidation={value => !existingNames.includes(value) || 'Capacity provider name already exists'} - /> - )} - - {isOperatorRoleStep && ( - { - setOperatorRoleArn(value); - setStep('subnets'); - }} - onCancel={() => setStep('name')} - allowEmpty - customValidation={value => - value.trim() === '' || isValidOperatorRoleArn(value) || 'Must be a valid IAM role ARN' - } - /> - )} - - {isSubnetsStep && ( - { - setSubnets(value); - setStep('security-groups'); - }} - onCancel={() => setStep('operator-role')} - customValidation={value => { - const ids = splitList(value); - if (ids.length < 1 || ids.length > 16) return 'Provide 1-16 subnet IDs'; - return ids.every(id => SUBNET_PATTERN.test(id)) || 'Each must be a valid subnet ID (subnet-...)'; - }} - /> - )} - - {isSecurityGroupsStep && ( - { - setSecurityGroups(value); - setStep('os'); - }} - onCancel={() => setStep('subnets')} - customValidation={value => { - const ids = splitList(value); - if (ids.length < 1 || ids.length > 16) return 'Provide 1-16 security group IDs'; - return ( - ids.every(id => SECURITY_GROUP_PATTERN.test(id)) || 'Each must be a valid security group ID (sg-...)' - ); - }} - /> - )} - - {isOsStep && ( - - )} - - {isInstanceTypesStep && ( - { - setInstanceTypes(value); - setStep('description'); - }} - onCancel={() => setStep('os')} - customValidation={value => { - const types = splitList(value); - return (types.length >= 1 && types.length <= 30) || 'Provide 1-30 instance types'; - }} - /> - )} - - {isDescriptionStep && ( - { - setDescription(value); - setStep('confirm'); - }} - onCancel={() => setStep('instance-types')} - allowEmpty - /> - )} - - {isConfirmStep && } - - - ); -} diff --git a/src/cli/tui/screens/capacity-provider/index.ts b/src/cli/tui/screens/capacity-provider/index.ts deleted file mode 100644 index f9c391a36..000000000 --- a/src/cli/tui/screens/capacity-provider/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AddCapacityProviderFlow } from './AddCapacityProviderFlow'; -export { AddCapacityProviderScreen } from './AddCapacityProviderScreen'; -export type { AddCapacityProviderConfig } from './AddCapacityProviderScreen'; diff --git a/src/cli/tui/screens/remove/RemoveCapacityProviderScreen.tsx b/src/cli/tui/screens/remove/RemoveCapacityProviderScreen.tsx deleted file mode 100644 index dabb0d2f2..000000000 --- a/src/cli/tui/screens/remove/RemoveCapacityProviderScreen.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { RemovableResource } from '../../../primitives/types'; -import { SelectScreen } from '../../components'; -import React from 'react'; - -interface RemoveCapacityProviderScreenProps { - capacityProviders: RemovableResource[]; - onSelect: (capacityProviderName: string) => void; - onExit: () => void; -} - -export function RemoveCapacityProviderScreen({ - capacityProviders, - onSelect, - onExit, -}: RemoveCapacityProviderScreenProps) { - const items = capacityProviders.map(cp => ({ - id: cp.name, - title: cp.name, - description: 'Capacity Provider', - })); - - return ( - onSelect(item.id)} - onExit={onExit} - /> - ); -} diff --git a/src/cli/tui/screens/remove/RemoveFlow.tsx b/src/cli/tui/screens/remove/RemoveFlow.tsx index cfa48e439..036bf6cb4 100644 --- a/src/cli/tui/screens/remove/RemoveFlow.tsx +++ b/src/cli/tui/screens/remove/RemoveFlow.tsx @@ -4,7 +4,6 @@ import { harnessPrimitive, paymentManagerPrimitive } from '../../../primitives/r import { ErrorPrompt, Panel, Screen, SelectScreen } from '../../components'; import { useRemovableAgents, - useRemovableCapacityProviders, useRemovableConfigBundles, useRemovableDatasets, useRemovableEvaluators, @@ -21,7 +20,6 @@ import { useRemovableRuntimeEndpoints, useRemovalPreview, useRemoveAgent, - useRemoveCapacityProvider, useRemoveConfigBundle, useRemoveDataset, useRemoveEvaluator, @@ -38,7 +36,6 @@ import { } from '../../hooks/useRemove'; import { RemoveAgentScreen } from './RemoveAgentScreen'; import { RemoveAllScreen } from './RemoveAllScreen'; -import { RemoveCapacityProviderScreen } from './RemoveCapacityProviderScreen'; import { RemoveConfigBundleScreen } from './RemoveConfigBundleScreen'; import { RemoveConfirmScreen } from './RemoveConfirmScreen'; import { RemoveDatasetScreen } from './RemoveDatasetScreen'; @@ -78,7 +75,6 @@ type FlowState = | { name: 'select-config-bundle' } | { name: 'select-runtime-endpoint' } | { name: 'select-payment' } - | { name: 'select-capacity-provider' } | { name: 'confirm-agent'; agentName: string; preview: RemovalPreview } | { name: 'confirm-gateway'; gatewayName: string; preview: RemovalPreview } | { name: 'confirm-gateway-target'; tool: RemovableGatewayTarget; preview: RemovalPreview } @@ -93,7 +89,6 @@ type FlowState = | { name: 'confirm-config-bundle'; bundleName: string; preview: RemovalPreview } | { name: 'confirm-runtime-endpoint'; endpointName: string; preview: RemovalPreview } | { name: 'confirm-payment'; managerName: string; preview: RemovalPreview } - | { name: 'confirm-capacity-provider'; capacityProviderName: string; preview: RemovalPreview } | { name: 'loading'; message: string } | { name: 'harness-success'; harnessName: string; logFilePath?: string } | { name: 'agent-success'; agentName: string; logFilePath?: string } @@ -110,7 +105,6 @@ type FlowState = | { name: 'config-bundle-success'; bundleName: string; logFilePath?: string } | { name: 'runtime-endpoint-success'; endpointName: string; logFilePath?: string } | { name: 'payment-success'; managerName: string } - | { name: 'capacity-provider-success'; capacityProviderName: string; logFilePath?: string } | { name: 'remove-all' } | { name: 'error'; message: string }; @@ -142,7 +136,6 @@ interface RemoveFlowProps { | 'payment' | 'payment-manager' | 'payment-connector' - | 'capacity-provider' | 'all'; /** Initial resource name to auto-select (for CLI --name flag) */ initialResourceName?: string; @@ -193,8 +186,6 @@ export function RemoveFlow({ case 'payment-manager': case 'payment-connector': return { name: 'select-payment' }; - case 'capacity-provider': - return { name: 'select-capacity-provider' }; case 'all': return { name: 'remove-all' }; default: @@ -239,11 +230,6 @@ export function RemoveFlow({ refresh: refreshRuntimeEndpoints, } = useRemovableRuntimeEndpoints(); const { paymentManagers, isLoading: isLoadingPayments, refresh: refreshPayments } = useRemovablePaymentManagers(); - const { - capacityProviders, - isLoading: isLoadingCapacityProviders, - refresh: refreshCapacityProviders, - } = useRemovableCapacityProviders(); // Check if any data is still loading const isLoading = @@ -261,8 +247,7 @@ export function RemoveFlow({ isLoadingPolicies || isLoadingConfigBundles || isLoadingRuntimeEndpoints || - isLoadingPayments || - isLoadingCapacityProviders; + isLoadingPayments; // Preview hook const { @@ -280,7 +265,6 @@ export function RemoveFlow({ loadPolicyPreview, loadConfigBundlePreview, loadRuntimeEndpointPreview, - loadCapacityProviderPreview, reset: resetPreview, } = useRemovalPreview(); @@ -299,7 +283,6 @@ export function RemoveFlow({ const { remove: removePolicyOp, reset: resetRemovePolicy } = useRemovePolicy(); const { remove: removeConfigBundleOp, reset: resetRemoveConfigBundle } = useRemoveConfigBundle(); const { remove: removeRuntimeEndpointOp, reset: resetRemoveRuntimeEndpoint } = useRemoveRuntimeEndpoint(); - const { remove: removeCapacityProviderOp, reset: resetRemoveCapacityProvider } = useRemoveCapacityProvider(); // Track pending result state const pendingResultRef = useRef(null); @@ -336,7 +319,6 @@ export function RemoveFlow({ 'config-bundle-success', 'runtime-endpoint-success', 'payment-success', - 'capacity-provider-success', ]; if (successStates.includes(flow.name)) { onExit(); @@ -394,9 +376,6 @@ export function RemoveFlow({ case 'payment': setFlow({ name: 'select-payment' }); break; - case 'capacity-provider': - setFlow({ name: 'select-capacity-provider' }); - break; case 'all': setFlow({ name: 'remove-all' }); break; @@ -599,28 +578,6 @@ export function RemoveFlow({ [loadDatasetPreview, force, removeDatasetOp] ); - const handleSelectCapacityProvider = useCallback( - async (capacityProviderName: string) => { - const result = await loadCapacityProviderPreview(capacityProviderName); - if (result.ok) { - if (force) { - setFlow({ name: 'loading', message: `Removing capacity provider ${capacityProviderName}...` }); - const removeResult = await removeCapacityProviderOp(capacityProviderName, result.preview); - if (removeResult.success) { - setFlow({ name: 'capacity-provider-success', capacityProviderName }); - } else { - setFlow({ name: 'error', message: removeResult.error.message }); - } - } else { - setFlow({ name: 'confirm-capacity-provider', capacityProviderName, preview: result.preview }); - } - } else { - setFlow({ name: 'error', message: result.error }); - } - }, - [loadCapacityProviderPreview, force, removeCapacityProviderOp] - ); - const handleSelectKnowledgeBase = useCallback( async (knowledgeBaseName: string) => { const result = await loadKnowledgeBasePreview(knowledgeBaseName); @@ -833,9 +790,6 @@ export function RemoveFlow({ case 'payment-manager': void handleSelectPaymentManager(initialResourceName); break; - case 'capacity-provider': - void handleSelectCapacityProvider(initialResourceName); - break; } }, 0); }, [ @@ -855,7 +809,6 @@ export function RemoveFlow({ handleSelectConfigBundle, handleSelectRuntimeEndpoint, handleSelectPaymentManager, - handleSelectCapacityProvider, ]); // Confirm handlers - pass preview for logging @@ -987,26 +940,6 @@ export function RemoveFlow({ [removeDatasetOp] ); - const handleConfirmCapacityProvider = useCallback( - async (capacityProviderName: string, preview: RemovalPreview) => { - pendingResultRef.current = null; - setResultReady(false); - setFlow({ name: 'loading', message: `Removing capacity provider ${capacityProviderName}...` }); - const result = await removeCapacityProviderOp(capacityProviderName, preview); - if (result.success) { - pendingResultRef.current = { - name: 'capacity-provider-success', - capacityProviderName, - logFilePath: result.logFilePath, - }; - } else { - pendingResultRef.current = { name: 'error', message: result.error.message }; - } - setResultReady(true); - }, - [removeCapacityProviderOp] - ); - const handleConfirmKnowledgeBase = useCallback( async (knowledgeBaseName: string, preview: RemovalPreview) => { pendingResultRef.current = null; @@ -1123,7 +1056,6 @@ export function RemoveFlow({ resetRemovePolicy(); resetRemoveConfigBundle(); resetRemoveRuntimeEndpoint(); - resetRemoveCapacityProvider(); }, [ resetPreview, resetRemoveAgent, @@ -1140,7 +1072,6 @@ export function RemoveFlow({ resetRemovePolicy, resetRemoveConfigBundle, resetRemoveRuntimeEndpoint, - resetRemoveCapacityProvider, ]); const refreshAll = useCallback(async () => { @@ -1160,7 +1091,6 @@ export function RemoveFlow({ refreshConfigBundles(), refreshRuntimeEndpoints(), refreshPayments(), - refreshCapacityProviders(), ]); }, [ refreshAgents, @@ -1178,7 +1108,6 @@ export function RemoveFlow({ refreshConfigBundles, refreshRuntimeEndpoints, refreshPayments, - refreshCapacityProviders, ]); // Select screen - wait for data to load to avoid arrow position issues @@ -1205,7 +1134,6 @@ export function RemoveFlow({ datasetCount={datasets.length} knowledgeBaseCount={knowledgeBases.length} paymentCount={paymentManagers.length} - capacityProviderCount={capacityProviders.length} /> ); } @@ -1327,19 +1255,6 @@ export function RemoveFlow({ ); } - if (flow.name === 'select-capacity-provider') { - if (initialResourceName && isLoading) { - return null; - } - return ( - void handleSelectCapacityProvider(name)} - onExit={() => setFlow({ name: 'select' })} - /> - ); - } - if (flow.name === 'select-knowledge-base') { if (initialResourceName && isLoading) { return null; @@ -1565,17 +1480,6 @@ export function RemoveFlow({ ); } - if (flow.name === 'confirm-capacity-provider') { - return ( - void handleConfirmCapacityProvider(flow.capacityProviderName, flow.preview)} - onCancel={() => setFlow({ name: 'select-capacity-provider' })} - /> - ); - } - if (flow.name === 'confirm-knowledge-base') { return ( { - resetAll(); - void refreshAll().then(() => setFlow({ name: 'select' })); - }} - onExit={onExit} - /> - ); - } - if (flow.name === 'knowledge-base-success') { return ( { return REMOVE_RESOURCES.map(r => { @@ -199,12 +194,6 @@ export function RemoveScreen({ description = 'No payment managers to remove'; } break; - case 'capacity-provider': - if (capacityProviderCount === 0) { - disabled = true; - description = 'No capacity providers to remove'; - } - break; case 'all': // 'all' is always available break; @@ -228,7 +217,6 @@ export function RemoveScreen({ datasetCount, knowledgeBaseCount, paymentCount, - capacityProviderCount, ]); const isDisabled = (item: SelectableItem) => item.disabled ?? false; diff --git a/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx b/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx index 0db65700b..a849ab45e 100644 --- a/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx +++ b/src/cli/tui/screens/remove/__tests__/RemoveScreen.test.tsx @@ -27,7 +27,6 @@ describe('RemoveScreen', () => { datasetCount={0} knowledgeBaseCount={0} paymentCount={1} - capacityProviderCount={0} /> ); @@ -64,7 +63,6 @@ describe('RemoveScreen', () => { datasetCount={0} knowledgeBaseCount={0} paymentCount={0} - capacityProviderCount={0} /> ); @@ -97,7 +95,6 @@ describe('RemoveScreen', () => { datasetCount={0} knowledgeBaseCount={3} paymentCount={0} - capacityProviderCount={0} /> ); @@ -128,65 +125,9 @@ describe('RemoveScreen', () => { datasetCount={0} knowledgeBaseCount={0} paymentCount={0} - capacityProviderCount={0} /> ); expect(lastFrame()).toContain('No knowledge bases to remove'); }); - - it('Capacity Provider option enabled when capacityProviderCount > 0', () => { - const { lastFrame } = render( - - ); - - expect(lastFrame()).toContain('Capacity Provider'); - expect(lastFrame()).not.toContain('No capacity providers to remove'); - }); - - it('Capacity Provider option disabled when capacityProviderCount = 0', () => { - const { lastFrame } = render( - - ); - - expect(lastFrame()).toContain('No capacity providers to remove'); - }); }); diff --git a/src/cli/tui/screens/remove/useRemoveFlow.ts b/src/cli/tui/screens/remove/useRemoveFlow.ts index 393040db4..11ecb5774 100644 --- a/src/cli/tui/screens/remove/useRemoveFlow.ts +++ b/src/cli/tui/screens/remove/useRemoveFlow.ts @@ -96,11 +96,6 @@ export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowS items.push(`${totalConnectors} payment connector${totalConnectors > 1 ? 's' : ''}`); } } - if (projectSpec.capacityProviders && projectSpec.capacityProviders.length > 0) { - items.push( - `${projectSpec.capacityProviders.length} capacity provider${projectSpec.capacityProviders.length > 1 ? 's' : ''}` - ); - } } catch { // Project exists but has issues - still allow reset items.push('AgentCore project (corrupted or incomplete)'); diff --git a/src/schema/llm-compacted/agentcore.ts b/src/schema/llm-compacted/agentcore.ts index 0d259a9c0..c2915ea8d 100644 --- a/src/schema/llm-compacted/agentcore.ts +++ b/src/schema/llm-compacted/agentcore.ts @@ -26,7 +26,6 @@ interface AgentCoreProjectSpec { abTests: ABTest[]; // default [], unique by name harnesses: HarnessRegistryEntry[]; // default [], unique by name datasets?: Dataset[]; // unique by name - capacityProviders?: CapacityProvider[]; // unique by name payments?: PaymentManager[]; // unique by name } @@ -582,57 +581,6 @@ interface Dataset { kmsKeyArn?: string; } -// CAPACITY PROVIDER - -interface CapacityProvider { - name: string; // @regex ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ @min 1 @max 48; immutable after creation - description?: string; // @min 1 @max 4096; mutable (the only mutable field besides tags) - operatorRoleArn: string; // IAM role ARN @regex ^arn:aws(-[^:]+)?:iam::([0-9]{12})?:role/.+$ @max 2048; immutable - computeConfiguration: ComputeConfiguration; // immutable after creation - tags?: Tags; -} - -interface ComputeConfiguration { - ec2Configuration: Ec2Configuration; -} - -interface Ec2Configuration { - launchTemplateSource: { launchParameters: LaunchParameters }; - vpcConfiguration: CapacityProviderVpcConfiguration; - volumes?: VolumeConfiguration[]; // @max 5 - lifecycleConfiguration?: InstanceLifecycleConfiguration; - // long tail (rootVolume, etc.) accepted via passthrough and validated by CFN -} - -interface LaunchParameters { - operatingSystem: 'LINUX_X86_64' | 'LINUX_ARM64'; - instanceRequirements: { allowedInstanceTypes: string[] }; // @min 1 @max 30; each @min 1 @max 255 - instanceProfileArn?: string; // @regex ^arn:aws(-[^:]+)?:iam::[0-9]{12}:instance-profile/.+$ - // long tail (sshKeyName, monitoring, licenseSpecifications, capacityReservationSpecification, - // ephemeralVolumes, propagatedTags) accepted via passthrough and validated by CFN -} - -interface CapacityProviderVpcConfiguration { - subnets: string[]; // @min 1 @max 16; each @regex ^subnet-[0-9a-zA-Z]{8,17}$ - securityGroups: string[]; // @min 1 @max 16; each @regex ^sg-[0-9a-zA-Z]{8,17}$ -} - -interface VolumeConfiguration { - ebsConfiguration: { - name: string; // @regex ^[a-zA-Z][a-zA-Z0-9_-]{0,47}$ @min 1 @max 48 - sizeGiB: number; // integer @min 1 @max 65536 - volumeType?: 'standard' | 'io1' | 'io2' | 'gp2' | 'sc1' | 'st1' | 'gp3'; - encrypted?: boolean; - kmsKeyId?: string; // KMS key ARN - // long tail (iops, throughput, snapshotId) accepted via passthrough - }; -} - -interface InstanceLifecycleConfiguration { - idleInstanceTimeout?: number; // integer seconds @min 60 @max 1209600 - maxLifetime?: number; // integer seconds @min 60 @max 1209600 -} - // PAYMENTS type PaymentProvider = 'CoinbaseCDP' | 'StripePrivy'; diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 26539b063..c1eaa46fd 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -10,7 +10,6 @@ import { isReservedProjectName } from '../constants'; import { AgentEnvSpecSchema } from './agent-env'; import { AgentCoreGatewaySchema, AgentCoreGatewayTargetSchema, AgentCoreMcpRuntimeToolSchema } from './mcp'; import { ABTestSchema } from './primitives/ab-test'; -import { CapacityProviderSchema } from './primitives/capacity-provider'; import { ConfigBundleSchema } from './primitives/config-bundle'; import { DatasetSchema } from './primitives/dataset'; import { @@ -92,15 +91,6 @@ export type { Tags } from './primitives/tags'; export { DatasetSchema }; export { DatasetNameSchema, DatasetSchemaTypeSchema } from './primitives/dataset'; export type { Dataset, DatasetSchemaType } from './primitives/dataset'; -export { CapacityProviderSchema }; -export { - CapacityProviderNameSchema, - CAPACITY_PROVIDER_OPERATOR_ROLE_ARN_PATTERN, - isValidOperatorRoleArn, - OperatingSystemSchema, - OperatorRoleArnSchema, -} from './primitives/capacity-provider'; -export type { CapacityProvider, OperatingSystem } from './primitives/capacity-provider'; export type { ABTestMode, TargetRef, GatewayFilter, PerVariantOnlineEvaluationConfig } from './primitives/ab-test'; export { ABTestModeSchema, TargetRefSchema, GatewayFilterSchema } from './primitives/ab-test'; export type { @@ -552,17 +542,6 @@ export const AgentCoreProjectSpecSchema = z } }), - capacityProviders: z - .array(CapacityProviderSchema) - .optional() - .superRefine((items, ctx) => { - if (!items) return; - uniqueBy( - (cp: { name: string }) => cp.name, - (name: string) => `Duplicate capacity provider name: ${name}` - )(items, ctx); - }), - httpGateways: z .array(z.unknown()) .max( diff --git a/src/schema/schemas/deployed-state.ts b/src/schema/schemas/deployed-state.ts index 74b6a9e63..49d369da2 100644 --- a/src/schema/schemas/deployed-state.ts +++ b/src/schema/schemas/deployed-state.ts @@ -314,17 +314,6 @@ export const PaymentDeployedStateSchema = z.object({ export type PaymentDeployedState = z.infer; -// ============================================================================ -// Capacity Provider Deployed State -// ============================================================================ - -export const CapacityProviderDeployedStateSchema = z.object({ - capacityProviderId: z.string().min(1), - capacityProviderArn: z.string().min(1), -}); - -export type CapacityProviderDeployedState = z.infer; - // ============================================================================ // Deployed Resource State // ============================================================================ @@ -347,7 +336,6 @@ export const DeployedResourceStateSchema = z.object({ harnesses: z.record(z.string(), HarnessDeployedStateSchema).optional(), runtimeEndpoints: z.record(z.string(), RuntimeEndpointDeployedStateSchema).optional(), payments: z.record(z.string(), PaymentDeployedStateSchema).optional(), - capacityProviders: z.record(z.string(), CapacityProviderDeployedStateSchema).optional(), stackName: z.string().optional(), identityKmsKeyArn: z.string().optional(), deployHash: z.string().optional(), diff --git a/src/schema/schemas/primitives/__tests__/capacity-provider.test.ts b/src/schema/schemas/primitives/__tests__/capacity-provider.test.ts deleted file mode 100644 index 951d48939..000000000 --- a/src/schema/schemas/primitives/__tests__/capacity-provider.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { CapacityProviderSchema, isValidOperatorRoleArn } from '../capacity-provider'; -import { describe, expect, it } from 'vitest'; - -const validCp = { - name: 'myCp', - operatorRoleArn: 'arn:aws:iam::123456789012:role/MyOperatorRole', - computeConfiguration: { - ec2Configuration: { - launchTemplateSource: { - launchParameters: { - operatingSystem: 'LINUX_X86_64', - instanceRequirements: { allowedInstanceTypes: ['c6a.large'] }, - }, - }, - vpcConfiguration: { subnets: ['subnet-0123456789abcdef0'], securityGroups: ['sg-0123456789abcdef0'] }, - }, - }, -}; - -describe('CapacityProviderSchema', () => { - it('accepts a minimal valid capacity provider', () => { - expect(CapacityProviderSchema.safeParse(validCp).success).toBe(true); - }); - - it('rejects a name that does not start with a letter', () => { - const result = CapacityProviderSchema.safeParse({ ...validCp, name: '1bad' }); - expect(result.success).toBe(false); - }); - - it('rejects a description over 4096 characters', () => { - const result = CapacityProviderSchema.safeParse({ ...validCp, description: 'x'.repeat(4097) }); - expect(result.success).toBe(false); - }); - - it('accepts a 4096-character description', () => { - const result = CapacityProviderSchema.safeParse({ ...validCp, description: 'x'.repeat(4096) }); - expect(result.success).toBe(true); - }); - - it('only accepts the two Linux operating systems', () => { - for (const os of ['LINUX_X86_64', 'LINUX_ARM64']) { - const cp = structuredClone(validCp); - cp.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters.operatingSystem = os; - expect(CapacityProviderSchema.safeParse(cp).success).toBe(true); - } - for (const os of ['MAC_ARM64', 'WINDOWS_X86_64']) { - const cp = structuredClone(validCp); - cp.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters.operatingSystem = os; - expect(CapacityProviderSchema.safeParse(cp).success).toBe(false); - } - }); - - it('requires 1-30 instance types', () => { - const empty = structuredClone(validCp); - empty.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters.instanceRequirements.allowedInstanceTypes = - []; - expect(CapacityProviderSchema.safeParse(empty).success).toBe(false); - }); - - it('rejects malformed subnet and security group IDs', () => { - const badSubnet = structuredClone(validCp); - badSubnet.computeConfiguration.ec2Configuration.vpcConfiguration.subnets = ['not-a-subnet']; - expect(CapacityProviderSchema.safeParse(badSubnet).success).toBe(false); - }); - - it('accepts up to 5 volumes but rejects 6', () => { - const mkVol = (i: number) => ({ ebsConfiguration: { name: `vol${i}`, sizeGiB: 10 } }); - const five = structuredClone(validCp) as Record & typeof validCp; - (five.computeConfiguration.ec2Configuration as Record).volumes = [0, 1, 2, 3, 4].map(mkVol); - expect(CapacityProviderSchema.safeParse(five).success).toBe(true); - - const six = structuredClone(validCp) as Record & typeof validCp; - (six.computeConfiguration.ec2Configuration as Record).volumes = [0, 1, 2, 3, 4, 5].map(mkVol); - expect(CapacityProviderSchema.safeParse(six).success).toBe(false); - }); - - it('passes through unknown launch parameters (long tail)', () => { - const cp = structuredClone(validCp) as Record & typeof validCp; - ( - cp.computeConfiguration.ec2Configuration.launchTemplateSource.launchParameters as Record - ).sshKeyName = 'my-key'; - const result = CapacityProviderSchema.safeParse(cp); - expect(result.success).toBe(true); - }); -}); - -describe('isValidOperatorRoleArn', () => { - it('accepts standard and account-less role ARNs', () => { - expect(isValidOperatorRoleArn('arn:aws:iam::123456789012:role/MyRole')).toBe(true); - expect(isValidOperatorRoleArn('arn:aws:iam:::role/MyRole')).toBe(true); - expect(isValidOperatorRoleArn('arn:aws-us-gov:iam::123456789012:role/MyRole')).toBe(true); - }); - - it('rejects non-role and malformed ARNs', () => { - expect(isValidOperatorRoleArn('arn:aws:iam::123456789012:user/Bob')).toBe(false); - expect(isValidOperatorRoleArn('not-an-arn')).toBe(false); - }); -}); diff --git a/src/schema/schemas/primitives/capacity-provider.ts b/src/schema/schemas/primitives/capacity-provider.ts deleted file mode 100644 index 0dbd434c4..000000000 --- a/src/schema/schemas/primitives/capacity-provider.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { TagsSchema } from './tags'; -import { z } from 'zod'; - -// ============================================================================ -// Capacity Provider Types -// -// Models the AWS::BedrockAgentCore::CapacityProvider CFN resource. Known fields -// are typed and validated here so bad input is rejected at `add` time instead -// of failing late at deploy/CFN time; the long tail of launch parameters is -// accepted via `.passthrough()` and validated by CFN on deploy. -// -// NOTE: This schema is duplicated in @aws/agentcore-cdk -// (src/schema/schemas/primitives/capacity-provider.ts). Keep the two in sync. -// ============================================================================ - -/** - * Capacity provider name validation. - * Pattern: ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ (matches the CFN Name property). - */ -export const CapacityProviderNameSchema = z - .string() - .min(1, 'Capacity provider name is required') - .max(48) - .regex( - /^[a-zA-Z][a-zA-Z0-9_]{0,47}$/, - 'Must begin with a letter and contain only alphanumeric characters and underscores (max 48 chars)' - ); - -// ============================================================================ -// Operator Role ARN Validation -// ============================================================================ - -/** - * Pattern for the capacity provider operator role ARN, matching the CFN - * resource contract exactly. The account segment is OPTIONAL — the service - * accepts role ARNs without an account id, so we must not force 12 digits. - */ -// eslint-disable-next-line security/detect-unsafe-regex -- anchored ARN pattern, no backtracking risk -export const CAPACITY_PROVIDER_OPERATOR_ROLE_ARN_PATTERN = /^arn:[^:]+:iam::([0-9]{12})?:role\/.+$/; - -export const OperatorRoleArnSchema = z - .string() - .min(1, 'Operator role ARN is required') - .max(2048) - .regex( - CAPACITY_PROVIDER_OPERATOR_ROLE_ARN_PATTERN, - 'Must be a valid IAM role ARN (e.g. arn::iam::123456789012:role/MyOperatorRole)' - ); - -export function isValidOperatorRoleArn(value: string): boolean { - return CAPACITY_PROVIDER_OPERATOR_ROLE_ARN_PATTERN.test(value); -} - -// ============================================================================ -// Operating System -// -// The CFN resource enum lists four values (LINUX_X86_64, LINUX_ARM64, -// MAC_ARM64, WINDOWS_X86_64), but the service API contract only supports the -// two Linux values today; the extra two leaked into the CFN autogen ahead of -// real support. The CLI follows the API and exposes only the Linux values. -// ============================================================================ - -export const OperatingSystemSchema = z.enum(['LINUX_X86_64', 'LINUX_ARM64']); -export type OperatingSystem = z.infer; - -// ============================================================================ -// VPC Configuration -// ============================================================================ - -export const VpcConfigurationSchema = z.object({ - subnets: z - .array(z.string().regex(/^subnet-[0-9a-zA-Z]{8,17}$/, 'Must be a valid subnet ID')) - .min(1, 'At least one subnet is required') - .max(16), - securityGroups: z - .array(z.string().regex(/^sg-[0-9a-zA-Z]{8,17}$/, 'Must be a valid security group ID')) - .min(1, 'At least one security group is required') - .max(16), -}); - -export type VpcConfiguration = z.infer; - -// ============================================================================ -// Instance Requirements -// ============================================================================ - -export const InstanceRequirementsSchema = z.object({ - allowedInstanceTypes: z.array(z.string().min(1).max(255)).min(1, 'At least one instance type is required').max(30), -}); - -export type InstanceRequirements = z.infer; - -// ============================================================================ -// Launch Parameters -// -// Known fields are typed; the long tail (sshKeyName, monitoring, -// licenseSpecifications, capacityReservationSpecification, ephemeralVolumes, -// propagatedTags) is accepted via `.passthrough()` and validated by CFN. -// ============================================================================ - -export const LaunchParametersSchema = z - .object({ - operatingSystem: OperatingSystemSchema, - instanceRequirements: InstanceRequirementsSchema, - instanceProfileArn: z - .string() - .regex(/^arn:[^:]+:iam::[0-9]{12}:instance-profile\/.+$/, 'Must be a valid IAM instance profile ARN') - .optional(), - }) - .passthrough(); - -export type LaunchParameters = z.infer; - -// ============================================================================ -// EBS Volume Configuration -// -// Known fields typed; EBS long-tail tuning (iops, throughput, snapshotId) is -// accepted via `.passthrough()`. -// ============================================================================ - -export const EbsVolumeConfigurationSchema = z - .object({ - name: z - .string() - .min(1) - .max(48) - .regex( - /^[a-zA-Z][a-zA-Z0-9_-]{0,47}$/, - 'Volume name must begin with a letter and contain only alphanumerics, underscores, and hyphens (max 48 chars)' - ), - sizeGiB: z.number().int().min(1).max(65536), - volumeType: z.enum(['standard', 'io1', 'io2', 'gp2', 'sc1', 'st1', 'gp3']).optional(), - encrypted: z.boolean().optional(), - kmsKeyId: z - .string() - .regex( - /^arn:[^:]+:kms:[a-z0-9-]+:[0-9]{12}:key\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/, - 'Must be a valid KMS key ARN' - ) - .optional(), - }) - .passthrough(); - -export type EbsVolumeConfiguration = z.infer; - -export const VolumeConfigurationSchema = z.object({ - ebsConfiguration: EbsVolumeConfigurationSchema, -}); - -export type VolumeConfiguration = z.infer; - -// ============================================================================ -// Instance Lifecycle Configuration -// ============================================================================ - -const LIFECYCLE_SECONDS_MIN = 60; -const LIFECYCLE_SECONDS_MAX = 1209600; - -export const InstanceLifecycleConfigurationSchema = z.object({ - idleInstanceTimeout: z.number().int().min(LIFECYCLE_SECONDS_MIN).max(LIFECYCLE_SECONDS_MAX).optional(), - maxLifetime: z.number().int().min(LIFECYCLE_SECONDS_MIN).max(LIFECYCLE_SECONDS_MAX).optional(), -}); - -export type InstanceLifecycleConfiguration = z.infer; - -// ============================================================================ -// Compute Configuration -// -// `rootVolume` is accepted via `.passthrough()` on ec2Configuration (long-tail, -// service-managed). -// ============================================================================ - -export const Ec2ConfigurationSchema = z - .object({ - launchTemplateSource: z.object({ - launchParameters: LaunchParametersSchema, - }), - vpcConfiguration: VpcConfigurationSchema, - volumes: z.array(VolumeConfigurationSchema).max(5).optional(), - lifecycleConfiguration: InstanceLifecycleConfigurationSchema.optional(), - }) - .passthrough(); - -export type Ec2Configuration = z.infer; - -export const ComputeConfigurationSchema = z.object({ - ec2Configuration: Ec2ConfigurationSchema, -}); - -export type ComputeConfiguration = z.infer; - -// ============================================================================ -// Capacity Provider Schema -// ============================================================================ - -export const CapacityProviderSchema = z.object({ - /** Capacity provider name (immutable after creation). */ - name: CapacityProviderNameSchema, - /** Optional description (max 4096 chars). The only mutable field besides tags. */ - description: z.string().min(1).max(4096).optional(), - /** - * ARN of the IAM role AgentCore assumes to manage the capacity provider (immutable). Optional: - * when omitted, an operator role with the required trust policy and managed permissions is - * created automatically at deploy time. - */ - operatorRoleArn: OperatorRoleArnSchema.optional(), - /** Compute resources for the capacity provider (immutable after creation). */ - computeConfiguration: ComputeConfigurationSchema, - /** Optional resource tags. */ - tags: TagsSchema.optional(), -}); - -export type CapacityProvider = z.infer; diff --git a/src/schema/schemas/primitives/index.ts b/src/schema/schemas/primitives/index.ts index 3be2a5543..962989e54 100644 --- a/src/schema/schemas/primitives/index.ts +++ b/src/schema/schemas/primitives/index.ts @@ -9,35 +9,6 @@ export type { export type { Dataset, DatasetSchemaType } from './dataset'; export { DatasetNameSchema, DatasetSchema, DatasetSchemaTypeSchema } from './dataset'; - -export type { - CapacityProvider, - ComputeConfiguration, - Ec2Configuration, - EbsVolumeConfiguration, - InstanceLifecycleConfiguration, - InstanceRequirements, - LaunchParameters, - OperatingSystem, - VolumeConfiguration, - VpcConfiguration, -} from './capacity-provider'; -export { - CAPACITY_PROVIDER_OPERATOR_ROLE_ARN_PATTERN, - CapacityProviderNameSchema, - CapacityProviderSchema, - ComputeConfigurationSchema, - Ec2ConfigurationSchema, - EbsVolumeConfigurationSchema, - InstanceLifecycleConfigurationSchema, - InstanceRequirementsSchema, - isValidOperatorRoleArn, - LaunchParametersSchema, - OperatingSystemSchema, - OperatorRoleArnSchema, - VolumeConfigurationSchema, - VpcConfigurationSchema, -} from './capacity-provider'; export { ABTestNameSchema, ABTestDescriptionSchema,