@aws/nx-plugin 1.0.0-rc.59 → 1.0.0-rc.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations.json +17 -2
- package/package.json +1 -1
- package/src/agentcore-gateway/agent-connection/generator.js +7 -2
- package/src/agentcore-gateway/agent-connection/generator.js.map +1 -1
- package/src/migrations/latest/gateway-cognito-passthrough/__snapshots__/migration.spec.ts.snap +149 -0
- package/src/migrations/latest/gateway-cognito-passthrough/metadata.json +3 -0
- package/src/migrations/latest/gateway-cognito-passthrough/migration.d.ts +6 -0
- package/src/migrations/latest/gateway-cognito-passthrough/migration.js +195 -0
- package/src/migrations/latest/gateway-cognito-passthrough/migration.js.map +1 -0
- package/src/migrations/latest/strip-pip-from-python-images/__snapshots__/migration.spec.ts.snap +48 -0
- package/src/migrations/latest/strip-pip-from-python-images/metadata.json +3 -0
- package/src/migrations/latest/strip-pip-from-python-images/migration.d.ts +6 -0
- package/src/migrations/latest/strip-pip-from-python-images/migration.js +91 -0
- package/src/migrations/latest/strip-pip-from-python-images/migration.js.map +1 -0
- package/src/migrations/latest/waf-log-group-removal/__snapshots__/migration.spec.ts.snap +57 -0
- package/src/migrations/latest/waf-log-group-removal/metadata.json +3 -0
- package/src/migrations/latest/waf-log-group-removal/migration.d.ts +6 -0
- package/src/migrations/latest/waf-log-group-removal/migration.js +77 -0
- package/src/migrations/latest/waf-log-group-removal/migration.js.map +1 -0
- package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +9 -0
- package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +8 -0
- package/src/py/agent/files/deploy/Dockerfile.template +7 -0
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +4 -1
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +8 -0
- package/src/py/mcp-server/files/deploy/Dockerfile.template +7 -0
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +4 -1
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +12 -0
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +8 -0
- package/src/utils/agent-core-constructs/files/cdk/app/agent-core/__nameKebabCase__/__nameKebabCase__.ts.template +6 -0
- package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +14 -3
- package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +8 -0
- package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +4 -1
package/migrations.json
CHANGED
|
@@ -7,11 +7,26 @@
|
|
|
7
7
|
"description": "Add agent runtime target support to the vended AgentCoreGateway construct and an agentName member to vended agent constructs",
|
|
8
8
|
"implementation": "./src/migrations/latest/gateway-agent-targets/migration"
|
|
9
9
|
},
|
|
10
|
+
"latest-gateway-cognito-passthrough": {
|
|
11
|
+
"version": "1.0.0-rc.60",
|
|
12
|
+
"description": "Front Cognito agents with an AgentCore Gateway via JWT passthrough: addAgent picks the target credential from the agent's auth, agent constructs gain an auth member, and Cognito runtimes allowlist the Authorization header",
|
|
13
|
+
"implementation": "./src/migrations/latest/gateway-cognito-passthrough/migration"
|
|
14
|
+
},
|
|
10
15
|
"latest-smithy-ssdk-bundle-pins": {
|
|
11
16
|
"version": "1.0.0-rc.58",
|
|
12
17
|
"description": "Bump the rolldown and dts plugin pins in the Smithy build.Dockerfile so the bundled SSDK type declaration resolves",
|
|
13
18
|
"implementation": "./src/migrations/latest/smithy-ssdk-bundle-pins/migration"
|
|
14
19
|
},
|
|
20
|
+
"latest-strip-pip-from-python-images": {
|
|
21
|
+
"version": "1.0.0-rc.60",
|
|
22
|
+
"description": "Remove pip from vended Python agent / MCP server images so its vulnerable vendored packages are not present at runtime",
|
|
23
|
+
"implementation": "./src/migrations/latest/strip-pip-from-python-images/migration"
|
|
24
|
+
},
|
|
25
|
+
"latest-waf-log-group-removal": {
|
|
26
|
+
"version": "1.0.0-rc.60",
|
|
27
|
+
"description": "Set RemovalPolicy.DESTROY on WAF log groups in the vended AgentCoreGateway and REST API constructs so a rolled-back deploy does not orphan them",
|
|
28
|
+
"implementation": "./src/migrations/latest/waf-log-group-removal/migration"
|
|
29
|
+
},
|
|
15
30
|
"v1.0.0-rc.50-modernize-function-props-cast": {
|
|
16
31
|
"version": "1.0.0-rc.50",
|
|
17
32
|
"description": "Replace the legacy angle-bracket FunctionProps type assertion with the modern as syntax in generated API constructs",
|
|
@@ -53,14 +68,14 @@
|
|
|
53
68
|
"implementation": "./src/migrations/v1.0.0-rc.57/ts-agent-session-management-support/migration"
|
|
54
69
|
},
|
|
55
70
|
"sync-vended-versions": {
|
|
56
|
-
"version": "1.0.0-rc.
|
|
71
|
+
"version": "1.0.0-rc.60",
|
|
57
72
|
"description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
|
|
58
73
|
"implementation": "./src/utils/version-upgrade-migration/migration"
|
|
59
74
|
}
|
|
60
75
|
},
|
|
61
76
|
"packageJsonUpdates": {
|
|
62
77
|
"nx-23.1.1-nx-packages": {
|
|
63
|
-
"version": "1.0.0-rc.
|
|
78
|
+
"version": "1.0.0-rc.60",
|
|
64
79
|
"packages": {
|
|
65
80
|
"nx": {
|
|
66
81
|
"version": "23.1.1",
|
package/package.json
CHANGED
|
@@ -55,8 +55,13 @@ export const AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO = getGeneratorInf
|
|
|
55
55
|
if (!supportedProtocols.includes(agentProtocol)) {
|
|
56
56
|
throw new Error(`Agent '${agentComponent.name}' uses protocol='${agentProtocol}', which cannot be fronted by a gateway` + (agentGenerator === 'ts#agent' && agentProtocol === 'http' ? ` — a TypeScript HTTP agent serves tRPC over WebSocket, and AgentCore Gateway does not support WebSocket streaming. Consider the ag-ui protocol instead.` : `. Supported protocols: ${supportedProtocols.join(', ')}.`));
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Both IAM and Cognito agents can be fronted. An IAM agent is invoked with
|
|
59
|
+
// the gateway's own role (GATEWAY_IAM_ROLE); a Cognito agent has the caller's
|
|
60
|
+
// JWT forwarded to it (JWT_PASSTHROUGH) so it authorizes on the token. The
|
|
61
|
+
// vended `addAgent` picks the credential from the agent's `auth`.
|
|
62
|
+
const agentAuth = agentComponent.auth ?? 'iam';
|
|
63
|
+
if (agentAuth !== 'iam' && agentAuth !== 'cognito') {
|
|
64
|
+
throw new Error(`Agent '${agentComponent.name}' uses auth='${agentAuth}', which a gateway cannot front. Supported: iam, cognito.`);
|
|
60
65
|
}
|
|
61
66
|
// The target name must match what the deployed Gateway uses (`agentName` on
|
|
62
67
|
// the agent construct, derived from the project's class name) so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/agentcore-gateway/agent-connection/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { GeneratorCallback, Tree } from '@nx/devkit';\nimport { formatFilesInSubtree } from '../../utils/format';\nimport { installDependencies } from '../../utils/install';\nimport { addGeneratorMetricsIfApplicable } from '../../utils/metrics';\nimport { kebabCase, toClassName } from '../../utils/names';\nimport {\n addComponentGeneratorMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../utils/nx';\nimport { attachAgentToLocalGateway } from '../attach-upstream';\nimport { readAgentCoreGatewayMetadata } from '../generator';\nimport type { AgentcoreGatewayAgentConnectionGeneratorSchema } from './schema';\n\nexport const AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\n/**\n * The agent protocols an http gateway can front, by agent language. The\n * gateway proxies plain HTTP request/response and SSE streams:\n *\n * - `ag-ui` (ts + py) and `http` (py) serve POST /invocations with streamed\n * responses, which proxy cleanly.\n * - `a2a` (ts + py) is JSON-RPC over POST, which AgentCore applies a default\n * schema for.\n * - `http` (ts) is tRPC over WebSocket, and the gateway does not support\n * WebSocket / bidirectional streaming, so it cannot sit behind one.\n */\nconst SUPPORTED_AGENT_PROTOCOLS: Record<string, string[]> = {\n 'ts#agent': ['ag-ui', 'a2a'],\n 'py#agent': ['ag-ui', 'http', 'a2a'],\n};\n\n/**\n * Connect an AgentCore Gateway to an agent, so the agent is added as a\n * gateway target and served under `<gatewayUrl>/<targetName>/invocations`.\n *\n * Chains the gateway's dev target to the agent's dev target, and registers\n * the agent in the gateway's local-dev.ts so the local gateway proxies to it.\n * Users must still call `gateway.addAgent(...)` in their application stack to\n * create the actual CDK/Terraform resource.\n */\nexport const agentcoreGatewayAgentConnectionGenerator = async (\n tree: Tree,\n options: AgentcoreGatewayAgentConnectionGeneratorSchema,\n): Promise<GeneratorCallback> => {\n const sourceProject = readProjectConfigurationUnqualified(\n tree,\n options.sourceProject,\n );\n const targetProject = readProjectConfigurationUnqualified(\n tree,\n options.targetProject,\n );\n\n const gateway = readAgentCoreGatewayMetadata(sourceProject);\n const agentComponent = options.targetComponent;\n\n if (!agentComponent) {\n throw new Error(\n `Target project '${options.targetProject}' has no agent component metadata. Did you run the 'ts#agent' or 'py#agent' generator?`,\n );\n }\n\n if (gateway.protocol !== 'http') {\n throw new Error(\n `Gateway '${gateway.name}' has protocol='${gateway.protocol}'. Agent targets can only be attached to http-protocol gateways — generate one with the 'agentcore-gateway' generator and --protocol=http.`,\n );\n }\n\n const agentGenerator = agentComponent.generator ?? 'ts#agent';\n const agentProtocol = (\n (agentComponent.protocol as string | undefined) ?? 'http'\n ).toLowerCase();\n const supportedProtocols = SUPPORTED_AGENT_PROTOCOLS[agentGenerator] ?? [];\n if (!supportedProtocols.includes(agentProtocol)) {\n throw new Error(\n `Agent '${agentComponent.name}' uses protocol='${agentProtocol}', which cannot be fronted by a gateway` +\n (agentGenerator === 'ts#agent' && agentProtocol === 'http'\n ? ` — a TypeScript HTTP agent serves tRPC over WebSocket, and AgentCore Gateway does not support WebSocket streaming. Consider the ag-ui protocol instead.`\n : `. Supported protocols: ${supportedProtocols.join(', ')}.`),\n );\n }\n
|
|
1
|
+
{"version":3,"sources":["../../../../../../packages/nx-plugin/src/agentcore-gateway/agent-connection/generator.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { GeneratorCallback, Tree } from '@nx/devkit';\nimport { formatFilesInSubtree } from '../../utils/format';\nimport { installDependencies } from '../../utils/install';\nimport { addGeneratorMetricsIfApplicable } from '../../utils/metrics';\nimport { kebabCase, toClassName } from '../../utils/names';\nimport {\n addComponentGeneratorMetadata,\n getGeneratorInfo,\n type NxGeneratorInfo,\n readProjectConfigurationUnqualified,\n} from '../../utils/nx';\nimport { attachAgentToLocalGateway } from '../attach-upstream';\nimport { readAgentCoreGatewayMetadata } from '../generator';\nimport type { AgentcoreGatewayAgentConnectionGeneratorSchema } from './schema';\n\nexport const AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO: NxGeneratorInfo =\n getGeneratorInfo(import.meta.filename);\n\n/**\n * The agent protocols an http gateway can front, by agent language. The\n * gateway proxies plain HTTP request/response and SSE streams:\n *\n * - `ag-ui` (ts + py) and `http` (py) serve POST /invocations with streamed\n * responses, which proxy cleanly.\n * - `a2a` (ts + py) is JSON-RPC over POST, which AgentCore applies a default\n * schema for.\n * - `http` (ts) is tRPC over WebSocket, and the gateway does not support\n * WebSocket / bidirectional streaming, so it cannot sit behind one.\n */\nconst SUPPORTED_AGENT_PROTOCOLS: Record<string, string[]> = {\n 'ts#agent': ['ag-ui', 'a2a'],\n 'py#agent': ['ag-ui', 'http', 'a2a'],\n};\n\n/**\n * Connect an AgentCore Gateway to an agent, so the agent is added as a\n * gateway target and served under `<gatewayUrl>/<targetName>/invocations`.\n *\n * Chains the gateway's dev target to the agent's dev target, and registers\n * the agent in the gateway's local-dev.ts so the local gateway proxies to it.\n * Users must still call `gateway.addAgent(...)` in their application stack to\n * create the actual CDK/Terraform resource.\n */\nexport const agentcoreGatewayAgentConnectionGenerator = async (\n tree: Tree,\n options: AgentcoreGatewayAgentConnectionGeneratorSchema,\n): Promise<GeneratorCallback> => {\n const sourceProject = readProjectConfigurationUnqualified(\n tree,\n options.sourceProject,\n );\n const targetProject = readProjectConfigurationUnqualified(\n tree,\n options.targetProject,\n );\n\n const gateway = readAgentCoreGatewayMetadata(sourceProject);\n const agentComponent = options.targetComponent;\n\n if (!agentComponent) {\n throw new Error(\n `Target project '${options.targetProject}' has no agent component metadata. Did you run the 'ts#agent' or 'py#agent' generator?`,\n );\n }\n\n if (gateway.protocol !== 'http') {\n throw new Error(\n `Gateway '${gateway.name}' has protocol='${gateway.protocol}'. Agent targets can only be attached to http-protocol gateways — generate one with the 'agentcore-gateway' generator and --protocol=http.`,\n );\n }\n\n const agentGenerator = agentComponent.generator ?? 'ts#agent';\n const agentProtocol = (\n (agentComponent.protocol as string | undefined) ?? 'http'\n ).toLowerCase();\n const supportedProtocols = SUPPORTED_AGENT_PROTOCOLS[agentGenerator] ?? [];\n if (!supportedProtocols.includes(agentProtocol)) {\n throw new Error(\n `Agent '${agentComponent.name}' uses protocol='${agentProtocol}', which cannot be fronted by a gateway` +\n (agentGenerator === 'ts#agent' && agentProtocol === 'http'\n ? ` — a TypeScript HTTP agent serves tRPC over WebSocket, and AgentCore Gateway does not support WebSocket streaming. Consider the ag-ui protocol instead.`\n : `. Supported protocols: ${supportedProtocols.join(', ')}.`),\n );\n }\n // Both IAM and Cognito agents can be fronted. An IAM agent is invoked with\n // the gateway's own role (GATEWAY_IAM_ROLE); a Cognito agent has the caller's\n // JWT forwarded to it (JWT_PASSTHROUGH) so it authorizes on the token. The\n // vended `addAgent` picks the credential from the agent's `auth`.\n const agentAuth = (agentComponent.auth as string | undefined) ?? 'iam';\n if (agentAuth !== 'iam' && agentAuth !== 'cognito') {\n throw new Error(\n `Agent '${agentComponent.name}' uses auth='${agentAuth}', which a gateway cannot front. Supported: iam, cognito.`,\n );\n }\n\n // The target name must match what the deployed Gateway uses (`agentName` on\n // the agent construct, derived from the project's class name) so the\n // `/<target>/invocations` path resolves identically locally and deployed.\n const agentComponentName = agentComponent.name ?? 'agent';\n const agentTargetName = kebabCase(\n (agentComponent.rc as string | undefined) ??\n toClassName(agentComponentName),\n );\n\n await attachAgentToLocalGateway(tree, sourceProject, 'dev', {\n targetName: agentTargetName,\n port: (agentComponent.port as number | undefined) ?? 8081,\n upstreamProjectName: targetProject.name,\n upstreamDevTargetName: `${agentComponentName}-dev`,\n // The deployed gateway maps an A2A target's `/invocations/...` paths onto\n // the agent container's root; HTTP and AG-UI agents serve /invocations\n // themselves.\n stripInvocations: agentProtocol === 'a2a',\n });\n\n // Recorded so the version sync can identify this connection.\n addComponentGeneratorMetadata(\n tree,\n sourceProject.name,\n AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO,\n targetProject.root,\n agentTargetName,\n );\n\n await addGeneratorMetricsIfApplicable(tree, [\n AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO,\n ]);\n\n await formatFilesInSubtree(tree);\n\n return () =>\n installDependencies(tree, options.preferInstallDependencies, {\n languages: ['typescript'],\n });\n};\n\nexport default agentcoreGatewayAgentConnectionGenerator;\n"],"names":["formatFilesInSubtree","installDependencies","addGeneratorMetricsIfApplicable","kebabCase","toClassName","addComponentGeneratorMetadata","getGeneratorInfo","readProjectConfigurationUnqualified","attachAgentToLocalGateway","readAgentCoreGatewayMetadata","AGENTCORE_GATEWAY_AGENT_CONNECTION_GENERATOR_INFO","filename","SUPPORTED_AGENT_PROTOCOLS","agentcoreGatewayAgentConnectionGenerator","tree","options","sourceProject","targetProject","gateway","agentComponent","targetComponent","Error","protocol","name","agentGenerator","generator","agentProtocol","toLowerCase","supportedProtocols","includes","join","agentAuth","auth","agentComponentName","agentTargetName","rc","targetName","port","upstreamProjectName","upstreamDevTargetName","stripInvocations","root","preferInstallDependencies","languages"],"mappings":"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"}
|
package/src/migrations/latest/gateway-cognito-passthrough/__snapshots__/migration.spec.ts.snap
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`gateway-cognito-passthrough migration > adds auth=cognito and the Authorization allowlist to a Cognito agent 1`] = `
|
|
4
|
+
"import {
|
|
5
|
+
Runtime,
|
|
6
|
+
RuntimeAuthorizerConfiguration,
|
|
7
|
+
} from 'aws-cdk-lib/aws-bedrockagentcore';
|
|
8
|
+
import { Construct } from 'constructs';
|
|
9
|
+
|
|
10
|
+
export class MyCognitoAgent extends Construct {
|
|
11
|
+
public readonly agentCoreRuntime: Runtime;
|
|
12
|
+
/** Default Gateway target name for this agent. */
|
|
13
|
+
public readonly agentName = 'my-cognito-agent';
|
|
14
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
15
|
+
public readonly auth = 'cognito';
|
|
16
|
+
|
|
17
|
+
constructor(scope: Construct, id: string, props: MyCognitoAgentProps) {
|
|
18
|
+
super(scope, id);
|
|
19
|
+
const { identity } = props;
|
|
20
|
+
this.agentCoreRuntime = new Runtime(this, 'MyCognitoAgent', {
|
|
21
|
+
authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito(
|
|
22
|
+
identity.userPool,
|
|
23
|
+
[identity.userPoolClient],
|
|
24
|
+
),
|
|
25
|
+
// Receive the caller's Authorization header (validated by the authorizer).
|
|
26
|
+
requestHeaderConfiguration: {
|
|
27
|
+
allowlistedHeaders: ['Authorization'],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
"
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
exports[`gateway-cognito-passthrough migration > adds the request_header_configuration block to the terraform runtime 1`] = `
|
|
36
|
+
"resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
37
|
+
agent_runtime_name = "test"
|
|
38
|
+
|
|
39
|
+
dynamic "authorizer_configuration" {
|
|
40
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [var.authorizer_configuration.custom_jwt_authorizer] : []
|
|
41
|
+
content {
|
|
42
|
+
custom_jwt_authorizer {
|
|
43
|
+
discovery_url = authorizer_configuration.value.discovery_url
|
|
44
|
+
allowed_audience = authorizer_configuration.value.allowed_audience
|
|
45
|
+
allowed_clients = authorizer_configuration.value.allowed_clients
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
51
|
+
dynamic "request_header_configuration" {
|
|
52
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
53
|
+
content {
|
|
54
|
+
request_header_allowlist = ["Authorization"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
network_configuration {
|
|
59
|
+
network_mode = "PUBLIC"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
"
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
exports[`gateway-cognito-passthrough migration > makes the gateway pick the target credential from the agent auth 1`] = `
|
|
66
|
+
"import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore';
|
|
67
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
68
|
+
import { Construct } from 'constructs';
|
|
69
|
+
|
|
70
|
+
export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
71
|
+
public addAgent(
|
|
72
|
+
agent: Construct & {
|
|
73
|
+
readonly agentCoreRuntime: agentcore.Runtime;
|
|
74
|
+
readonly agentName: string;
|
|
75
|
+
readonly auth?: 'iam' | 'cognito';
|
|
76
|
+
},
|
|
77
|
+
props?: {
|
|
78
|
+
gatewayTargetName?: string;
|
|
79
|
+
},
|
|
80
|
+
): agentcore.CfnGatewayTarget {
|
|
81
|
+
return this.addAgentTarget({
|
|
82
|
+
gatewayTargetName: props?.gatewayTargetName ?? agent.agentName,
|
|
83
|
+
agentRuntimeArn: agent.agentCoreRuntime.agentRuntimeArn,
|
|
84
|
+
credentials: agent.auth === 'cognito' ? 'jwt-passthrough' : 'gateway-iam',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public addAgentTarget(props: {
|
|
89
|
+
gatewayTargetName: string;
|
|
90
|
+
agentRuntimeArn: string;
|
|
91
|
+
credentials?: 'gateway-iam' | 'jwt-passthrough';
|
|
92
|
+
}): agentcore.CfnGatewayTarget {
|
|
93
|
+
const target = new agentcore.CfnGatewayTarget(
|
|
94
|
+
this,
|
|
95
|
+
\`Target-\${props.gatewayTargetName}\`,
|
|
96
|
+
{
|
|
97
|
+
gatewayIdentifier: this.gateway.gatewayId,
|
|
98
|
+
name: props.gatewayTargetName,
|
|
99
|
+
targetConfiguration: {
|
|
100
|
+
http: {
|
|
101
|
+
agentcoreRuntime: {
|
|
102
|
+
arn: props.agentRuntimeArn,
|
|
103
|
+
qualifier: 'DEFAULT',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
// The bare gateway IAM role credential: runtime targets reject the
|
|
108
|
+
// iamCredentialProvider detail MCP targets carry.
|
|
109
|
+
credentialProviderConfigurations: [
|
|
110
|
+
{
|
|
111
|
+
credentialProviderType:
|
|
112
|
+
props.credentials === 'jwt-passthrough'
|
|
113
|
+
? 'JWT_PASSTHROUGH'
|
|
114
|
+
: 'GATEWAY_IAM_ROLE',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
return target;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public addMcpServerTarget(props: {
|
|
123
|
+
gatewayTargetName: string;
|
|
124
|
+
mcpServerRuntimeArn: string;
|
|
125
|
+
}): agentcore.CfnGatewayTarget {
|
|
126
|
+
const target = new agentcore.CfnGatewayTarget(
|
|
127
|
+
this,
|
|
128
|
+
\`Target-\${props.gatewayTargetName}\`,
|
|
129
|
+
{
|
|
130
|
+
gatewayIdentifier: this.gateway.gatewayId,
|
|
131
|
+
name: props.gatewayTargetName,
|
|
132
|
+
targetConfiguration: {
|
|
133
|
+
mcp: { openApiSchema: { inlinePayload: 'x' } },
|
|
134
|
+
},
|
|
135
|
+
credentialProviderConfigurations: [
|
|
136
|
+
{
|
|
137
|
+
credentialProviderType: 'GATEWAY_IAM_ROLE',
|
|
138
|
+
credentialProvider: {
|
|
139
|
+
iamCredentialProvider: { service: 'bedrock-agentcore' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
return target;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
"
|
|
149
|
+
`;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { applyGritQL, GRIT_INSERT_PLACEHOLDER, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
5
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
6
|
+
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR, SHARED_TERRAFORM_DIR } from "../../../utils/shared-constructs-constants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Bring existing workspaces up to the generators' current gateway->agent
|
|
9
|
+
* support for Cognito agents, which forward the caller's JWT to the runtime:
|
|
10
|
+
*
|
|
11
|
+
* - The vended `AgentCoreGateway` construct's `addAgent`/`addAgentTarget` pick
|
|
12
|
+
* the outbound credential from the agent's `auth`: a `cognito` agent uses
|
|
13
|
+
* `JWT_PASSTHROUGH` (the caller's token reaches the runtime), an `iam` agent
|
|
14
|
+
* the gateway role (`GATEWAY_IAM_ROLE`, the previous behaviour).
|
|
15
|
+
* - Vended agent constructs gain an `auth` member `addAgent` reads, and Cognito
|
|
16
|
+
* agents allowlist the `Authorization` header (CDK + Terraform runtime) so the
|
|
17
|
+
* runtime receives the forwarded token.
|
|
18
|
+
*
|
|
19
|
+
* These files are generated with `KeepExisting`, so without this an upgraded
|
|
20
|
+
* workspace pairs new app-level constructs with stale core ones. Diverged files
|
|
21
|
+
* are left untouched and reported via `nextSteps`.
|
|
22
|
+
*/ const GATEWAY_CONSTRUCT_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/agentcore-gateway/agentcore-gateway.ts`;
|
|
23
|
+
const AGENTS_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/agents`;
|
|
24
|
+
const TF_RUNTIME_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/agent-core/runtime.tf`;
|
|
25
|
+
/**
|
|
26
|
+
* Teach the vended `AgentCoreGateway` construct to pick the target credential
|
|
27
|
+
* from the agent's `auth` (JWT passthrough for Cognito, gateway role for IAM).
|
|
28
|
+
*/ const migrateGatewayConstruct = async (tree, nextSteps)=>{
|
|
29
|
+
if (!tree.exists(GATEWAY_CONSTRUCT_FILE)) {
|
|
30
|
+
return; // No vended gateway construct.
|
|
31
|
+
}
|
|
32
|
+
// Predates agent-target support (the gateway-agent-targets migration adds
|
|
33
|
+
// addAgentTarget first) — match the method, not a bare identifier.
|
|
34
|
+
if (!await matchGritQL(tree, GATEWAY_CONSTRUCT_FILE, '`addAgentTarget(props: { $_ }): agentcore.CfnGatewayTarget { $_ }`')) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Already migrated: the credentials member is on addAgentTarget's props.
|
|
38
|
+
if (await matchGritQL(tree, GATEWAY_CONSTRUCT_FILE, "`credentials?: 'gateway-iam' | 'jwt-passthrough'`")) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const divergedMessage = `In ${GATEWAY_CONSTRUCT_FILE}: have addAgent read the agent's \`auth\` and pass \`credentials: 'jwt-passthrough'\` to addAgentTarget, so the target's credentialProviderType becomes 'JWT_PASSTHROUGH' for Cognito agents.`;
|
|
42
|
+
// GritQL patterns for each of the four edits. Verified against the tree
|
|
43
|
+
// up-front so the transform is all-or-nothing: a construct missing any
|
|
44
|
+
// anchor is left untouched with a single actionable next step, rather than
|
|
45
|
+
// half-migrated.
|
|
46
|
+
const authParamPattern = `\`readonly agentName: string\` as $sig where {
|
|
47
|
+
$program <: not contains \`readonly auth?\`
|
|
48
|
+
} => \`$sig;
|
|
49
|
+
readonly auth?: 'iam' | 'cognito'\``;
|
|
50
|
+
const callPattern = `\`agentRuntimeArn: agent.agentCoreRuntime.agentRuntimeArn\` as $prop where {
|
|
51
|
+
$program <: not contains \`credentials:\`
|
|
52
|
+
} => \`$prop,
|
|
53
|
+
credentials: agent.auth === 'cognito' ? 'jwt-passthrough' : 'gateway-iam'\``;
|
|
54
|
+
// The addAgentTarget props type: matched on its `agentRuntimeArn: string`
|
|
55
|
+
// member signature (without trailing semicolon), which the rewrite re-adds
|
|
56
|
+
// followed by the credentials member. The `: string` type distinguishes it
|
|
57
|
+
// from the `agentRuntimeArn:` value in the addAgent call.
|
|
58
|
+
const paramPattern = `\`agentRuntimeArn: string\` as $sig where {
|
|
59
|
+
$program <: not contains \`credentials?\`
|
|
60
|
+
} => \`$sig;
|
|
61
|
+
credentials?: 'gateway-iam' | 'jwt-passthrough'\``;
|
|
62
|
+
// Scoped to the agent target's own CfnGatewayTarget (the one configured with
|
|
63
|
+
// an agentcoreRuntime), so the MCP server target's GATEWAY_IAM_ROLE — a
|
|
64
|
+
// sibling method with its own credential block — is left untouched.
|
|
65
|
+
const credentialPattern = `\`credentialProviderType: 'GATEWAY_IAM_ROLE'\` as $cred where {
|
|
66
|
+
$cred <: within \`new agentcore.CfnGatewayTarget($_, $_, $config)\`,
|
|
67
|
+
$config <: contains \`agentcoreRuntime\`
|
|
68
|
+
} => \`credentialProviderType:
|
|
69
|
+
props.credentials === 'jwt-passthrough'
|
|
70
|
+
? 'JWT_PASSTHROUGH'
|
|
71
|
+
: 'GATEWAY_IAM_ROLE'\``;
|
|
72
|
+
// Verify every anchor matches before mutating anything (matchGritQL is
|
|
73
|
+
// read-only), so a diverged construct is never left partially migrated.
|
|
74
|
+
for (const pattern of [
|
|
75
|
+
authParamPattern,
|
|
76
|
+
callPattern,
|
|
77
|
+
paramPattern,
|
|
78
|
+
credentialPattern
|
|
79
|
+
]){
|
|
80
|
+
if (!await matchGritQL(tree, GATEWAY_CONSTRUCT_FILE, pattern)) {
|
|
81
|
+
nextSteps.push(divergedMessage);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, authParamPattern);
|
|
86
|
+
await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, callPattern);
|
|
87
|
+
await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, paramPattern);
|
|
88
|
+
await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, credentialPattern);
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Add the `auth` member to each vended agent construct, and for Cognito agents
|
|
92
|
+
* allowlist the Authorization header so the runtime receives the forwarded JWT.
|
|
93
|
+
*/ const migrateAgentConstructs = async (tree, nextSteps)=>{
|
|
94
|
+
if (!tree.exists(AGENTS_DIR)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const dir of tree.children(AGENTS_DIR)){
|
|
98
|
+
const constructFile = `${AGENTS_DIR}/${dir}/${dir}.ts`;
|
|
99
|
+
if (!tree.exists(constructFile)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Only agents (which carry a gateway target name), not MCP servers.
|
|
103
|
+
if (!await matchGritQL(tree, constructFile, '`public readonly agentName = $_`')) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (await matchGritQL(tree, constructFile, '`public readonly auth = $_`')) {
|
|
107
|
+
continue; // Already migrated (or user-added).
|
|
108
|
+
}
|
|
109
|
+
// Cognito agents configure the runtime's inbound authorizer with a Cognito
|
|
110
|
+
// JWT — match that authorizerConfiguration property specifically.
|
|
111
|
+
const isCognito = await matchGritQL(tree, constructFile, '`authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito($_)`');
|
|
112
|
+
const authValue = isCognito ? 'cognito' : 'iam';
|
|
113
|
+
// The `auth` member, after the agentName member. Matched without the
|
|
114
|
+
// trailing semicolon — GritQL parses the field's initializer as its AST
|
|
115
|
+
// node, so the semicolon is appended by the rewrite.
|
|
116
|
+
const authPattern = `\`public readonly agentName = $name\` as $field => \`$field;
|
|
117
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
118
|
+
public readonly auth = '${authValue}'\``;
|
|
119
|
+
// Cognito agents allowlist the Authorization header so the runtime (and a
|
|
120
|
+
// fronting gateway, via passthrough) receives the caller's token.
|
|
121
|
+
const headerNeeded = isCognito && !await matchGritQL(tree, constructFile, '`requestHeaderConfiguration: $_`');
|
|
122
|
+
const headerPattern = `\`authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito($args)\` as $authz where {
|
|
123
|
+
$program <: not contains \`requestHeaderConfiguration\`
|
|
124
|
+
} => \`$authz,
|
|
125
|
+
${GRIT_INSERT_PLACEHOLDER}\``;
|
|
126
|
+
const headerText = `// Receive the caller's Authorization header (validated by the authorizer).
|
|
127
|
+
requestHeaderConfiguration: {
|
|
128
|
+
allowlistedHeaders: ['Authorization'],
|
|
129
|
+
}`;
|
|
130
|
+
// Verify every anchor up-front so the construct is never half-migrated:
|
|
131
|
+
// if any edit can't be applied, leave the file untouched with one
|
|
132
|
+
// actionable next step.
|
|
133
|
+
const authOk = await matchGritQL(tree, constructFile, authPattern);
|
|
134
|
+
const headerOk = !headerNeeded || await matchGritQL(tree, constructFile, headerPattern);
|
|
135
|
+
if (!authOk || !headerOk) {
|
|
136
|
+
const actions = [
|
|
137
|
+
`add a \`public readonly auth = '${authValue}';\` member`
|
|
138
|
+
];
|
|
139
|
+
if (headerNeeded) {
|
|
140
|
+
actions.push(`add \`requestHeaderConfiguration: { allowlistedHeaders: ['Authorization'] }\` to its Runtime so a gateway can pass the caller's JWT through`);
|
|
141
|
+
}
|
|
142
|
+
nextSteps.push(`In ${constructFile}: ${actions.join('; and ')}.`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
await applyGritQL(tree, constructFile, authPattern);
|
|
146
|
+
if (headerNeeded) {
|
|
147
|
+
await insertViaGritQL(tree, constructFile, headerPattern, headerText);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Add the `request_header_configuration` block to the vended Terraform runtime
|
|
153
|
+
* module so a JWT-authorized runtime receives the forwarded Authorization
|
|
154
|
+
* header (the analogue of the CDK requestHeaderConfiguration).
|
|
155
|
+
*/ const migrateTerraformRuntime = async (tree, nextSteps)=>{
|
|
156
|
+
if (!tree.exists(TF_RUNTIME_FILE)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (await matchGritQL(tree, TF_RUNTIME_FILE, 'language hcl\n`dynamic "request_header_configuration" { $_ }`')) {
|
|
160
|
+
return; // Already migrated.
|
|
161
|
+
}
|
|
162
|
+
const tfActionMessage = `In ${TF_RUNTIME_FILE}: add a \`request_header_configuration { request_header_allowlist = ["Authorization"] }\` block (guarded by the JWT authorizer) to the aws_bedrockagentcore_agent_runtime resource so a gateway can pass the caller's JWT through.`;
|
|
163
|
+
const hasAuthorizerBlock = await matchGritQL(tree, TF_RUNTIME_FILE, 'language hcl\n`dynamic "authorizer_configuration" { $_ }`');
|
|
164
|
+
if (!hasAuthorizerBlock) {
|
|
165
|
+
nextSteps.push(tfActionMessage);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const added = await insertViaGritQL(tree, TF_RUNTIME_FILE, `language hcl
|
|
169
|
+
\`dynamic "authorizer_configuration" { $body }\` as $block where {
|
|
170
|
+
$block <: within \`resource "aws_bedrockagentcore_agent_runtime" $_ { $_ }\`
|
|
171
|
+
} => \`$block
|
|
172
|
+
|
|
173
|
+
${GRIT_INSERT_PLACEHOLDER}\``, `# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
174
|
+
dynamic "request_header_configuration" {
|
|
175
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
176
|
+
content {
|
|
177
|
+
request_header_allowlist = ["Authorization"]
|
|
178
|
+
}
|
|
179
|
+
}`);
|
|
180
|
+
if (!added) {
|
|
181
|
+
nextSteps.push(tfActionMessage);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
export default async function migration(tree) {
|
|
185
|
+
const nextSteps = [];
|
|
186
|
+
await migrateGatewayConstruct(tree, nextSteps);
|
|
187
|
+
await migrateAgentConstructs(tree, nextSteps);
|
|
188
|
+
await migrateTerraformRuntime(tree, nextSteps);
|
|
189
|
+
await formatFilesInSubtree(tree);
|
|
190
|
+
return {
|
|
191
|
+
nextSteps
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/gateway-cognito-passthrough/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n applyGritQL,\n GRIT_INSERT_PLACEHOLDER,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n SHARED_TERRAFORM_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Bring existing workspaces up to the generators' current gateway->agent\n * support for Cognito agents, which forward the caller's JWT to the runtime:\n *\n * - The vended `AgentCoreGateway` construct's `addAgent`/`addAgentTarget` pick\n * the outbound credential from the agent's `auth`: a `cognito` agent uses\n * `JWT_PASSTHROUGH` (the caller's token reaches the runtime), an `iam` agent\n * the gateway role (`GATEWAY_IAM_ROLE`, the previous behaviour).\n * - Vended agent constructs gain an `auth` member `addAgent` reads, and Cognito\n * agents allowlist the `Authorization` header (CDK + Terraform runtime) so the\n * runtime receives the forwarded token.\n *\n * These files are generated with `KeepExisting`, so without this an upgraded\n * workspace pairs new app-level constructs with stale core ones. Diverged files\n * are left untouched and reported via `nextSteps`.\n */\n\nconst GATEWAY_CONSTRUCT_FILE = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/core/agentcore-gateway/agentcore-gateway.ts`;\nconst AGENTS_DIR = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src/app/agents`;\nconst TF_RUNTIME_FILE = `${PACKAGES_DIR}/${SHARED_TERRAFORM_DIR}/src/core/agent-core/runtime.tf`;\n\n/**\n * Teach the vended `AgentCoreGateway` construct to pick the target credential\n * from the agent's `auth` (JWT passthrough for Cognito, gateway role for IAM).\n */\nconst migrateGatewayConstruct = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(GATEWAY_CONSTRUCT_FILE)) {\n return; // No vended gateway construct.\n }\n // Predates agent-target support (the gateway-agent-targets migration adds\n // addAgentTarget first) — match the method, not a bare identifier.\n if (\n !(await matchGritQL(\n tree,\n GATEWAY_CONSTRUCT_FILE,\n '`addAgentTarget(props: { $_ }): agentcore.CfnGatewayTarget { $_ }`',\n ))\n ) {\n return;\n }\n // Already migrated: the credentials member is on addAgentTarget's props.\n if (\n await matchGritQL(\n tree,\n GATEWAY_CONSTRUCT_FILE,\n \"`credentials?: 'gateway-iam' | 'jwt-passthrough'`\",\n )\n ) {\n return;\n }\n\n const divergedMessage = `In ${GATEWAY_CONSTRUCT_FILE}: have addAgent read the agent's \\`auth\\` and pass \\`credentials: 'jwt-passthrough'\\` to addAgentTarget, so the target's credentialProviderType becomes 'JWT_PASSTHROUGH' for Cognito agents.`;\n\n // GritQL patterns for each of the four edits. Verified against the tree\n // up-front so the transform is all-or-nothing: a construct missing any\n // anchor is left untouched with a single actionable next step, rather than\n // half-migrated.\n const authParamPattern = `\\`readonly agentName: string\\` as $sig where {\n $program <: not contains \\`readonly auth?\\`\n} => \\`$sig;\n readonly auth?: 'iam' | 'cognito'\\``;\n\n const callPattern = `\\`agentRuntimeArn: agent.agentCoreRuntime.agentRuntimeArn\\` as $prop where {\n $program <: not contains \\`credentials:\\`\n} => \\`$prop,\n credentials: agent.auth === 'cognito' ? 'jwt-passthrough' : 'gateway-iam'\\``;\n\n // The addAgentTarget props type: matched on its `agentRuntimeArn: string`\n // member signature (without trailing semicolon), which the rewrite re-adds\n // followed by the credentials member. The `: string` type distinguishes it\n // from the `agentRuntimeArn:` value in the addAgent call.\n const paramPattern = `\\`agentRuntimeArn: string\\` as $sig where {\n $program <: not contains \\`credentials?\\`\n} => \\`$sig;\n credentials?: 'gateway-iam' | 'jwt-passthrough'\\``;\n\n // Scoped to the agent target's own CfnGatewayTarget (the one configured with\n // an agentcoreRuntime), so the MCP server target's GATEWAY_IAM_ROLE — a\n // sibling method with its own credential block — is left untouched.\n const credentialPattern = `\\`credentialProviderType: 'GATEWAY_IAM_ROLE'\\` as $cred where {\n $cred <: within \\`new agentcore.CfnGatewayTarget($_, $_, $config)\\`,\n $config <: contains \\`agentcoreRuntime\\`\n} => \\`credentialProviderType:\n props.credentials === 'jwt-passthrough'\n ? 'JWT_PASSTHROUGH'\n : 'GATEWAY_IAM_ROLE'\\``;\n\n // Verify every anchor matches before mutating anything (matchGritQL is\n // read-only), so a diverged construct is never left partially migrated.\n for (const pattern of [\n authParamPattern,\n callPattern,\n paramPattern,\n credentialPattern,\n ]) {\n if (!(await matchGritQL(tree, GATEWAY_CONSTRUCT_FILE, pattern))) {\n nextSteps.push(divergedMessage);\n return;\n }\n }\n\n await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, authParamPattern);\n await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, callPattern);\n await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, paramPattern);\n await applyGritQL(tree, GATEWAY_CONSTRUCT_FILE, credentialPattern);\n};\n\n/**\n * Add the `auth` member to each vended agent construct, and for Cognito agents\n * allowlist the Authorization header so the runtime receives the forwarded JWT.\n */\nconst migrateAgentConstructs = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(AGENTS_DIR)) {\n return;\n }\n for (const dir of tree.children(AGENTS_DIR)) {\n const constructFile = `${AGENTS_DIR}/${dir}/${dir}.ts`;\n if (!tree.exists(constructFile)) {\n continue;\n }\n // Only agents (which carry a gateway target name), not MCP servers.\n if (\n !(await matchGritQL(\n tree,\n constructFile,\n '`public readonly agentName = $_`',\n ))\n ) {\n continue;\n }\n if (await matchGritQL(tree, constructFile, '`public readonly auth = $_`')) {\n continue; // Already migrated (or user-added).\n }\n\n // Cognito agents configure the runtime's inbound authorizer with a Cognito\n // JWT — match that authorizerConfiguration property specifically.\n const isCognito = await matchGritQL(\n tree,\n constructFile,\n '`authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito($_)`',\n );\n const authValue = isCognito ? 'cognito' : 'iam';\n\n // The `auth` member, after the agentName member. Matched without the\n // trailing semicolon — GritQL parses the field's initializer as its AST\n // node, so the semicolon is appended by the rewrite.\n const authPattern = `\\`public readonly agentName = $name\\` as $field => \\`$field;\n /** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */\n public readonly auth = '${authValue}'\\``;\n\n // Cognito agents allowlist the Authorization header so the runtime (and a\n // fronting gateway, via passthrough) receives the caller's token.\n const headerNeeded =\n isCognito &&\n !(await matchGritQL(\n tree,\n constructFile,\n '`requestHeaderConfiguration: $_`',\n ));\n const headerPattern = `\\`authorizerConfiguration: RuntimeAuthorizerConfiguration.usingCognito($args)\\` as $authz where {\n $program <: not contains \\`requestHeaderConfiguration\\`\n} => \\`$authz,\n ${GRIT_INSERT_PLACEHOLDER}\\``;\n const headerText = `// Receive the caller's Authorization header (validated by the authorizer).\n requestHeaderConfiguration: {\n allowlistedHeaders: ['Authorization'],\n }`;\n\n // Verify every anchor up-front so the construct is never half-migrated:\n // if any edit can't be applied, leave the file untouched with one\n // actionable next step.\n const authOk = await matchGritQL(tree, constructFile, authPattern);\n const headerOk =\n !headerNeeded || (await matchGritQL(tree, constructFile, headerPattern));\n if (!authOk || !headerOk) {\n const actions = [\n `add a \\`public readonly auth = '${authValue}';\\` member`,\n ];\n if (headerNeeded) {\n actions.push(\n `add \\`requestHeaderConfiguration: { allowlistedHeaders: ['Authorization'] }\\` to its Runtime so a gateway can pass the caller's JWT through`,\n );\n }\n nextSteps.push(`In ${constructFile}: ${actions.join('; and ')}.`);\n continue;\n }\n\n await applyGritQL(tree, constructFile, authPattern);\n if (headerNeeded) {\n await insertViaGritQL(tree, constructFile, headerPattern, headerText);\n }\n }\n};\n\n/**\n * Add the `request_header_configuration` block to the vended Terraform runtime\n * module so a JWT-authorized runtime receives the forwarded Authorization\n * header (the analogue of the CDK requestHeaderConfiguration).\n */\nconst migrateTerraformRuntime = async (\n tree: Tree,\n nextSteps: string[],\n): Promise<void> => {\n if (!tree.exists(TF_RUNTIME_FILE)) {\n return;\n }\n if (\n await matchGritQL(\n tree,\n TF_RUNTIME_FILE,\n 'language hcl\\n`dynamic \"request_header_configuration\" { $_ }`',\n )\n ) {\n return; // Already migrated.\n }\n\n const tfActionMessage = `In ${TF_RUNTIME_FILE}: add a \\`request_header_configuration { request_header_allowlist = [\"Authorization\"] }\\` block (guarded by the JWT authorizer) to the aws_bedrockagentcore_agent_runtime resource so a gateway can pass the caller's JWT through.`;\n\n const hasAuthorizerBlock = await matchGritQL(\n tree,\n TF_RUNTIME_FILE,\n 'language hcl\\n`dynamic \"authorizer_configuration\" { $_ }`',\n );\n if (!hasAuthorizerBlock) {\n nextSteps.push(tfActionMessage);\n return;\n }\n\n const added = await insertViaGritQL(\n tree,\n TF_RUNTIME_FILE,\n `language hcl\n\\`dynamic \"authorizer_configuration\" { $body }\\` as $block where {\n $block <: within \\`resource \"aws_bedrockagentcore_agent_runtime\" $_ { $_ }\\`\n} => \\`$block\n\n ${GRIT_INSERT_PLACEHOLDER}\\``,\n `# Receive the caller's Authorization header (validated by the JWT authorizer).\n dynamic \"request_header_configuration\" {\n for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []\n content {\n request_header_allowlist = [\"Authorization\"]\n }\n }`,\n );\n if (!added) {\n nextSteps.push(tfActionMessage);\n }\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n await migrateGatewayConstruct(tree, nextSteps);\n await migrateAgentConstructs(tree, nextSteps);\n await migrateTerraformRuntime(tree, nextSteps);\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["applyGritQL","GRIT_INSERT_PLACEHOLDER","insertViaGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","SHARED_TERRAFORM_DIR","GATEWAY_CONSTRUCT_FILE","AGENTS_DIR","TF_RUNTIME_FILE","migrateGatewayConstruct","tree","nextSteps","exists","divergedMessage","authParamPattern","callPattern","paramPattern","credentialPattern","pattern","push","migrateAgentConstructs","dir","children","constructFile","isCognito","authValue","authPattern","headerNeeded","headerPattern","headerText","authOk","headerOk","actions","join","migrateTerraformRuntime","tfActionMessage","hasAuthorizerBlock","added","migration"],"mappings":"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"}
|
package/src/migrations/latest/strip-pip-from-python-images/__snapshots__/migration.spec.ts.snap
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`strip-pip-from-python-images migration > strips pip from a py#agent Dockerfile 1`] = `
|
|
4
|
+
"FROM public.ecr.aws/docker/library/python:3.14-slim
|
|
5
|
+
|
|
6
|
+
# Remove the base image's pip: the runtime runs from the bundled environment and
|
|
7
|
+
# never needs it, and pip's vendored packages carry known HIGH/CRITICAL
|
|
8
|
+
# vulnerabilities flagged by the image scan.
|
|
9
|
+
RUN python -m pip uninstall -y pip && \\
|
|
10
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \\
|
|
11
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info
|
|
12
|
+
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
|
|
15
|
+
# Copy bundled package
|
|
16
|
+
COPY . /app
|
|
17
|
+
|
|
18
|
+
EXPOSE 8080
|
|
19
|
+
|
|
20
|
+
ENV PYTHONPATH=/app
|
|
21
|
+
ENV PATH="/app/bin:\${PATH}"
|
|
22
|
+
|
|
23
|
+
CMD ["python", "bin/opentelemetry-instrument", "python", "-m", "proj_py_project.my_agent.main"]
|
|
24
|
+
"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`strip-pip-from-python-images migration > strips pip from a py#mcp-server Dockerfile 1`] = `
|
|
28
|
+
"FROM public.ecr.aws/docker/library/python:3.14-slim
|
|
29
|
+
|
|
30
|
+
# Remove the base image's pip: the runtime runs from the bundled environment and
|
|
31
|
+
# never needs it, and pip's vendored packages carry known HIGH/CRITICAL
|
|
32
|
+
# vulnerabilities flagged by the image scan.
|
|
33
|
+
RUN python -m pip uninstall -y pip && \\
|
|
34
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \\
|
|
35
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info
|
|
36
|
+
|
|
37
|
+
WORKDIR /app
|
|
38
|
+
|
|
39
|
+
# Copy bundled package
|
|
40
|
+
COPY . /app
|
|
41
|
+
|
|
42
|
+
EXPOSE 8000
|
|
43
|
+
|
|
44
|
+
ENV PYTHONPATH=/app
|
|
45
|
+
|
|
46
|
+
CMD ["python", "bin/opentelemetry-instrument", "python", "-m", "uvicorn", "proj_py_project.my_mcp.http:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
47
|
+
"
|
|
48
|
+
`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { type MigrationReturnObject, type Tree } from '@nx/devkit';
|
|
6
|
+
export default function migration(tree: Tree): Promise<MigrationReturnObject>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { getProjects, joinPathFragments } from "@nx/devkit";
|
|
5
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
6
|
+
/**
|
|
7
|
+
* Remove pip from the vended Python agent / MCP server images.
|
|
8
|
+
*
|
|
9
|
+
* The runtime runs entirely from the bundled `/app` environment and never needs
|
|
10
|
+
* pip, whose vendored packages (setuptools, msgpack) in the base image carry
|
|
11
|
+
* known HIGH/CRITICAL vulnerabilities the image scan flags. New workspaces get
|
|
12
|
+
* this from the generator templates; this brings existing ones up to date.
|
|
13
|
+
*
|
|
14
|
+
* Only the Dockerfiles of `py#agent` / `py#mcp-server` components are touched,
|
|
15
|
+
* located from their project metadata. The Dockerfile is generated
|
|
16
|
+
* `KeepExisting`, so it is edited in place: the pip removal is inserted right
|
|
17
|
+
* after the `FROM` line. A Dockerfile whose pip usage has been customised (an
|
|
18
|
+
* added `pip install`, or any pip command other than this removal) is left
|
|
19
|
+
* untouched and reported via `nextSteps`, since stripping pip could break it.
|
|
20
|
+
*/ /**
|
|
21
|
+
* The generators whose runtime images vend pip and should have it stripped.
|
|
22
|
+
* Hardcoded rather than imported from the generators: a migration matches the
|
|
23
|
+
* ids workspaces recorded at generation time, which must stay fixed even if the
|
|
24
|
+
* generators' current ids change later.
|
|
25
|
+
*/ const RUNTIME_IMAGE_GENERATOR_IDS = [
|
|
26
|
+
'py#agent',
|
|
27
|
+
'py#mcp-server'
|
|
28
|
+
];
|
|
29
|
+
const PIP_REMOVAL = `# Remove the base image's pip: the runtime runs from the bundled environment and
|
|
30
|
+
# never needs it, and pip's vendored packages carry known HIGH/CRITICAL
|
|
31
|
+
# vulnerabilities flagged by the image scan.
|
|
32
|
+
RUN python -m pip uninstall -y pip && \\
|
|
33
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \\
|
|
34
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info`;
|
|
35
|
+
/** The exact pip command this migration adds; used to detect an applied fix. */ const PIP_UNINSTALL = 'pip uninstall -y pip';
|
|
36
|
+
/** Matches any `pip` invocation in a RUN instruction (python -m pip … or pip …). */ const PIP_COMMAND = /\b(?:python\s+-m\s+)?pip\s+\S+/g;
|
|
37
|
+
/**
|
|
38
|
+
* The Dockerfiles of every `py#agent` / `py#mcp-server` component, located from
|
|
39
|
+
* each project's recorded component metadata (`path` is the component directory
|
|
40
|
+
* relative to the project root).
|
|
41
|
+
*/ const runtimeImageDockerfiles = (tree)=>{
|
|
42
|
+
const paths = [];
|
|
43
|
+
for (const project of getProjects(tree).values()){
|
|
44
|
+
const components = project.metadata?.components ?? [];
|
|
45
|
+
for (const component of components){
|
|
46
|
+
if (!RUNTIME_IMAGE_GENERATOR_IDS.includes(component.generator) || !component.path) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const dockerfile = joinPathFragments(projectRoot(project), component.path, 'Dockerfile');
|
|
50
|
+
if (tree.exists(dockerfile)) {
|
|
51
|
+
paths.push(dockerfile);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return paths;
|
|
56
|
+
};
|
|
57
|
+
/** A project's root, falling back to sourceRoot for the TS-solution layout. */ const projectRoot = (project)=>project.root ?? project.sourceRoot ?? '';
|
|
58
|
+
export default async function migration(tree) {
|
|
59
|
+
const nextSteps = [];
|
|
60
|
+
for (const filePath of runtimeImageDockerfiles(tree)){
|
|
61
|
+
const content = tree.read(filePath, 'utf-8');
|
|
62
|
+
if (!content) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (content.includes(PIP_UNINSTALL)) {
|
|
66
|
+
continue; // Already stripped (migrated or user-added).
|
|
67
|
+
}
|
|
68
|
+
// Leave a customised Dockerfile alone: any pip usage other than this
|
|
69
|
+
// migration's removal (e.g. an added `pip install`) means stripping pip
|
|
70
|
+
// could break the image, so flag it for the user instead of editing.
|
|
71
|
+
const pipUsages = content.match(PIP_COMMAND) ?? [];
|
|
72
|
+
if (pipUsages.length > 0) {
|
|
73
|
+
nextSteps.push(`In ${filePath}: this image uses pip (${pipUsages.join(', ')}), so it was left unchanged. pip's vendored packages carry known vulnerabilities flagged by the image scan — remove pip once it is no longer needed, e.g. \`RUN python -m pip uninstall -y pip && rm -rf /usr/local/lib/python*/site-packages/pip /usr/local/lib/python*/site-packages/pip-*.dist-info\`.`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Insert the pip removal after the FROM line and its trailing blank line,
|
|
77
|
+
// matching the spacing a freshly generated Dockerfile has.
|
|
78
|
+
const updated = content.replace(/^(FROM\s+\S*python:.*\n)\n/m, `$1\n${PIP_REMOVAL}\n\n`);
|
|
79
|
+
if (updated === content) {
|
|
80
|
+
nextSteps.push(`In ${filePath}: remove pip from the image so its vulnerable vendored packages are not scanned — add \`RUN python -m pip uninstall -y pip && rm -rf /usr/local/lib/python*/site-packages/pip /usr/local/lib/python*/site-packages/pip-*.dist-info\` after the FROM line (the runtime does not need pip).`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
tree.write(filePath, updated);
|
|
84
|
+
}
|
|
85
|
+
await formatFilesInSubtree(tree);
|
|
86
|
+
return {
|
|
87
|
+
nextSteps
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/strip-pip-from-python-images/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport {\n getProjects,\n joinPathFragments,\n type MigrationReturnObject,\n type ProjectConfiguration,\n type Tree,\n} from '@nx/devkit';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport type { ComponentMetadata } from '../../../utils/nx';\n\n/**\n * Remove pip from the vended Python agent / MCP server images.\n *\n * The runtime runs entirely from the bundled `/app` environment and never needs\n * pip, whose vendored packages (setuptools, msgpack) in the base image carry\n * known HIGH/CRITICAL vulnerabilities the image scan flags. New workspaces get\n * this from the generator templates; this brings existing ones up to date.\n *\n * Only the Dockerfiles of `py#agent` / `py#mcp-server` components are touched,\n * located from their project metadata. The Dockerfile is generated\n * `KeepExisting`, so it is edited in place: the pip removal is inserted right\n * after the `FROM` line. A Dockerfile whose pip usage has been customised (an\n * added `pip install`, or any pip command other than this removal) is left\n * untouched and reported via `nextSteps`, since stripping pip could break it.\n */\n\n/**\n * The generators whose runtime images vend pip and should have it stripped.\n * Hardcoded rather than imported from the generators: a migration matches the\n * ids workspaces recorded at generation time, which must stay fixed even if the\n * generators' current ids change later.\n */\nconst RUNTIME_IMAGE_GENERATOR_IDS = ['py#agent', 'py#mcp-server'];\n\nconst PIP_REMOVAL = `# Remove the base image's pip: the runtime runs from the bundled environment and\n# never needs it, and pip's vendored packages carry known HIGH/CRITICAL\n# vulnerabilities flagged by the image scan.\nRUN python -m pip uninstall -y pip && \\\\\n rm -rf /usr/local/lib/python*/site-packages/pip \\\\\n /usr/local/lib/python*/site-packages/pip-*.dist-info`;\n\n/** The exact pip command this migration adds; used to detect an applied fix. */\nconst PIP_UNINSTALL = 'pip uninstall -y pip';\n\n/** Matches any `pip` invocation in a RUN instruction (python -m pip … or pip …). */\nconst PIP_COMMAND = /\\b(?:python\\s+-m\\s+)?pip\\s+\\S+/g;\n\n/**\n * The Dockerfiles of every `py#agent` / `py#mcp-server` component, located from\n * each project's recorded component metadata (`path` is the component directory\n * relative to the project root).\n */\nconst runtimeImageDockerfiles = (tree: Tree): string[] => {\n const paths: string[] = [];\n for (const project of getProjects(tree).values()) {\n const components = ((\n project.metadata as { components?: ComponentMetadata[] }\n )?.components ?? []) as ComponentMetadata[];\n for (const component of components) {\n if (\n !RUNTIME_IMAGE_GENERATOR_IDS.includes(component.generator) ||\n !component.path\n ) {\n continue;\n }\n const dockerfile = joinPathFragments(\n projectRoot(project),\n component.path,\n 'Dockerfile',\n );\n if (tree.exists(dockerfile)) {\n paths.push(dockerfile);\n }\n }\n }\n return paths;\n};\n\n/** A project's root, falling back to sourceRoot for the TS-solution layout. */\nconst projectRoot = (project: ProjectConfiguration): string =>\n project.root ?? project.sourceRoot ?? '';\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const filePath of runtimeImageDockerfiles(tree)) {\n const content = tree.read(filePath, 'utf-8');\n if (!content) {\n continue;\n }\n if (content.includes(PIP_UNINSTALL)) {\n continue; // Already stripped (migrated or user-added).\n }\n\n // Leave a customised Dockerfile alone: any pip usage other than this\n // migration's removal (e.g. an added `pip install`) means stripping pip\n // could break the image, so flag it for the user instead of editing.\n const pipUsages = content.match(PIP_COMMAND) ?? [];\n if (pipUsages.length > 0) {\n nextSteps.push(\n `In ${filePath}: this image uses pip (${pipUsages.join(', ')}), so it was left unchanged. pip's vendored packages carry known vulnerabilities flagged by the image scan — remove pip once it is no longer needed, e.g. \\`RUN python -m pip uninstall -y pip && rm -rf /usr/local/lib/python*/site-packages/pip /usr/local/lib/python*/site-packages/pip-*.dist-info\\`.`,\n );\n continue;\n }\n\n // Insert the pip removal after the FROM line and its trailing blank line,\n // matching the spacing a freshly generated Dockerfile has.\n const updated = content.replace(\n /^(FROM\\s+\\S*python:.*\\n)\\n/m,\n `$1\\n${PIP_REMOVAL}\\n\\n`,\n );\n if (updated === content) {\n nextSteps.push(\n `In ${filePath}: remove pip from the image so its vulnerable vendored packages are not scanned — add \\`RUN python -m pip uninstall -y pip && rm -rf /usr/local/lib/python*/site-packages/pip /usr/local/lib/python*/site-packages/pip-*.dist-info\\` after the FROM line (the runtime does not need pip).`,\n );\n continue;\n }\n tree.write(filePath, updated);\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["getProjects","joinPathFragments","formatFilesInSubtree","RUNTIME_IMAGE_GENERATOR_IDS","PIP_REMOVAL","PIP_UNINSTALL","PIP_COMMAND","runtimeImageDockerfiles","tree","paths","project","values","components","metadata","component","includes","generator","path","dockerfile","projectRoot","exists","push","root","sourceRoot","migration","nextSteps","filePath","content","read","pipUsages","match","length","join","updated","replace","write"],"mappings":"AAAA;;;CAGC,GACD,SACEA,WAAW,EACXC,iBAAiB,QAIZ,aAAa;AACpB,SAASC,oBAAoB,QAAQ,2BAAwB;AAG7D;;;;;;;;;;;;;;CAcC,GAED;;;;;CAKC,GACD,MAAMC,8BAA8B;IAAC;IAAY;CAAgB;AAEjE,MAAMC,cAAc,CAAC;;;;;+DAK0C,CAAC;AAEhE,8EAA8E,GAC9E,MAAMC,gBAAgB;AAEtB,kFAAkF,GAClF,MAAMC,cAAc;AAEpB;;;;CAIC,GACD,MAAMC,0BAA0B,CAACC;IAC/B,MAAMC,QAAkB,EAAE;IAC1B,KAAK,MAAMC,WAAWV,YAAYQ,MAAMG,MAAM,GAAI;QAChD,MAAMC,aAAc,AAClBF,QAAQG,QAAQ,EACfD,cAAc,EAAE;QACnB,KAAK,MAAME,aAAaF,WAAY;YAClC,IACE,CAACT,4BAA4BY,QAAQ,CAACD,UAAUE,SAAS,KACzD,CAACF,UAAUG,IAAI,EACf;gBACA;YACF;YACA,MAAMC,aAAajB,kBACjBkB,YAAYT,UACZI,UAAUG,IAAI,EACd;YAEF,IAAIT,KAAKY,MAAM,CAACF,aAAa;gBAC3BT,MAAMY,IAAI,CAACH;YACb;QACF;IACF;IACA,OAAOT;AACT;AAEA,6EAA6E,GAC7E,MAAMU,cAAc,CAACT,UACnBA,QAAQY,IAAI,IAAIZ,QAAQa,UAAU,IAAI;AAExC,eAAe,eAAeC,UAC5BhB,IAAU;IAEV,MAAMiB,YAAsB,EAAE;IAE9B,KAAK,MAAMC,YAAYnB,wBAAwBC,MAAO;QACpD,MAAMmB,UAAUnB,KAAKoB,IAAI,CAACF,UAAU;QACpC,IAAI,CAACC,SAAS;YACZ;QACF;QACA,IAAIA,QAAQZ,QAAQ,CAACV,gBAAgB;YACnC,UAAU,6CAA6C;QACzD;QAEA,qEAAqE;QACrE,wEAAwE;QACxE,qEAAqE;QACrE,MAAMwB,YAAYF,QAAQG,KAAK,CAACxB,gBAAgB,EAAE;QAClD,IAAIuB,UAAUE,MAAM,GAAG,GAAG;YACxBN,UAAUJ,IAAI,CACZ,CAAC,GAAG,EAAEK,SAAS,uBAAuB,EAAEG,UAAUG,IAAI,CAAC,MAAM,ySAAyS,CAAC;YAEzW;QACF;QAEA,0EAA0E;QAC1E,2DAA2D;QAC3D,MAAMC,UAAUN,QAAQO,OAAO,CAC7B,+BACA,CAAC,IAAI,EAAE9B,YAAY,IAAI,CAAC;QAE1B,IAAI6B,YAAYN,SAAS;YACvBF,UAAUJ,IAAI,CACZ,CAAC,GAAG,EAAEK,SAAS,yRAAyR,CAAC;YAE3S;QACF;QACAlB,KAAK2B,KAAK,CAACT,UAAUO;IACvB;IAEA,MAAM/B,qBAAqBM;IAE3B,OAAO;QAAEiB;IAAU;AACrB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`waf-log-group-removal migration > should add a DESTROY removal policy and import to the rest api WAF log group 1`] = `
|
|
4
|
+
"import { IAspect, Stack, RemovalPolicy } from 'aws-cdk-lib';
|
|
5
|
+
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
|
|
6
|
+
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
7
|
+
import { CfnLoggingConfiguration } from 'aws-cdk-lib/aws-wafv2';
|
|
8
|
+
import { Construct } from 'constructs';
|
|
9
|
+
|
|
10
|
+
export class RestApi extends Construct {
|
|
11
|
+
constructor(scope: Construct, id: string) {
|
|
12
|
+
super(scope, id);
|
|
13
|
+
const logsKey = new Key(this, 'WebAclLogsKey', { enableKeyRotation: true });
|
|
14
|
+
// The construct has another log group sharing the encryptionKey shape, so
|
|
15
|
+
// the migration must scope its rewrite to the WAF log group only.
|
|
16
|
+
const accessLogs = new LogGroup(this, 'AccessLogs', {
|
|
17
|
+
retention: RetentionDays.ONE_YEAR,
|
|
18
|
+
encryptionKey: logsKey,
|
|
19
|
+
});
|
|
20
|
+
const wafLogGroup = new LogGroup(this, 'WebAclLogs', {
|
|
21
|
+
logGroupName: \`aws-waf-logs-\${apiName}-\${this.node.addr.slice(-8)}\`,
|
|
22
|
+
retention: RetentionDays.ONE_YEAR,
|
|
23
|
+
encryptionKey: logsKey,
|
|
24
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
25
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
26
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
"
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
exports[`waf-log-group-removal migration > should add a DESTROY removal policy to the gateway WAF log group 1`] = `
|
|
34
|
+
"import * as cdk from 'aws-cdk-lib';
|
|
35
|
+
import * as kms from 'aws-cdk-lib/aws-kms';
|
|
36
|
+
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
37
|
+
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
|
|
38
|
+
import { Construct } from 'constructs';
|
|
39
|
+
|
|
40
|
+
export class AgentCoreGateway extends Construct {
|
|
41
|
+
private createWebAcl(metricPrefix: string): wafv2.CfnWebACL {
|
|
42
|
+
const logsKey = new kms.Key(this, 'WebAclLogsKey', {
|
|
43
|
+
enableKeyRotation: true,
|
|
44
|
+
});
|
|
45
|
+
const wafLogGroup = new logs.LogGroup(this, 'WebAclLogs', {
|
|
46
|
+
logGroupName: \`aws-waf-logs-\${metricPrefix}-\${this.node.addr.slice(-8)}\`,
|
|
47
|
+
retention: logs.RetentionDays.ONE_YEAR,
|
|
48
|
+
encryptionKey: logsKey,
|
|
49
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
50
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
51
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
52
|
+
});
|
|
53
|
+
return undefined as any;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
"
|
|
57
|
+
`;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/ import { addDestructuredImport, insertViaGritQL, matchGritQL } from "../../../utils/ast.js";
|
|
5
|
+
import { formatFilesInSubtree } from "../../../utils/format.js";
|
|
6
|
+
import { PACKAGES_DIR, SHARED_CONSTRUCTS_DIR } from "../../../utils/shared-constructs-constants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Vended WAF log groups have a deterministic name (WAFv2 requires the
|
|
9
|
+
* `aws-waf-logs-` prefix, so the name can't be CloudFormation-generated) and,
|
|
10
|
+
* without an explicit removal policy, inherit CloudFormation's default RETAIN
|
|
11
|
+
* for log groups. A rolled-back or replaced deploy then orphans the log group,
|
|
12
|
+
* so the next deploy fails to create one of the same name. Set
|
|
13
|
+
* `RemovalPolicy.DESTROY` so it is torn down with the stack, matching the
|
|
14
|
+
* vended user-identity (Cognito) construct.
|
|
15
|
+
*/ const CONSTRUCTS_SRC = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src`;
|
|
16
|
+
const WAF_CONSTRUCTS = [
|
|
17
|
+
{
|
|
18
|
+
file: `${CONSTRUCTS_SRC}/core/agentcore-gateway/agentcore-gateway.ts`,
|
|
19
|
+
logGroupExpr: 'logs.LogGroup',
|
|
20
|
+
removalPolicyExpr: 'cdk.RemovalPolicy.DESTROY'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
file: `${CONSTRUCTS_SRC}/core/api/rest-api.ts`,
|
|
24
|
+
logGroupExpr: 'LogGroup',
|
|
25
|
+
removalPolicyExpr: 'RemovalPolicy.DESTROY',
|
|
26
|
+
destructuredImport: 'RemovalPolicy'
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
const migrateConstruct = async (tree, construct, nextSteps)=>{
|
|
30
|
+
const { file, logGroupExpr } = construct;
|
|
31
|
+
if (!tree.exists(file)) {
|
|
32
|
+
// Construct not vended in this workspace - nothing to migrate.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const contents = tree.read(file, 'utf-8') ?? '';
|
|
36
|
+
// The WAF web ACL (and its log group) is optional in some constructs, so it
|
|
37
|
+
// may be absent. Nothing to do if it is.
|
|
38
|
+
if (!contents.includes(`new ${logGroupExpr}(this, 'WebAclLogs'`)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Already migrated (or user-added a removal policy).
|
|
42
|
+
if (await matchGritQL(tree, file, `\`new ${logGroupExpr}(this, 'WebAclLogs', { $props })\` where { $props <: contains \`removalPolicy\` }`)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Append the removal policy after the WAF log group's `encryptionKey`
|
|
46
|
+
// property. The rewrite is scoped to the `WebAclLogs` LogGroup because a
|
|
47
|
+
// construct can have other log groups sharing the `encryptionKey: logsKey`
|
|
48
|
+
// shape (e.g. the rest api's access logs).
|
|
49
|
+
const added = await insertViaGritQL(tree, file, `\`new ${logGroupExpr}(this, 'WebAclLogs', { $props })\` where {
|
|
50
|
+
$props <: contains \`encryptionKey: logsKey\` as $enc,
|
|
51
|
+
$enc => \`encryptionKey: logsKey,
|
|
52
|
+
__GRIT_INSERT_PLACEHOLDER__\`
|
|
53
|
+
}`, `// The log group name is deterministic, so retaining it on delete would
|
|
54
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
55
|
+
removalPolicy: ${construct.removalPolicyExpr}`);
|
|
56
|
+
if (!added) {
|
|
57
|
+
nextSteps.push(`${file}: could not add a removal policy to the WAF log group automatically - it may have diverged from the generated shape. Add \`removalPolicy: ${construct.removalPolicyExpr}\` to the \`new ${logGroupExpr}(this, 'WebAclLogs', ...)\` call so a rolled-back deploy does not orphan the log group.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (construct.destructuredImport) {
|
|
61
|
+
await addDestructuredImport(tree, file, [
|
|
62
|
+
construct.destructuredImport
|
|
63
|
+
], 'aws-cdk-lib');
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export default async function migration(tree) {
|
|
67
|
+
const nextSteps = [];
|
|
68
|
+
for (const construct of WAF_CONSTRUCTS){
|
|
69
|
+
await migrateConstruct(tree, construct, nextSteps);
|
|
70
|
+
}
|
|
71
|
+
await formatFilesInSubtree(tree);
|
|
72
|
+
return {
|
|
73
|
+
nextSteps
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../packages/nx-plugin/src/migrations/latest/waf-log-group-removal/migration.ts"],"sourcesContent":["/**\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\nimport type { MigrationReturnObject, Tree } from '@nx/devkit';\nimport {\n addDestructuredImport,\n insertViaGritQL,\n matchGritQL,\n} from '../../../utils/ast';\nimport { formatFilesInSubtree } from '../../../utils/format';\nimport {\n PACKAGES_DIR,\n SHARED_CONSTRUCTS_DIR,\n} from '../../../utils/shared-constructs-constants';\n\n/**\n * Vended WAF log groups have a deterministic name (WAFv2 requires the\n * `aws-waf-logs-` prefix, so the name can't be CloudFormation-generated) and,\n * without an explicit removal policy, inherit CloudFormation's default RETAIN\n * for log groups. A rolled-back or replaced deploy then orphans the log group,\n * so the next deploy fails to create one of the same name. Set\n * `RemovalPolicy.DESTROY` so it is torn down with the stack, matching the\n * vended user-identity (Cognito) construct.\n */\n\nconst CONSTRUCTS_SRC = `${PACKAGES_DIR}/${SHARED_CONSTRUCTS_DIR}/src`;\n\n/**\n * A vended construct carrying a WAF log group. They differ in how the log\n * group is constructed and how `cdk` is imported: the gateway namespaces both\n * (`logs.LogGroup`, `cdk.RemovalPolicy`), the rest api destructures both\n * (`LogGroup`, `RemovalPolicy` — the latter added by this migration when\n * absent).\n */\ninterface WafConstruct {\n file: string;\n /** How `LogGroup` is referenced in this file (`LogGroup` or `logs.LogGroup`). */\n logGroupExpr: string;\n /** The removal policy expression, matching the file's import style. */\n removalPolicyExpr: string;\n /** When set, ensure this destructured import from `aws-cdk-lib`. */\n destructuredImport?: string;\n}\n\nconst WAF_CONSTRUCTS: WafConstruct[] = [\n {\n file: `${CONSTRUCTS_SRC}/core/agentcore-gateway/agentcore-gateway.ts`,\n logGroupExpr: 'logs.LogGroup',\n removalPolicyExpr: 'cdk.RemovalPolicy.DESTROY',\n },\n {\n file: `${CONSTRUCTS_SRC}/core/api/rest-api.ts`,\n logGroupExpr: 'LogGroup',\n removalPolicyExpr: 'RemovalPolicy.DESTROY',\n destructuredImport: 'RemovalPolicy',\n },\n];\n\nconst migrateConstruct = async (\n tree: Tree,\n construct: WafConstruct,\n nextSteps: string[],\n): Promise<void> => {\n const { file, logGroupExpr } = construct;\n if (!tree.exists(file)) {\n // Construct not vended in this workspace - nothing to migrate.\n return;\n }\n\n const contents = tree.read(file, 'utf-8') ?? '';\n\n // The WAF web ACL (and its log group) is optional in some constructs, so it\n // may be absent. Nothing to do if it is.\n if (!contents.includes(`new ${logGroupExpr}(this, 'WebAclLogs'`)) {\n return;\n }\n\n // Already migrated (or user-added a removal policy).\n if (\n await matchGritQL(\n tree,\n file,\n `\\`new ${logGroupExpr}(this, 'WebAclLogs', { $props })\\` where { $props <: contains \\`removalPolicy\\` }`,\n )\n ) {\n return;\n }\n\n // Append the removal policy after the WAF log group's `encryptionKey`\n // property. The rewrite is scoped to the `WebAclLogs` LogGroup because a\n // construct can have other log groups sharing the `encryptionKey: logsKey`\n // shape (e.g. the rest api's access logs).\n const added = await insertViaGritQL(\n tree,\n file,\n `\\`new ${logGroupExpr}(this, 'WebAclLogs', { $props })\\` where {\n $props <: contains \\`encryptionKey: logsKey\\` as $enc,\n $enc => \\`encryptionKey: logsKey,\n __GRIT_INSERT_PLACEHOLDER__\\`\n}`,\n `// The log group name is deterministic, so retaining it on delete would\n // orphan it and block re-creation (e.g. after a rolled-back deploy).\n removalPolicy: ${construct.removalPolicyExpr}`,\n );\n\n if (!added) {\n nextSteps.push(\n `${file}: could not add a removal policy to the WAF log group automatically - it may have diverged from the generated shape. Add \\`removalPolicy: ${construct.removalPolicyExpr}\\` to the \\`new ${logGroupExpr}(this, 'WebAclLogs', ...)\\` call so a rolled-back deploy does not orphan the log group.`,\n );\n return;\n }\n\n if (construct.destructuredImport) {\n await addDestructuredImport(\n tree,\n file,\n [construct.destructuredImport],\n 'aws-cdk-lib',\n );\n }\n};\n\nexport default async function migration(\n tree: Tree,\n): Promise<MigrationReturnObject> {\n const nextSteps: string[] = [];\n\n for (const construct of WAF_CONSTRUCTS) {\n await migrateConstruct(tree, construct, nextSteps);\n }\n\n await formatFilesInSubtree(tree);\n\n return { nextSteps };\n}\n"],"names":["addDestructuredImport","insertViaGritQL","matchGritQL","formatFilesInSubtree","PACKAGES_DIR","SHARED_CONSTRUCTS_DIR","CONSTRUCTS_SRC","WAF_CONSTRUCTS","file","logGroupExpr","removalPolicyExpr","destructuredImport","migrateConstruct","tree","construct","nextSteps","exists","contents","read","includes","added","push","migration"],"mappings":"AAAA;;;CAGC,GAED,SACEA,qBAAqB,EACrBC,eAAe,EACfC,WAAW,QACN,wBAAqB;AAC5B,SAASC,oBAAoB,QAAQ,2BAAwB;AAC7D,SACEC,YAAY,EACZC,qBAAqB,QAChB,gDAA6C;AAEpD;;;;;;;;CAQC,GAED,MAAMC,iBAAiB,GAAGF,aAAa,CAAC,EAAEC,sBAAsB,IAAI,CAAC;AAmBrE,MAAME,iBAAiC;IACrC;QACEC,MAAM,GAAGF,eAAe,4CAA4C,CAAC;QACrEG,cAAc;QACdC,mBAAmB;IACrB;IACA;QACEF,MAAM,GAAGF,eAAe,qBAAqB,CAAC;QAC9CG,cAAc;QACdC,mBAAmB;QACnBC,oBAAoB;IACtB;CACD;AAED,MAAMC,mBAAmB,OACvBC,MACAC,WACAC;IAEA,MAAM,EAAEP,IAAI,EAAEC,YAAY,EAAE,GAAGK;IAC/B,IAAI,CAACD,KAAKG,MAAM,CAACR,OAAO;QACtB,+DAA+D;QAC/D;IACF;IAEA,MAAMS,WAAWJ,KAAKK,IAAI,CAACV,MAAM,YAAY;IAE7C,4EAA4E;IAC5E,yCAAyC;IACzC,IAAI,CAACS,SAASE,QAAQ,CAAC,CAAC,IAAI,EAAEV,aAAa,mBAAmB,CAAC,GAAG;QAChE;IACF;IAEA,qDAAqD;IACrD,IACE,MAAMP,YACJW,MACAL,MACA,CAAC,MAAM,EAAEC,aAAa,iFAAiF,CAAC,GAE1G;QACA;IACF;IAEA,sEAAsE;IACtE,yEAAyE;IACzE,2EAA2E;IAC3E,2CAA2C;IAC3C,MAAMW,QAAQ,MAAMnB,gBAClBY,MACAL,MACA,CAAC,MAAM,EAAEC,aAAa;;;;CAIzB,CAAC,EACE,CAAC;;qBAEgB,EAAEK,UAAUJ,iBAAiB,EAAE;IAGlD,IAAI,CAACU,OAAO;QACVL,UAAUM,IAAI,CACZ,GAAGb,KAAK,0IAA0I,EAAEM,UAAUJ,iBAAiB,CAAC,gBAAgB,EAAED,aAAa,uFAAuF,CAAC;QAEzS;IACF;IAEA,IAAIK,UAAUH,kBAAkB,EAAE;QAChC,MAAMX,sBACJa,MACAL,MACA;YAACM,UAAUH,kBAAkB;SAAC,EAC9B;IAEJ;AACF;AAEA,eAAe,eAAeW,UAC5BT,IAAU;IAEV,MAAME,YAAsB,EAAE;IAE9B,KAAK,MAAMD,aAAaP,eAAgB;QACtC,MAAMK,iBAAiBC,MAAMC,WAAWC;IAC1C;IAEA,MAAMZ,qBAAqBU;IAE3B,OAAO;QAAEE;IAAU;AACrB"}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
exports[`py#agent generator > should match snapshot for BedrockAgentCoreRuntime generated constructs files > agent-Dockerfile 1`] = `
|
|
4
4
|
"FROM public.ecr.aws/docker/library/python:3.14-slim
|
|
5
5
|
|
|
6
|
+
# Remove the base image's pip: the agent runs from the bundled environment and
|
|
7
|
+
# never needs it at runtime, and pip's vendored packages carry known
|
|
8
|
+
# HIGH/CRITICAL vulnerabilities flagged by the image scan.
|
|
9
|
+
RUN python -m pip uninstall -y pip && \\
|
|
10
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \\
|
|
11
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info
|
|
12
|
+
|
|
6
13
|
WORKDIR /app
|
|
7
14
|
|
|
8
15
|
# Copy bundled package
|
|
@@ -57,6 +64,8 @@ export class SnapshotBedrockAgent
|
|
|
57
64
|
public readonly agentCoreRuntime: Runtime;
|
|
58
65
|
/** Default Gateway target name for this agent. */
|
|
59
66
|
public readonly agentName = 'snapshot-bedrock-agent';
|
|
67
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
68
|
+
public readonly auth = 'iam';
|
|
60
69
|
|
|
61
70
|
constructor(scope: Construct, id: string, props?: SnapshotBedrockAgentProps) {
|
|
62
71
|
super(scope, id);
|
|
@@ -506,6 +506,14 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
510
|
+
dynamic "request_header_configuration" {
|
|
511
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
512
|
+
content {
|
|
513
|
+
request_header_allowlist = ["Authorization"]
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
509
517
|
network_configuration {
|
|
510
518
|
network_mode = var.enable_vpc ? "VPC" : "PUBLIC"
|
|
511
519
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
FROM <%- pythonBaseImage %>
|
|
2
2
|
|
|
3
|
+
# Remove the base image's pip: the agent runs from the bundled environment and
|
|
4
|
+
# never needs it at runtime, and pip's vendored packages carry known
|
|
5
|
+
# HIGH/CRITICAL vulnerabilities flagged by the image scan.
|
|
6
|
+
RUN python -m pip uninstall -y pip && \
|
|
7
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \
|
|
8
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info
|
|
9
|
+
|
|
3
10
|
WORKDIR /app
|
|
4
11
|
|
|
5
12
|
# Copy bundled package
|
|
@@ -1085,7 +1085,7 @@ import {
|
|
|
1085
1085
|
Stage,
|
|
1086
1086
|
ThrottleSettings,
|
|
1087
1087
|
} from 'aws-cdk-lib/aws-apigateway';
|
|
1088
|
-
import { IAspect, Stack } from 'aws-cdk-lib';
|
|
1088
|
+
import { IAspect, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
1089
1089
|
import {
|
|
1090
1090
|
CfnLoggingConfiguration,
|
|
1091
1091
|
CfnWebACL,
|
|
@@ -1284,6 +1284,9 @@ export class RestApi<
|
|
|
1284
1284
|
logGroupName: \`aws-waf-logs-\${apiName}-\${this.node.addr.slice(-8)}\`,
|
|
1285
1285
|
retention: RetentionDays.ONE_YEAR,
|
|
1286
1286
|
encryptionKey: logsKey,
|
|
1287
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
1288
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
1289
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
1287
1290
|
});
|
|
1288
1291
|
|
|
1289
1292
|
new CfnLoggingConfiguration(this, 'WebAclLoggingConfig', {
|
|
@@ -527,6 +527,14 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
531
|
+
dynamic "request_header_configuration" {
|
|
532
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
533
|
+
content {
|
|
534
|
+
request_header_allowlist = ["Authorization"]
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
530
538
|
network_configuration {
|
|
531
539
|
network_mode = var.enable_vpc ? "VPC" : "PUBLIC"
|
|
532
540
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
FROM <%- pythonBaseImage %>
|
|
2
2
|
|
|
3
|
+
# Remove the base image's pip: the server runs from the bundled environment and
|
|
4
|
+
# never needs it at runtime, and pip's vendored packages carry known
|
|
5
|
+
# HIGH/CRITICAL vulnerabilities flagged by the image scan.
|
|
6
|
+
RUN python -m pip uninstall -y pip && \
|
|
7
|
+
rm -rf /usr/local/lib/python*/site-packages/pip \
|
|
8
|
+
/usr/local/lib/python*/site-packages/pip-*.dist-info
|
|
9
|
+
|
|
3
10
|
WORKDIR /app
|
|
4
11
|
|
|
5
12
|
# Copy bundled package
|
|
@@ -2122,7 +2122,7 @@ import {
|
|
|
2122
2122
|
Stage,
|
|
2123
2123
|
ThrottleSettings,
|
|
2124
2124
|
} from 'aws-cdk-lib/aws-apigateway';
|
|
2125
|
-
import { IAspect, Stack } from 'aws-cdk-lib';
|
|
2125
|
+
import { IAspect, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
2126
2126
|
import {
|
|
2127
2127
|
CfnLoggingConfiguration,
|
|
2128
2128
|
CfnWebACL,
|
|
@@ -2321,6 +2321,9 @@ export class RestApi<
|
|
|
2321
2321
|
logGroupName: \`aws-waf-logs-\${apiName}-\${this.node.addr.slice(-8)}\`,
|
|
2322
2322
|
retention: RetentionDays.ONE_YEAR,
|
|
2323
2323
|
encryptionKey: logsKey,
|
|
2324
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
2325
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
2326
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
2324
2327
|
});
|
|
2325
2328
|
|
|
2326
2329
|
new CfnLoggingConfiguration(this, 'WebAclLoggingConfig', {
|
|
@@ -724,6 +724,8 @@ export class SnapshotBedrockAgent
|
|
|
724
724
|
public readonly agentCoreRuntime: Runtime;
|
|
725
725
|
/** Default Gateway target name for this agent. */
|
|
726
726
|
public readonly agentName = 'snapshot-bedrock-agent';
|
|
727
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
728
|
+
public readonly auth = 'iam';
|
|
727
729
|
|
|
728
730
|
constructor(scope: Construct, id: string, props?: SnapshotBedrockAgentProps) {
|
|
729
731
|
super(scope, id);
|
|
@@ -1633,6 +1635,14 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
|
1633
1635
|
}
|
|
1634
1636
|
}
|
|
1635
1637
|
|
|
1638
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
1639
|
+
dynamic "request_header_configuration" {
|
|
1640
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
1641
|
+
content {
|
|
1642
|
+
request_header_allowlist = ["Authorization"]
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1636
1646
|
network_configuration {
|
|
1637
1647
|
network_mode = var.enable_vpc ? "VPC" : "PUBLIC"
|
|
1638
1648
|
|
|
@@ -2359,6 +2369,8 @@ export class InMemoryAgent
|
|
|
2359
2369
|
public readonly agentCoreRuntime: Runtime;
|
|
2360
2370
|
/** Default Gateway target name for this agent. */
|
|
2361
2371
|
public readonly agentName = 'in-memory-agent';
|
|
2372
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
2373
|
+
public readonly auth = 'iam';
|
|
2362
2374
|
|
|
2363
2375
|
constructor(scope: Construct, id: string, props?: InMemoryAgentProps) {
|
|
2364
2376
|
super(scope, id);
|
|
@@ -527,6 +527,14 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
531
|
+
dynamic "request_header_configuration" {
|
|
532
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
533
|
+
content {
|
|
534
|
+
request_header_allowlist = ["Authorization"]
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
530
538
|
network_configuration {
|
|
531
539
|
network_mode = var.enable_vpc ? "VPC" : "PUBLIC"
|
|
532
540
|
|
|
@@ -69,6 +69,8 @@ export class <%- nameClassName %> extends Construct implements IGrantable, IConn
|
|
|
69
69
|
<%_ } else { _%>
|
|
70
70
|
/** Default Gateway target name for this agent. */
|
|
71
71
|
public readonly agentName = '<%- nameKebabCase %>';
|
|
72
|
+
/** Inbound auth — a fronting Gateway uses this to pick its outbound credential. */
|
|
73
|
+
public readonly auth = '<%- auth %>';
|
|
72
74
|
<%_ } _%>
|
|
73
75
|
|
|
74
76
|
constructor(scope: Construct, id: string, props<%_ if (auth !== 'cognito') { _%>?<%_ } _%>: <%- nameClassName %>Props) {
|
|
@@ -207,6 +209,10 @@ export class <%- nameClassName %> extends Construct implements IGrantable, IConn
|
|
|
207
209
|
identity.userPool,
|
|
208
210
|
[identity.userPoolClient],
|
|
209
211
|
),
|
|
212
|
+
// Receive the caller's Authorization header (validated by the authorizer).
|
|
213
|
+
requestHeaderConfiguration: {
|
|
214
|
+
allowlistedHeaders: ['Authorization'],
|
|
215
|
+
},
|
|
210
216
|
...restProps,
|
|
211
217
|
<%_ } else { _%>
|
|
212
218
|
...props,
|
|
@@ -266,6 +266,9 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
266
266
|
logGroupName: `aws-waf-logs-${metricPrefix}-${this.node.addr.slice(-8)}`,
|
|
267
267
|
retention: logs.RetentionDays.ONE_YEAR,
|
|
268
268
|
encryptionKey: logsKey,
|
|
269
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
270
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
271
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
269
272
|
});
|
|
270
273
|
|
|
271
274
|
new wafv2.CfnLoggingConfiguration(this, 'WebAclLoggingConfig', {
|
|
@@ -458,6 +461,7 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
458
461
|
agent: Construct & {
|
|
459
462
|
readonly agentCoreRuntime: agentcore.Runtime;
|
|
460
463
|
readonly agentName: string;
|
|
464
|
+
readonly auth?: 'iam' | 'cognito';
|
|
461
465
|
},
|
|
462
466
|
props?: {
|
|
463
467
|
gatewayTargetName?: string;
|
|
@@ -466,16 +470,22 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
466
470
|
return this.addAgentTarget({
|
|
467
471
|
gatewayTargetName: props?.gatewayTargetName ?? agent.agentName,
|
|
468
472
|
agentRuntimeArn: agent.agentCoreRuntime.agentRuntimeArn,
|
|
473
|
+
credentials: agent.auth === 'cognito' ? 'jwt-passthrough' : 'gateway-iam',
|
|
469
474
|
});
|
|
470
475
|
}
|
|
471
476
|
|
|
472
477
|
/**
|
|
473
478
|
* Register an agent runtime ARN as a Gateway target. Prefer
|
|
474
479
|
* {@link addAgent} when the construct is in scope.
|
|
480
|
+
*
|
|
481
|
+
* `credentials` selects the outbound authorization to the runtime:
|
|
482
|
+
* `gateway-iam` (default) signs with the gateway role; `jwt-passthrough`
|
|
483
|
+
* forwards the caller's JWT so the runtime authorizes on it.
|
|
475
484
|
*/
|
|
476
485
|
public addAgentTarget(props: {
|
|
477
486
|
gatewayTargetName: string;
|
|
478
487
|
agentRuntimeArn: string;
|
|
488
|
+
credentials?: 'gateway-iam' | 'jwt-passthrough';
|
|
479
489
|
}): agentcore.CfnGatewayTarget {
|
|
480
490
|
if (this.protocol !== 'http') {
|
|
481
491
|
throw new Error(
|
|
@@ -500,11 +510,12 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
500
510
|
},
|
|
501
511
|
},
|
|
502
512
|
},
|
|
503
|
-
// The bare gateway IAM role credential: runtime targets reject the
|
|
504
|
-
// iamCredentialProvider detail MCP targets carry.
|
|
505
513
|
credentialProviderConfigurations: [
|
|
506
514
|
{
|
|
507
|
-
credentialProviderType:
|
|
515
|
+
credentialProviderType:
|
|
516
|
+
props.credentials === 'jwt-passthrough'
|
|
517
|
+
? 'JWT_PASSTHROUGH'
|
|
518
|
+
: 'GATEWAY_IAM_ROLE',
|
|
508
519
|
},
|
|
509
520
|
],
|
|
510
521
|
},
|
|
@@ -376,6 +376,14 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
|
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
# Receive the caller's Authorization header (validated by the JWT authorizer).
|
|
380
|
+
dynamic "request_header_configuration" {
|
|
381
|
+
for_each = var.authorizer_configuration != null && try(var.authorizer_configuration.custom_jwt_authorizer, null) != null ? [1] : []
|
|
382
|
+
content {
|
|
383
|
+
request_header_allowlist = ["Authorization"]
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
379
387
|
network_configuration {
|
|
380
388
|
network_mode = var.enable_vpc ? "VPC" : "PUBLIC"
|
|
381
389
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Stage,
|
|
11
11
|
ThrottleSettings,
|
|
12
12
|
} from 'aws-cdk-lib/aws-apigateway';
|
|
13
|
-
import { IAspect, Stack } from 'aws-cdk-lib';
|
|
13
|
+
import { IAspect, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
14
14
|
import {
|
|
15
15
|
CfnLoggingConfiguration,
|
|
16
16
|
CfnWebACL,
|
|
@@ -209,6 +209,9 @@ export class RestApi<
|
|
|
209
209
|
logGroupName: `aws-waf-logs-${apiName}-${this.node.addr.slice(-8)}`,
|
|
210
210
|
retention: RetentionDays.ONE_YEAR,
|
|
211
211
|
encryptionKey: logsKey,
|
|
212
|
+
// The log group name is deterministic, so retaining it on delete would
|
|
213
|
+
// orphan it and block re-creation (e.g. after a rolled-back deploy).
|
|
214
|
+
removalPolicy: RemovalPolicy.DESTROY,
|
|
212
215
|
});
|
|
213
216
|
|
|
214
217
|
new CfnLoggingConfiguration(this, 'WebAclLoggingConfig', {
|