From 3a221ee1652ca736153b6b78943a37c9e2b40def Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 25 Jun 2026 06:12:42 +0000 Subject: [PATCH] feat(cli): add --execution-role-arn flag to add agent (#870) CLI flag enhancement only: add `--execution-role-arn` to `agentcore add agent` and wire it into the create + BYO non-interactive add paths. No deploy-path or CDK changes (field + wiring already correct at pinned CDK tag). Import path intentionally untouched (imported agents derive their role). The unknown-key silent-strip hardening was left out of scope. Refs aws/agentcore-cli#870 --- src/cli/commands/add/types.ts | 1 + src/cli/commands/add/validate.ts | 10 +++++++++- src/cli/commands/shared/arn-utils.ts | 3 +++ .../generate/__tests__/schema-mapper.test.ts | 16 ++++++++++++++++ .../operations/agent/generate/schema-mapper.ts | 1 + src/cli/primitives/AgentPrimitive.tsx | 8 ++++++++ src/cli/tui/screens/generate/types.ts | 2 ++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index 465c4d717..d258ce66b 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -34,6 +34,7 @@ export interface AddAgentOptions extends VpcOptions { customClaims?: string; clientId?: string; clientSecret?: string; + executionRoleArn?: string; requestHeaderAllowlist?: string; idleTimeout?: number | string; maxLifetime?: number | string; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 6246c2106..1d17447bd 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -24,7 +24,7 @@ import { matchEnumValue, validateApiFormat, } from '../../../schema'; -import { ARN_VALIDATION_MESSAGE, isValidArn } from '../shared/arn-utils'; +import { ARN_VALIDATION_MESSAGE, IAM_ROLE_ARN_REGEX, isValidArn } from '../shared/arn-utils'; import { validateHeaderAllowlist } from '../shared/header-utils'; import { MAX_INDEXED_KEYS, parseIndexedKeyArg } from '../shared/indexed-key-parser'; import { parseAndValidateLifecycleOptions } from '../shared/lifecycle-utils'; @@ -115,6 +115,14 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes return { valid: false, error: nameResult.error.issues[0]?.message ?? 'Invalid agent name' }; } + if (options.executionRoleArn && !IAM_ROLE_ARN_REGEX.test(options.executionRoleArn)) { + return { + valid: false, + error: + '--execution-role-arn must be a valid IAM role ARN (e.g. arn:aws:iam::123456789012:role/MyRole)', + }; + } + // Validate build type if provided if (options.build) { const buildResult = BuildTypeSchema.safeParse(options.build); diff --git a/src/cli/commands/shared/arn-utils.ts b/src/cli/commands/shared/arn-utils.ts index 8ff30ac06..d195d39bd 100644 --- a/src/cli/commands/shared/arn-utils.ts +++ b/src/cli/commands/shared/arn-utils.ts @@ -1,6 +1,9 @@ const ARN_PART_COUNT = 6; const ARN_FORMAT = 'arn:partition:service:region:account:resource'; +/** Matches an IAM role ARN across any partition (commercial, GovCloud, China). */ +export const IAM_ROLE_ARN_REGEX = /^arn:[^:]+:iam::\d{12}:role\/.+/; + /** * Check whether a string looks like a valid ARN (starts with `arn:` and has at least 6 colon-separated parts). */ diff --git a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts index a96db26a9..67c8df1ce 100644 --- a/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts +++ b/src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts @@ -1,3 +1,4 @@ +import { AgentEnvSpecSchema } from '../../../../../schema/index.js'; import { computeManagedOAuthCredentialName } from '../../../../primitives/credential-utils.js'; import { mapByoConfigToAgent } from '../../../../tui/screens/agent/useAddAgent.js'; import type { GenerateConfig } from '../../../../tui/screens/generate/types.js'; @@ -342,6 +343,21 @@ describe('mapGenerateConfigToAgent - requestHeaderAllowlist', () => { }); }); +describe('mapGenerateConfigToAgent - executionRoleArn', () => { + const ROLE_ARN = 'arn:aws:iam::123456789012:role/MyRole'; + + it('includes executionRoleArn when provided and round-trips through AgentEnvSpecSchema', () => { + const result = mapGenerateConfigToAgent({ ...baseConfig, executionRoleArn: ROLE_ARN }); + expect(result.executionRoleArn).toBe(ROLE_ARN); + expect(AgentEnvSpecSchema.parse(result).executionRoleArn).toBe(ROLE_ARN); + }); + + it('omits executionRoleArn when undefined', () => { + const result = mapGenerateConfigToAgent(baseConfig); + expect(result.executionRoleArn).toBeUndefined(); + }); +}); + describe('mapByoConfigToAgent - VPC support', () => { const baseByoConfig = { name: 'MyByo', diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index e9584e248..53bafca75 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -140,6 +140,7 @@ export function mapGenerateConfigToAgent(config: GenerateConfig): AgentEnvSpec { ...(config.requestHeaderAllowlist?.length && { requestHeaderAllowlist: config.requestHeaderAllowlist, }), + ...(config.executionRoleArn && { executionRoleArn: config.executionRoleArn }), ...(config.authorizerType && { authorizerType: config.authorizerType }), ...(config.authorizerType === 'CUSTOM_JWT' && config.jwtConfig && { diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 80ffa0fbf..2cf97b84f 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -109,6 +109,7 @@ export interface AddAgentOptions extends VpcOptions { customClaims?: CustomClaimValidation[]; clientId?: string; clientSecret?: string; + executionRoleArn?: string; idleTimeout?: number; maxLifetime?: number; sessionStorageMountPath?: string; @@ -274,6 +275,10 @@ export class AgentPrimitive extends BasePrimitive', 'Network mode (PUBLIC, VPC) [non-interactive]') .option('--subnets ', 'Comma-separated subnet IDs (required for VPC mode) [non-interactive]') .option('--security-groups ', 'Comma-separated security group IDs (required for VPC mode) [non-interactive]') + .option( + '--execution-role-arn ', + 'ARN of an existing IAM execution role to use instead of creating a CDK-managed one [non-interactive]' + ) .option('--authorizer-type ', 'Inbound auth: AWS_IAM or CUSTOM_JWT [non-interactive]') .option('--discovery-url ', 'OIDC discovery URL (for CUSTOM_JWT) [non-interactive]') .option('--allowed-audience ', 'Comma-separated allowed audiences (for CUSTOM_JWT) [non-interactive]') @@ -435,6 +440,7 @@ export class AgentPrimitive extends BasePrimitive