From 3ee2708be411b57cafc1f2ca266b5733eff4ceb6 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Thu, 25 Jun 2026 18:54:56 +0000 Subject: [PATCH 1/2] feat(config-bundle): add --kms-key for customer-managed encryption Surface CMK support for configuration bundles: a --kms-key flag and an optional wizard step set kmsKeyArn in agentcore.json, validated by the shared KmsKeyArnSchema and passed through to the create API and the L3 ConfigurationBundle resource (KmsKeyArn). Requires the matching change in @aws/agentcore-cdk. --- integ-tests/add-remove-config-bundle.test.ts | 31 ++- schemas/agentcore.schema.v1.json | 188 ++++++++++++++++++ .../agentcore-config-bundles.test.ts | 36 +++- src/cli/aws/agentcore-config-bundles.ts | 2 + src/cli/primitives/ConfigBundlePrimitive.ts | 5 + src/cli/tui/hooks/useCreateConfigBundle.ts | 2 + .../config-bundle/AddConfigBundleFlow.tsx | 1 + .../config-bundle/AddConfigBundleScreen.tsx | 17 +- .../useAddConfigBundleWizard.test.tsx | 40 ++++ src/cli/tui/screens/config-bundle/types.ts | 4 + .../config-bundle/useAddConfigBundleWizard.ts | 8 + .../schemas/primitives/config-bundle.ts | 3 + 12 files changed, 334 insertions(+), 3 deletions(-) diff --git a/integ-tests/add-remove-config-bundle.test.ts b/integ-tests/add-remove-config-bundle.test.ts index c6c37c257..d7752e44e 100644 --- a/integ-tests/add-remove-config-bundle.test.ts +++ b/integ-tests/add-remove-config-bundle.test.ts @@ -77,12 +77,13 @@ describe('integration: add and remove config-bundle', () => { expect(Object.keys(bundle!.components)).toHaveLength(2); }); - it('adds a config bundle with optional description, branch, and commit message', async () => { + it('adds a config bundle with optional description, branch, commit message, and KMS key', async () => { const components = JSON.stringify({ '{{runtime:MyAgent}}': { configuration: { systemPrompt: 'Placeholder-based bundle' }, }, }); + const kmsKeyArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'; const json = await runSuccess( [ @@ -98,6 +99,8 @@ describe('integration: add and remove config-bundle', () => { 'feature-branch', '--commit-message', 'initial config', + '--kms-key', + kmsKeyArn, '--json', ], project.projectPath @@ -111,6 +114,7 @@ describe('integration: add and remove config-bundle', () => { expect(bundle!.description).toBe('A bundle with all optional fields'); expect(bundle!.branchName).toBe('feature-branch'); expect(bundle!.commitMessage).toBe('initial config'); + expect(bundle!.kmsKeyArn).toBe(kmsKeyArn); }); it('adds a config bundle with placeholder component keys', async () => { @@ -214,6 +218,31 @@ describe('integration: add and remove config-bundle', () => { expect(json.error).toBeDefined(); }); + it('rejects an invalid KMS key ARN', async () => { + const components = JSON.stringify({ + 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/rt-kms': { + configuration: { foo: 'bar' }, + }, + }); + + const json = await runFailure( + [ + 'add', + 'config-bundle', + '--name', + 'BadKmsBundle', + '--components', + components, + '--kms-key', + 'not-a-valid-kms-arn', + '--json', + ], + project.projectPath + ); + + expect(json.error).toBeDefined(); + }); + it('rejects bundle name starting with a number', async () => { const components = JSON.stringify({ 'arn:test': { configuration: {} }, diff --git a/schemas/agentcore.schema.v1.json b/schemas/agentcore.schema.v1.json index dedc5b64e..01f21bd64 100644 --- a/schemas/agentcore.schema.v1.json +++ b/schemas/agentcore.schema.v1.json @@ -154,6 +154,13 @@ "executionRoleArn": { "type": "string" }, + "additionalPolicies": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, "authorizerType": { "type": "string", "enum": ["AWS_IAM", "CUSTOM_JWT"] @@ -536,6 +543,183 @@ "required": ["version"], "additionalProperties": false } + }, + "connections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_-]{0,63}$" + }, + "to": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "memory" + }, + "arn": { + "type": "string", + "pattern": "^arn:[^:]+:bedrock-agentcore:[a-z0-9-]+:\\d{12}:memory\\/.+$" + }, + "namespaces": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["type", "arn"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "gateway" + }, + "arn": { + "type": "string", + "pattern": "^arn:[^:]+:bedrock-agentcore:[a-z0-9-]+:\\d{12}:gateway\\/.+$" + }, + "outboundAuth": { + "anyOf": [ + { + "type": "object", + "properties": { + "awsIam": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "required": ["awsIam"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "none": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + "required": ["none"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "oauth": { + "type": "object", + "properties": { + "providerArn": { + "type": "string", + "minLength": 1 + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "grantType": { + "type": "string", + "enum": ["CLIENT_CREDENTIALS", "AUTHORIZATION_CODE", "TOKEN_EXCHANGE"] + }, + "customParameters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["providerArn", "scopes"], + "additionalProperties": false + } + }, + "required": ["oauth"], + "additionalProperties": false + } + ] + } + }, + "required": ["type", "arn"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "runtime" + }, + "arn": { + "type": "string", + "pattern": "^arn:[^:]+:bedrock-agentcore:[a-z0-9-]+:\\d{12}:runtime\\/.+$" + }, + "exec": { + "type": "boolean" + } + }, + "required": ["type", "arn"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "browser" + }, + "arn": { + "type": "string", + "pattern": "^arn:[^:]+:bedrock-agentcore:[a-z0-9-]+:(\\d{12}|aws):browser(-custom)?\\/.+$" + } + }, + "required": ["type"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "codeInterpreter" + }, + "arn": { + "type": "string", + "pattern": "^arn:[^:]+:bedrock-agentcore:[a-z0-9-]+:(\\d{12}|aws):code-interpreter(-custom)?\\/.+$" + } + }, + "required": ["type"], + "additionalProperties": false + } + ] + }, + "access": { + "type": "string", + "enum": ["read", "readwrite"] + }, + "description": { + "type": "string", + "maxLength": 200 + } + }, + "required": ["to"], + "additionalProperties": false + } } }, "required": ["name", "build", "entrypoint", "codeLocation"], @@ -2635,6 +2819,10 @@ "commitMessage": { "type": "string", "maxLength": 500 + }, + "kmsKeyArn": { + "type": "string", + "pattern": "^arn:[^:]+:kms:[a-zA-Z0-9-]*:[0-9]{12}:key\\/[a-zA-Z0-9-]{36}$" } }, "required": ["name", "components"], diff --git a/src/cli/aws/__tests__/agentcore-config-bundles.test.ts b/src/cli/aws/__tests__/agentcore-config-bundles.test.ts index 30a776b13..e707d622f 100644 --- a/src/cli/aws/__tests__/agentcore-config-bundles.test.ts +++ b/src/cli/aws/__tests__/agentcore-config-bundles.test.ts @@ -1,4 +1,4 @@ -import { listConfigurationBundles } from '../agentcore-config-bundles.js'; +import { createConfigurationBundle, listConfigurationBundles } from '../agentcore-config-bundles.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mockFetch = vi.fn(); @@ -43,6 +43,40 @@ describe('agentcore-config-bundles', () => { vi.clearAllMocks(); }); + describe('createConfigurationBundle', () => { + const components = { + 'arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/myRuntime-abc': { + configuration: { systemPrompt: 'hi' }, + }, + }; + + function parseRequestBody(): Record { + const init = mockFetch.mock.calls[0]![1] as { body: string }; + return JSON.parse(init.body) as Record; + } + + it('includes kmsKeyArn in the request body when provided', async () => { + mockFetch.mockResolvedValue( + mockJsonResponse({ bundleArn: 'arn', bundleId: 'id', versionId: 'v1', createdAt: '2026-01-01' }) + ); + const kmsKeyArn = 'arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012'; + + await createConfigurationBundle({ region: 'us-west-2', bundleName: 'b', components, kmsKeyArn }); + + expect(parseRequestBody().kmsKeyArn).toBe(kmsKeyArn); + }); + + it('omits kmsKeyArn from the request body when not provided', async () => { + mockFetch.mockResolvedValue( + mockJsonResponse({ bundleArn: 'arn', bundleId: 'id', versionId: 'v1', createdAt: '2026-01-01' }) + ); + + await createConfigurationBundle({ region: 'us-west-2', bundleName: 'b', components }); + + expect(parseRequestBody()).not.toHaveProperty('kmsKeyArn'); + }); + }); + describe('listConfigurationBundles', () => { it('returns bundles with createdAt timestamp', async () => { mockFetch.mockResolvedValue( diff --git a/src/cli/aws/agentcore-config-bundles.ts b/src/cli/aws/agentcore-config-bundles.ts index e2941acfe..3da1a7e37 100644 --- a/src/cli/aws/agentcore-config-bundles.ts +++ b/src/cli/aws/agentcore-config-bundles.ts @@ -43,6 +43,7 @@ export interface CreateConfigurationBundleOptions { components: ComponentConfigurationMap; branchName?: string; commitMessage?: string; + kmsKeyArn?: string; createdBy?: { name: string; arn?: string }; } @@ -243,6 +244,7 @@ export async function createConfigurationBundle( components: options.components, ...(options.branchName && { branchName: options.branchName }), ...(options.commitMessage && { commitMessage: options.commitMessage }), + ...(options.kmsKeyArn && { kmsKeyArn: options.kmsKeyArn }), ...(options.createdBy && { createdBy: options.createdBy }), }); diff --git a/src/cli/primitives/ConfigBundlePrimitive.ts b/src/cli/primitives/ConfigBundlePrimitive.ts index 2e41a3432..ee4408a75 100644 --- a/src/cli/primitives/ConfigBundlePrimitive.ts +++ b/src/cli/primitives/ConfigBundlePrimitive.ts @@ -15,6 +15,7 @@ export interface AddConfigBundleOptions { components: Record }>; branchName?: string; commitMessage?: string; + kmsKeyArn?: string; } export type RemovableConfigBundle = RemovableResource; @@ -116,6 +117,7 @@ export class ConfigBundlePrimitive extends BasePrimitive', 'Path to components JSON file (same format as --components)') .option('--branch ', 'Branch name for versioning') .option('--commit-message ', 'Commit message for this version') + .option('--kms-key ', 'KMS key ARN for encrypting the configuration bundle') .option('--json', 'Output as JSON') .action( async (cliOptions: { @@ -125,6 +127,7 @@ export class ConfigBundlePrimitive extends BasePrimitive { try { @@ -168,6 +171,7 @@ export class ConfigBundlePrimitive extends BasePrimitive }>; branchName?: string; commitMessage?: string; + kmsKeyArn?: string; } export function useCreateConfigBundle() { @@ -23,6 +24,7 @@ export function useCreateConfigBundle() { components: config.components, branchName: config.branchName, commitMessage: config.commitMessage, + kmsKeyArn: config.kmsKeyArn, }); if (!addResult.success) { throw new Error(addResult.error?.message ?? 'Failed to create configuration bundle'); diff --git a/src/cli/tui/screens/config-bundle/AddConfigBundleFlow.tsx b/src/cli/tui/screens/config-bundle/AddConfigBundleFlow.tsx index 511265ece..b9067656e 100644 --- a/src/cli/tui/screens/config-bundle/AddConfigBundleFlow.tsx +++ b/src/cli/tui/screens/config-bundle/AddConfigBundleFlow.tsx @@ -110,6 +110,7 @@ export function AddConfigBundleFlow({ components: config.components, branchName: config.branchName || 'mainline', commitMessage: config.commitMessage || `Create ${config.name}`, + kmsKeyArn: config.kmsKeyArn || undefined, }).then(result => { if (result.ok) { setFlow(prev => { diff --git a/src/cli/tui/screens/config-bundle/AddConfigBundleScreen.tsx b/src/cli/tui/screens/config-bundle/AddConfigBundleScreen.tsx index aab3b555a..1b21d7a6f 100644 --- a/src/cli/tui/screens/config-bundle/AddConfigBundleScreen.tsx +++ b/src/cli/tui/screens/config-bundle/AddConfigBundleScreen.tsx @@ -1,4 +1,4 @@ -import { ConfigBundleNameSchema } from '../../../../schema'; +import { ConfigBundleNameSchema, isValidKmsKeyArn } from '../../../../schema'; import type { SelectableItem } from '../../components'; import { ConfirmReview, Panel, Screen, StepIndicator, TextInput, WizardSelect } from '../../components'; import { HELP_TEXT } from '../../constants'; @@ -81,6 +81,7 @@ export function AddConfigBundleScreen({ const isAddAnotherStep = wizard.step === 'addAnother'; const isBranchNameStep = wizard.step === 'branchName'; const isCommitMessageStep = wizard.step === 'commitMessage'; + const isKmsKeyStep = wizard.step === 'kmsKey'; const isConfirmStep = wizard.step === 'confirm'; const componentTypeNav = useListNavigation({ @@ -278,6 +279,19 @@ export function AddConfigBundleScreen({ /> )} + {isKmsKeyStep && ( + wizard.goBack()} + customValidation={value => value === '' || isValidKmsKeyArn(value) || 'Must be a valid KMS key ARN'} + /> + )} + {isConfirmStep && ( )} diff --git a/src/cli/tui/screens/config-bundle/__tests__/useAddConfigBundleWizard.test.tsx b/src/cli/tui/screens/config-bundle/__tests__/useAddConfigBundleWizard.test.tsx index 19854a95d..7a09c8075 100644 --- a/src/cli/tui/screens/config-bundle/__tests__/useAddConfigBundleWizard.test.tsx +++ b/src/cli/tui/screens/config-bundle/__tests__/useAddConfigBundleWizard.test.tsx @@ -91,6 +91,46 @@ describe('useAddConfigBundleWizard — add-another back-navigation (BUG TUI-B)', }); }); +describe('useAddConfigBundleWizard — KMS key step', () => { + const KMS_ARN = 'arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012'; + + function advanceToKmsKey(ref: React.RefObject) { + advanceToAddAnother(ref); + act(() => ref.current!.wizard.doneAddingComponents()); + act(() => ref.current!.wizard.setBranchName('mainline')); + act(() => ref.current!.wizard.setCommitMessage('msg')); + } + + it('commitMessage advances to the kmsKey step', () => { + const { ref } = setup(); + advanceToKmsKey(ref); + expect(ref.current!.wizard.step).toBe('kmsKey'); + }); + + it('setKmsKey stores the ARN and advances to confirm', () => { + const { ref } = setup(); + advanceToKmsKey(ref); + act(() => ref.current!.wizard.setKmsKey(KMS_ARN)); + expect(ref.current!.wizard.step).toBe('confirm'); + expect(ref.current!.wizard.config.kmsKeyArn).toBe(KMS_ARN); + }); + + it('skipping the kmsKey step (empty) leaves kmsKeyArn empty and advances to confirm', () => { + const { ref } = setup(); + advanceToKmsKey(ref); + act(() => ref.current!.wizard.setKmsKey('')); + expect(ref.current!.wizard.step).toBe('confirm'); + expect(ref.current!.wizard.config.kmsKeyArn).toBe(''); + }); + + it('goBack from kmsKey returns to commitMessage', () => { + const { ref } = setup(); + advanceToKmsKey(ref); + act(() => ref.current!.wizard.goBack()); + expect(ref.current!.wizard.step).toBe('commitMessage'); + }); +}); + describe('useAddConfigBundleWizard — custom ARN component (Part 1)', () => { /** Drive the wizard to the componentType step. */ function advanceToComponentType(ref: React.RefObject) { diff --git a/src/cli/tui/screens/config-bundle/types.ts b/src/cli/tui/screens/config-bundle/types.ts index aed0c56b9..8112fb0fa 100644 --- a/src/cli/tui/screens/config-bundle/types.ts +++ b/src/cli/tui/screens/config-bundle/types.ts @@ -14,6 +14,7 @@ export type AddConfigBundleStep = | 'addAnother' | 'branchName' | 'commitMessage' + | 'kmsKey' | 'confirm'; export type ComponentType = 'runtime' | 'gateway' | 'custom'; @@ -34,6 +35,8 @@ export interface AddConfigBundleConfig { componentsRaw: string; branchName: string; commitMessage: string; + /** Optional KMS key ARN for customer-managed encryption of the bundle. */ + kmsKeyArn: string; /** Currently selected component type in wizard. */ currentComponentType?: ComponentType; /** Currently selected component ARN in wizard. */ @@ -50,6 +53,7 @@ export const CONFIG_BUNDLE_STEP_LABELS: Record = { addAnother: 'More?', branchName: 'Branch', commitMessage: 'Message', + kmsKey: 'KMS Key', confirm: 'Confirm', }; diff --git a/src/cli/tui/screens/config-bundle/useAddConfigBundleWizard.ts b/src/cli/tui/screens/config-bundle/useAddConfigBundleWizard.ts index ecc8eb7f0..2b2533823 100644 --- a/src/cli/tui/screens/config-bundle/useAddConfigBundleWizard.ts +++ b/src/cli/tui/screens/config-bundle/useAddConfigBundleWizard.ts @@ -12,6 +12,7 @@ const ALL_STEPS: AddConfigBundleStep[] = [ 'addAnother', 'branchName', 'commitMessage', + 'kmsKey', 'confirm', ]; @@ -23,6 +24,7 @@ function getDefaultConfig(): AddConfigBundleConfig { componentsRaw: '', branchName: 'mainline', commitMessage: '', + kmsKeyArn: '', }; } @@ -112,6 +114,11 @@ export function useAddConfigBundleWizard() { const setCommitMessage = useCallback((commitMessage: string) => { setConfig(c => ({ ...c, commitMessage })); + setStep('kmsKey'); + }, []); + + const setKmsKey = useCallback((kmsKeyArn: string) => { + setConfig(c => ({ ...c, kmsKeyArn })); setStep('confirm'); }, []); @@ -137,6 +144,7 @@ export function useAddConfigBundleWizard() { doneAddingComponents, setBranchName, setCommitMessage, + setKmsKey, reset, }; } diff --git a/src/schema/schemas/primitives/config-bundle.ts b/src/schema/schemas/primitives/config-bundle.ts index 06702bd3c..ba5032482 100644 --- a/src/schema/schemas/primitives/config-bundle.ts +++ b/src/schema/schemas/primitives/config-bundle.ts @@ -1,3 +1,4 @@ +import { KmsKeyArnSchema } from './evaluator'; import { z } from 'zod'; // ============================================================================ @@ -44,6 +45,8 @@ export const ConfigBundleSchema = z.object({ branchName: z.string().max(128).optional(), /** Optional commit message for this version. */ commitMessage: z.string().max(500).optional(), + /** Optional KMS key ARN for customer-managed encryption of the bundle. */ + kmsKeyArn: KmsKeyArnSchema.optional(), }); export type ConfigBundle = z.infer; From 91b9afd9cca55dbb5a69a718bae48dede89cab22 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Thu, 25 Jun 2026 19:14:40 +0000 Subject: [PATCH 2/2] feat(config-bundle): validate --kms-key up front in add handler Match the evaluator pattern: fail fast with a clear --kms-key message in non-interactive mode before attempting the add, rather than only surfacing the error at schema-write time. --- integ-tests/add-remove-config-bundle.test.ts | 2 +- src/cli/primitives/ConfigBundlePrimitive.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/integ-tests/add-remove-config-bundle.test.ts b/integ-tests/add-remove-config-bundle.test.ts index d7752e44e..e8d207d5f 100644 --- a/integ-tests/add-remove-config-bundle.test.ts +++ b/integ-tests/add-remove-config-bundle.test.ts @@ -240,7 +240,7 @@ describe('integration: add and remove config-bundle', () => { project.projectPath ); - expect(json.error).toBeDefined(); + expect(json.error).toContain('--kms-key must be a valid KMS key ARN'); }); it('rejects bundle name starting with a number', async () => { diff --git a/src/cli/primitives/ConfigBundlePrimitive.ts b/src/cli/primitives/ConfigBundlePrimitive.ts index ee4408a75..fc3e328db 100644 --- a/src/cli/primitives/ConfigBundlePrimitive.ts +++ b/src/cli/primitives/ConfigBundlePrimitive.ts @@ -1,7 +1,7 @@ import { ResourceNotFoundError, findConfigRoot, serializeResult, toError } from '../../lib'; import type { Result } from '../../lib/result'; import type { ConfigBundle } from '../../schema'; -import { ConfigBundleSchema } from '../../schema'; +import { ConfigBundleSchema, isValidKmsKeyArn } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, SchemaChange } from '../operations/remove/types'; import { BasePrimitive } from './BasePrimitive'; @@ -154,6 +154,12 @@ export class ConfigBundlePrimitive extends BasePrimitive }>; if (cliOptions.componentsFile) { const raw = readFileSync(cliOptions.componentsFile, 'utf-8');