Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/cli/commands/add/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
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';
Expand Down Expand Up @@ -115,6 +115,14 @@
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);
Expand Down Expand Up @@ -729,7 +737,7 @@
if (!passthroughEndpoint) {
return { valid: false, error: '--passthrough-endpoint is required for passthrough type' };
}
if (!/^https:\/\/[a-zA-Z0-9\-.]+(:[0-9]{1,5})?(\/.*)?$/.test(passthroughEndpoint)) {

Check warning on line 740 in src/cli/commands/add/validate.ts

View workflow job for this annotation

GitHub Actions / lint

Unsafe Regular Expression
return { valid: false, error: '--passthrough-endpoint must be a valid HTTPS URL' };
}
if (options.language && options.language !== 'Other') {
Expand Down
3 changes: 3 additions & 0 deletions src/cli/commands/shared/arn-utils.ts
Original file line number Diff line number Diff line change
@@ -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).
*/
Expand Down
16 changes: 16 additions & 0 deletions src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/cli/operations/agent/generate/schema-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 && {
Expand Down
8 changes: 8 additions & 0 deletions src/cli/primitives/AgentPrimitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface AddAgentOptions extends VpcOptions {
customClaims?: CustomClaimValidation[];
clientId?: string;
clientSecret?: string;
executionRoleArn?: string;
idleTimeout?: number;
maxLifetime?: number;
sessionStorageMountPath?: string;
Expand Down Expand Up @@ -274,6 +275,10 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
.option('--network-mode <mode>', 'Network mode (PUBLIC, VPC) [non-interactive]')
.option('--subnets <ids>', 'Comma-separated subnet IDs (required for VPC mode) [non-interactive]')
.option('--security-groups <ids>', 'Comma-separated security group IDs (required for VPC mode) [non-interactive]')
.option(
'--execution-role-arn <arn>',
'ARN of an existing IAM execution role to use instead of creating a CDK-managed one [non-interactive]'
)
.option('--authorizer-type <type>', 'Inbound auth: AWS_IAM or CUSTOM_JWT [non-interactive]')
.option('--discovery-url <url>', 'OIDC discovery URL (for CUSTOM_JWT) [non-interactive]')
.option('--allowed-audience <audience>', 'Comma-separated allowed audiences (for CUSTOM_JWT) [non-interactive]')
Expand Down Expand Up @@ -435,6 +440,7 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
customClaims,
clientId: cliOptions.clientId,
clientSecret: cliOptions.clientSecret,
executionRoleArn: cliOptions.executionRoleArn,
idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined,
maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined,
sessionStorageMountPath: cliOptions.sessionStorageMountPath,
Expand Down Expand Up @@ -558,6 +564,7 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
},
}),
requestHeaderAllowlist: options.requestHeaderAllowlist,
executionRoleArn: options.executionRoleArn,
idleRuntimeSessionTimeout: options.idleTimeout,
maxLifetime: options.maxLifetime,
sessionStorageMountPath: options.sessionStorageMountPath,
Expand Down Expand Up @@ -726,6 +733,7 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
...(options.requestHeaderAllowlist?.length && {
requestHeaderAllowlist: options.requestHeaderAllowlist,
}),
...(options.executionRoleArn && { executionRoleArn: options.executionRoleArn }),
...(authorizerType && { authorizerType }),
...(authorizerConfiguration && { authorizerConfiguration }),
...(lifecycleConfiguration && { lifecycleConfiguration }),
Expand Down
2 changes: 2 additions & 0 deletions src/cli/tui/screens/generate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export interface GenerateConfig {
securityGroups?: string[];
/** Allowed request headers for the runtime */
requestHeaderAllowlist?: string[];
/** ARN of an existing IAM execution role to use instead of creating a CDK-managed one */
executionRoleArn?: string;
/** Authorizer type for inbound requests */
authorizerType?: RuntimeAuthorizerType;
/** JWT config for CUSTOM_JWT authorizer */
Expand Down
Loading