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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion integ-tests/add-remove-config-bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand All @@ -98,6 +99,8 @@ describe('integration: add and remove config-bundle', () => {
'feature-branch',
'--commit-message',
'initial config',
'--kms-key',
kmsKeyArn,
'--json',
],
project.projectPath
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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).toContain('--kms-key must be a valid KMS key ARN');
});

it('rejects bundle name starting with a number', async () => {
const components = JSON.stringify({
'arn:test': { configuration: {} },
Expand Down
188 changes: 188 additions & 0 deletions schemas/agentcore.schema.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@
"executionRoleArn": {
"type": "string"
},
"additionalPolicies": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"authorizerType": {
"type": "string",
"enum": ["AWS_IAM", "CUSTOM_JWT"]
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
36 changes: 35 additions & 1 deletion src/cli/aws/__tests__/agentcore-config-bundles.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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<string, unknown> {
const init = mockFetch.mock.calls[0]![1] as { body: string };
return JSON.parse(init.body) as Record<string, unknown>;
}

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(
Expand Down
2 changes: 2 additions & 0 deletions src/cli/aws/agentcore-config-bundles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface CreateConfigurationBundleOptions {
components: ComponentConfigurationMap;
branchName?: string;
commitMessage?: string;
kmsKeyArn?: string;
createdBy?: { name: string; arn?: string };
}

Expand Down Expand Up @@ -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 }),
});

Expand Down
13 changes: 12 additions & 1 deletion src/cli/primitives/ConfigBundlePrimitive.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,6 +15,7 @@ export interface AddConfigBundleOptions {
components: Record<string, { configuration: Record<string, unknown> }>;
branchName?: string;
commitMessage?: string;
kmsKeyArn?: string;
}

export type RemovableConfigBundle = RemovableResource;
Expand Down Expand Up @@ -116,6 +117,7 @@ export class ConfigBundlePrimitive extends BasePrimitive<AddConfigBundleOptions,
.option('--components-file <path>', 'Path to components JSON file (same format as --components)')
.option('--branch <name>', 'Branch name for versioning')
.option('--commit-message <text>', 'Commit message for this version')
.option('--kms-key <arn>', 'KMS key ARN for encrypting the configuration bundle')
.option('--json', 'Output as JSON')
.action(
async (cliOptions: {
Expand All @@ -125,6 +127,7 @@ export class ConfigBundlePrimitive extends BasePrimitive<AddConfigBundleOptions,
componentsFile?: string;
branch?: string;
commitMessage?: string;
kmsKey?: string;
json?: boolean;
}) => {
try {
Expand All @@ -151,6 +154,12 @@ export class ConfigBundlePrimitive extends BasePrimitive<AddConfigBundleOptions,
fail('Either --components or --components-file is required');
}

if (cliOptions.kmsKey && !isValidKmsKeyArn(cliOptions.kmsKey)) {
fail(
'--kms-key must be a valid KMS key ARN (e.g. arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012)'
);
}

let components: Record<string, { configuration: Record<string, unknown> }>;
if (cliOptions.componentsFile) {
const raw = readFileSync(cliOptions.componentsFile, 'utf-8');
Expand All @@ -168,6 +177,7 @@ export class ConfigBundlePrimitive extends BasePrimitive<AddConfigBundleOptions,
components,
branchName: cliOptions.branch,
commitMessage: cliOptions.commitMessage,
kmsKeyArn: cliOptions.kmsKey,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent up-front validation + missing telemetry on this handler.

(1) EvaluatorPrimitive.ts:298 validates kmsKeyArn with isValidKmsKeyArn right before calling this.add(...) and fails with a clear, flag-named message. Here, an invalid ARN falls through to writeProjectSpec's Zod validation, producing a generic schema error. The integ test (integ-tests/add-remove-config-bundle.test.ts:243) only asserts json.error is defined, so the worse message wouldn't be caught. Either:

  • Mirror EvaluatorPrimitive and add an explicit isValidKmsKeyArn check just before this this.add({...}) call that calls fail('--kms-key must be a valid KMS key ARN ...').
  • Or keep relying on the Zod schema, but tighten the integ test to assert the actual error message users see.

(2) Missing telemetry instrumentation. Per src/cli/telemetry/README.md, new features should be instrumented. add.config-bundle isn't in COMMAND_SCHEMAS (src/cli/telemetry/schemas/command-run.ts) at all today, and this PR is a natural opportunity to add it since we now have an interesting attribute to track (CMK adoption). Suggested:

// command-run.ts
const AddConfigBundleAttrs = safeSchema({
  has_kms_key: z.boolean(),
  component_count: Count,
});
// ...
'add.config-bundle': AddConfigBundleAttrs,

Then wrap this non-interactive handler in runCliCommand('add.config-bundle', !!cliOptions.json, async () => { ...; return { has_kms_key: !!cliOptions.kmsKey, component_count: Object.keys(components).length }; }).

});

if (cliOptions.json) {
Expand Down Expand Up @@ -227,6 +237,7 @@ export class ConfigBundlePrimitive extends BasePrimitive<AddConfigBundleOptions,
components: options.components,
branchName: options.branchName ?? 'mainline',
...(options.commitMessage && { commitMessage: options.commitMessage }),
...(options.kmsKeyArn && { kmsKeyArn: options.kmsKeyArn }),
};

project.configBundles ??= [];
Expand Down
Loading
Loading