Skip to content
Closed
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
25 changes: 25 additions & 0 deletions src/cli/commands/shared/__tests__/header-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ describe('normalizeHeaderName', () => {
it('auto-prefixes suffix with hyphens like "My-Custom-Header"', () => {
expect(normalizeHeaderName('My-Custom-Header')).toBe('X-Amzn-Bedrock-AgentCore-Runtime-Custom-My-Custom-Header');
});

it('preserves a header already under the broader namespace prefix without re-prefixing', () => {
expect(normalizeHeaderName('X-Amzn-Bedrock-AgentCore-Runtime-User-Id')).toBe(
'X-Amzn-Bedrock-AgentCore-Runtime-User-Id'
);
expect(normalizeHeaderName('X-Amzn-Bedrock-AgentCore-Runtime-Session-Id')).toBe(
'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id'
);
});

it('canonicalizes the namespace prefix casing for non-Custom headers', () => {
expect(normalizeHeaderName('x-amzn-bedrock-agentcore-Runtime-User-Id')).toBe(
'X-Amzn-Bedrock-AgentCore-Runtime-User-Id'
);
});
});

describe('parseAndNormalizeHeaders', () => {
Expand Down Expand Up @@ -127,6 +142,16 @@ describe('validateHeaderAllowlist', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('Invalid header name');
});

it('accepts headers under the broader X-Amzn-Bedrock-AgentCore- namespace', () => {
expect(validateHeaderAllowlist('X-Amzn-Bedrock-AgentCore-Runtime-User-Id')).toEqual({ success: true });
expect(validateHeaderAllowlist('X-Amzn-Bedrock-AgentCore-Runtime-Session-Id')).toEqual({ success: true });
expect(
validateHeaderAllowlist(
'Authorization, X-Amzn-Bedrock-AgentCore-Runtime-User-Id, X-Amzn-Bedrock-AgentCore-Runtime-Custom-Foo'
)
).toEqual({ success: true });
});
});

describe('parseHeaderFlag', () => {
Expand Down
23 changes: 21 additions & 2 deletions src/cli/commands/shared/header-utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import {
HEADER_ALLOWLIST_NAMESPACE_PREFIX as HEADER_ALLOWLIST_NAMESPACE_PREFIX_FROM_SCHEMA,
HEADER_ALLOWLIST_PREFIX as HEADER_ALLOWLIST_PREFIX_FROM_SCHEMA,
MAX_HEADER_ALLOWLIST_SIZE as MAX_HEADER_ALLOWLIST_SIZE_FROM_SCHEMA,
isAllowedRequestHeader,
} from '../../../schema/schemas/agent-env';

export const HEADER_ALLOWLIST_PREFIX = HEADER_ALLOWLIST_PREFIX_FROM_SCHEMA;
export const HEADER_ALLOWLIST_NAMESPACE_PREFIX = HEADER_ALLOWLIST_NAMESPACE_PREFIX_FROM_SCHEMA;
export const MAX_HEADER_ALLOWLIST_SIZE = MAX_HEADER_ALLOWLIST_SIZE_FROM_SCHEMA;

const HEADER_NAME_PATTERN = /^[A-Za-z0-9-]+$/;

/**
* Normalize a header name according to AgentCore Runtime rules:
* - "Authorization" (case-insensitive) -> "Authorization"
* - Headers already starting with the prefix (case-insensitive) -> canonical prefix + original suffix
* - Other headers -> prepend the prefix
* - Headers already starting with the canonical custom prefix
* (case-insensitive) -> canonical prefix + original suffix
* - Headers already under the broader 'X-Amzn-Bedrock-AgentCore-' namespace
* (case-insensitive) -> canonical namespace prefix + original suffix
* (i.e. these are accepted as-is and NOT re-prefixed with the Custom- prefix)
* - Other (bare) header names -> prepend the Custom- prefix
*/
export function normalizeHeaderName(input: string): string {
if (input.toLowerCase() === 'authorization') {
Expand All @@ -21,6 +28,9 @@ export function normalizeHeaderName(input: string): string {
if (input.toLowerCase().startsWith(HEADER_ALLOWLIST_PREFIX.toLowerCase())) {
return `${HEADER_ALLOWLIST_PREFIX}${input.slice(HEADER_ALLOWLIST_PREFIX.length)}`;
}
if (input.toLowerCase().startsWith(HEADER_ALLOWLIST_NAMESPACE_PREFIX.toLowerCase())) {
return `${HEADER_ALLOWLIST_NAMESPACE_PREFIX}${input.slice(HEADER_ALLOWLIST_NAMESPACE_PREFIX.length)}`;
}
return `${HEADER_ALLOWLIST_PREFIX}${input}`;
}

Expand Down Expand Up @@ -69,6 +79,15 @@ export function validateHeaderAllowlist(value: string): { success: boolean; erro
};
}

for (const header of headers) {
if (!isAllowedRequestHeader(header)) {
return {
success: false,
error: `Invalid header "${header}". Must be "Authorization" or start with "${HEADER_ALLOWLIST_NAMESPACE_PREFIX}". See https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-header-allowlist.html`,
};
}
}

return { success: true };
}

Expand Down
2 changes: 1 addition & 1 deletion src/cli/primitives/AgentPrimitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export class AgentPrimitive extends BasePrimitive<AddAgentOptions, RemovableReso
.option('--client-secret <secret>', 'OAuth client secret [non-interactive]')
.option(
'--request-header-allowlist <headers>',
'Comma-separated list of custom header names to allow (auto-prefixed with X-Amzn-Bedrock-AgentCore-Runtime-Custom-) [non-interactive]'
'Comma-separated list of allowed request header names. Bare names are auto-prefixed with X-Amzn-Bedrock-AgentCore-Runtime-Custom-; "Authorization" and any header under the X-Amzn-Bedrock-AgentCore- namespace are also accepted. See https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-header-allowlist.html [non-interactive]'
)
.option(
'--idle-timeout <seconds>',
Expand Down
5 changes: 3 additions & 2 deletions src/cli/tui/screens/agent/AddAgentScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1181,8 +1181,9 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg
/>
<Box marginTop={1}>
<Text dimColor>
Enter header suffixes or full names. We auto-prefix with X-Amzn-Bedrock-AgentCore-Runtime-Custom- if
needed. &apos;Authorization&apos; is also accepted.
Enter header suffixes or full names. Bare names are auto-prefixed with
X-Amzn-Bedrock-AgentCore-Runtime-Custom-. &apos;Authorization&apos; and any header under the
X-Amzn-Bedrock-AgentCore- namespace are also accepted.
</Text>
</Box>
</Box>
Expand Down
61 changes: 61 additions & 0 deletions src/schema/schemas/__tests__/agent-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ import {
EnvVarNameSchema,
EnvVarSchema,
GatewayNameSchema,
HEADER_ALLOWLIST_NAMESPACE_PREFIX,
HEADER_ALLOWLIST_PREFIX,
InstrumentationSchema,
LifecycleConfigurationSchema,
MAX_HEADER_ALLOWLIST_SIZE,
NetworkConfigSchema,
REQUEST_HEADER_ALLOWLIST_PATTERN,
RequestHeaderAllowlistSchema,
RuntimeEndpointNameSchema,
RuntimeEndpointSchema,
isAllowedRequestHeader,
} from '../agent-env.js';
import { describe, expect, it } from 'vitest';

Expand Down Expand Up @@ -624,3 +630,58 @@ describe('AgentEnvSpecSchema - endpoints', () => {
}
});
});

describe('REQUEST_HEADER_ALLOWLIST_PATTERN / isAllowedRequestHeader', () => {
it('matches Authorization (case-insensitive)', () => {
expect(REQUEST_HEADER_ALLOWLIST_PATTERN.test('Authorization')).toBe(true);
expect(isAllowedRequestHeader('authorization')).toBe(true);
});

it('matches headers under the AgentCore namespace prefix', () => {
expect(isAllowedRequestHeader('X-Amzn-Bedrock-AgentCore-Runtime-Custom-Foo')).toBe(true);
expect(isAllowedRequestHeader('X-Amzn-Bedrock-AgentCore-Runtime-User-Id')).toBe(true);
expect(isAllowedRequestHeader('X-Amzn-Bedrock-AgentCore-Runtime-Session-Id')).toBe(true);
});

it('rejects non-matching names and the bare namespace prefix', () => {
expect(isAllowedRequestHeader('Foo')).toBe(false);
expect(isAllowedRequestHeader('X-Custom-Foo')).toBe(false);
expect(isAllowedRequestHeader(HEADER_ALLOWLIST_NAMESPACE_PREFIX)).toBe(false);
expect(isAllowedRequestHeader('')).toBe(false);
});
});

describe('RequestHeaderAllowlistSchema', () => {
it('accepts Authorization and Custom- prefixed headers', () => {
const result = RequestHeaderAllowlistSchema.safeParse(['Authorization', `${HEADER_ALLOWLIST_PREFIX}Foo`]);
expect(result.success).toBe(true);
});

it('accepts the broader namespace prefix', () => {
const result = RequestHeaderAllowlistSchema.safeParse([
'X-Amzn-Bedrock-AgentCore-Runtime-User-Id',
'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id',
]);
expect(result.success).toBe(true);
});

it('rejects bare names without the namespace prefix', () => {
const result = RequestHeaderAllowlistSchema.safeParse(['Foo-Bar']);
expect(result.success).toBe(false);
});

it(`rejects more than ${MAX_HEADER_ALLOWLIST_SIZE} headers`, () => {
const headers = Array.from({ length: MAX_HEADER_ALLOWLIST_SIZE + 1 }, (_, i) => `${HEADER_ALLOWLIST_PREFIX}H${i}`);
const result = RequestHeaderAllowlistSchema.safeParse(headers);
expect(result.success).toBe(false);
});

it('error message references the AWS doc URL', () => {
const result = RequestHeaderAllowlistSchema.safeParse(['Foo']);
expect(result.success).toBe(false);
if (!result.success) {
const allMessages = result.error.issues.map(i => i.message).join(' | ');
expect(allMessages).toContain('runtime-header-allowlist.html');
}
});
});
38 changes: 35 additions & 3 deletions src/schema/schemas/agent-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,51 @@ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;

/**
* Allowed request headers for the runtime.
* Each header must be 'Authorization' or start with 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-'.
*
* Per AWS Bedrock AgentCore runtime header allowlist documentation
* (https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-header-allowlist.html),
* each header must be either:
* - 'Authorization' (case-sensitive canonical form), or
* - any header beginning with the AgentCore runtime namespace prefix
* 'X-Amzn-Bedrock-AgentCore-' (case-insensitive), e.g.
* * 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-<Suffix>'
* * 'X-Amzn-Bedrock-AgentCore-Runtime-User-Id'
* * 'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id'
* * other documented headers under the same namespace
*
* Maximum 20 headers.
*
* NOTE: This file is duplicated in agentcore-l3-cdk-constructs/src/schema/schemas/agent-env.ts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Release coordination concern (not a code change request, but worth confirming before merge):

This schema is duplicated in agentcore-l3-cdk-constructs, and the generated CDK project consumes the L3 constructs' copy of RequestHeaderAllowlistSchema — not this one — at cdk synth/deploy time. If a CLI release containing this change ships before the companion L3 constructs release (branch fix/1151-6ed1535c) is merged and published to the registry, the flow breaks for users:

  1. User runs agentcore create / agentcore add with e.g. X-Amzn-Bedrock-AgentCore-Runtime-User-Id
  2. CLI validation passes (new behavior) and writes it into agent-env.yaml
  3. CDK synth uses the older L3 constructs schema and rejects it with the old error message

A few ways to de-risk this:

  • Confirm the companion L3 PR is merged and a version bump is released before the CLI release that carries this change, and (if applicable) bump the minimum @aws/agentcore-cdk version pinned in the vended CDK project templates / package.json so older constructs can't be resolved.
  • Or bundle the updated constructs via the bundled-agentcore-cdk.tgz path (src/cli/templates/CDKRenderer.ts) in the same release.
  • Or land + release the L3 constructs change first, then merge this PR.

Could you confirm which path you're taking? Happy to approve once the ordering/versioning is spelled out.

* Keep both copies in sync.
*/
export const HEADER_ALLOWLIST_PREFIX = 'X-Amzn-Bedrock-AgentCore-Runtime-Custom-';
export const HEADER_ALLOWLIST_NAMESPACE_PREFIX = 'X-Amzn-Bedrock-AgentCore-';
export const MAX_HEADER_ALLOWLIST_SIZE = 20;

/**
* Pattern matching any header name that is an acceptable entry in the
* request header allowlist. Matches case-insensitively for the namespace
* prefix to accommodate user-supplied casing variations; the runtime
* itself is HTTP-header-case-insensitive.
*/
export const REQUEST_HEADER_ALLOWLIST_PATTERN = /^(Authorization|X-Amzn-Bedrock-AgentCore-[A-Za-z0-9-]+)$/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor behavior-change check: the /i flag makes this accept authorization / AUTHORIZATION as well as the canonical Authorization. The previous refine used strict val === 'Authorization', so a YAML containing requestHeaderAllowlist: [authorization] went from rejected → accepted.

This is fine for the CLI entry points that flow through normalizeHeaderName (which canonicalizes casing), but the schema is also applied directly to hand-edited agent-env.yaml values, which aren't renormalized before being passed to the CreateAgentRuntime API. If the service is strict about the canonical Authorization casing in the allowlist array (header names on the wire are case-insensitive, but allowlist entries may not be), users who put lowercase in YAML would get an API-side failure instead of an up-front schema failure.

Could you confirm the service accepts any casing of Authorization in the allowlist? If not, consider dropping the /i or splitting the pattern so Authorization is matched case-sensitively and only the namespace portion is case-insensitive, e.g. /^Authorization$|^X-Amzn-Bedrock-AgentCore-[A-Za-z0-9-]+$/i won't help (/i flag is global) — something like two separate checks or new RegExp without the flag for the first alternative.


/**
* Returns true when the given header name is allowed in the runtime
* request header allowlist (Authorization or any header under the
* AgentCore runtime namespace prefix).
*/
export function isAllowedRequestHeader(name: string): boolean {
return REQUEST_HEADER_ALLOWLIST_PATTERN.test(name);
}

export const RequestHeaderAllowlistSchema = z
.array(
z
.string()
.refine(
val => val === 'Authorization' || val.startsWith(HEADER_ALLOWLIST_PREFIX),
`Must be "Authorization" or start with "${HEADER_ALLOWLIST_PREFIX}"`
isAllowedRequestHeader,
`Must be "Authorization" or start with "${HEADER_ALLOWLIST_NAMESPACE_PREFIX}". See https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-header-allowlist.html`
)
)
.max(MAX_HEADER_ALLOWLIST_SIZE, `Maximum ${MAX_HEADER_ALLOWLIST_SIZE} headers allowed`);
Expand Down
Loading