@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.
Files changed (32) hide show
  1. package/migrations.json +17 -2
  2. package/package.json +1 -1
  3. package/src/agentcore-gateway/agent-connection/generator.js +7 -2
  4. package/src/agentcore-gateway/agent-connection/generator.js.map +1 -1
  5. package/src/migrations/latest/gateway-cognito-passthrough/__snapshots__/migration.spec.ts.snap +149 -0
  6. package/src/migrations/latest/gateway-cognito-passthrough/metadata.json +3 -0
  7. package/src/migrations/latest/gateway-cognito-passthrough/migration.d.ts +6 -0
  8. package/src/migrations/latest/gateway-cognito-passthrough/migration.js +195 -0
  9. package/src/migrations/latest/gateway-cognito-passthrough/migration.js.map +1 -0
  10. package/src/migrations/latest/strip-pip-from-python-images/__snapshots__/migration.spec.ts.snap +48 -0
  11. package/src/migrations/latest/strip-pip-from-python-images/metadata.json +3 -0
  12. package/src/migrations/latest/strip-pip-from-python-images/migration.d.ts +6 -0
  13. package/src/migrations/latest/strip-pip-from-python-images/migration.js +91 -0
  14. package/src/migrations/latest/strip-pip-from-python-images/migration.js.map +1 -0
  15. package/src/migrations/latest/waf-log-group-removal/__snapshots__/migration.spec.ts.snap +57 -0
  16. package/src/migrations/latest/waf-log-group-removal/metadata.json +3 -0
  17. package/src/migrations/latest/waf-log-group-removal/migration.d.ts +6 -0
  18. package/src/migrations/latest/waf-log-group-removal/migration.js +77 -0
  19. package/src/migrations/latest/waf-log-group-removal/migration.js.map +1 -0
  20. package/src/py/agent/__snapshots__/generator.constructs.spec.ts.snap +9 -0
  21. package/src/py/agent/__snapshots__/generator.protocols.spec.ts.snap +8 -0
  22. package/src/py/agent/files/deploy/Dockerfile.template +7 -0
  23. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +4 -1
  24. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +8 -0
  25. package/src/py/mcp-server/files/deploy/Dockerfile.template +7 -0
  26. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +4 -1
  27. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +12 -0
  28. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +8 -0
  29. package/src/utils/agent-core-constructs/files/cdk/app/agent-core/__nameKebabCase__/__nameKebabCase__.ts.template +6 -0
  30. package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +14 -3
  31. package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +8 -0
  32. 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.59",
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.59",
78
+ "version": "1.0.0-rc.60",
64
79
  "packages": {
65
80
  "nx": {
66
81
  "version": "23.1.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.59",
3
+ "version": "1.0.0-rc.60",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -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
- if (agentComponent.auth && agentComponent.auth !== 'iam') {
59
- throw new Error(`Agent '${agentComponent.name}' uses auth='${agentComponent.auth}'. Gateway->agent connections currently require the agent to use IAM authentication.`);
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 if (agentComponent.auth && agentComponent.auth !== 'iam') {\n throw new Error(\n `Agent '${agentComponent.name}' uses auth='${agentComponent.auth}'. Gateway->agent connections currently require the agent to use IAM authentication.`,\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","auth","agentComponentName","agentTargetName","rc","targetName","port","upstreamProjectName","upstreamDevTargetName","stripInvocations","root","preferInstallDependencies","languages"],"mappings":"AAAA;;;CAGC,GAED,SAASA,oBAAoB,QAAQ,wBAAqB;AAC1D,SAASC,mBAAmB,QAAQ,yBAAsB;AAC1D,SAASC,+BAA+B,QAAQ,yBAAsB;AACtE,SAASC,SAAS,EAAEC,WAAW,QAAQ,uBAAoB;AAC3D,SACEC,6BAA6B,EAC7BC,gBAAgB,EAEhBC,mCAAmC,QAC9B,oBAAiB;AACxB,SAASC,yBAAyB,QAAQ,wBAAqB;AAC/D,SAASC,4BAA4B,QAAQ,kBAAe;AAG5D,OAAO,MAAMC,oDACXJ,iBAAiB,YAAYK,QAAQ,EAAE;AAEzC;;;;;;;;;;CAUC,GACD,MAAMC,4BAAsD;IAC1D,YAAY;QAAC;QAAS;KAAM;IAC5B,YAAY;QAAC;QAAS;QAAQ;KAAM;AACtC;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,2CAA2C,OACtDC,MACAC;IAEA,MAAMC,gBAAgBT,oCACpBO,MACAC,QAAQC,aAAa;IAEvB,MAAMC,gBAAgBV,oCACpBO,MACAC,QAAQE,aAAa;IAGvB,MAAMC,UAAUT,6BAA6BO;IAC7C,MAAMG,iBAAiBJ,QAAQK,eAAe;IAE9C,IAAI,CAACD,gBAAgB;QACnB,MAAM,IAAIE,MACR,CAAC,gBAAgB,EAAEN,QAAQE,aAAa,CAAC,sFAAsF,CAAC;IAEpI;IAEA,IAAIC,QAAQI,QAAQ,KAAK,QAAQ;QAC/B,MAAM,IAAID,MACR,CAAC,SAAS,EAAEH,QAAQK,IAAI,CAAC,gBAAgB,EAAEL,QAAQI,QAAQ,CAAC,0IAA0I,CAAC;IAE3M;IAEA,MAAME,iBAAiBL,eAAeM,SAAS,IAAI;IACnD,MAAMC,gBAAgB,AACpB,CAAA,AAACP,eAAeG,QAAQ,IAA2B,MAAK,EACxDK,WAAW;IACb,MAAMC,qBAAqBhB,yBAAyB,CAACY,eAAe,IAAI,EAAE;IAC1E,IAAI,CAACI,mBAAmBC,QAAQ,CAACH,gBAAgB;QAC/C,MAAM,IAAIL,MACR,CAAC,OAAO,EAAEF,eAAeI,IAAI,CAAC,iBAAiB,EAAEG,cAAc,uCAAuC,CAAC,GACpGF,CAAAA,mBAAmB,cAAcE,kBAAkB,SAChD,CAAC,uJAAuJ,CAAC,GACzJ,CAAC,uBAAuB,EAAEE,mBAAmBE,IAAI,CAAC,MAAM,CAAC,CAAC,AAAD;IAEnE;IACA,IAAIX,eAAeY,IAAI,IAAIZ,eAAeY,IAAI,KAAK,OAAO;QACxD,MAAM,IAAIV,MACR,CAAC,OAAO,EAAEF,eAAeI,IAAI,CAAC,aAAa,EAAEJ,eAAeY,IAAI,CAAC,oFAAoF,CAAC;IAE1J;IAEA,4EAA4E;IAC5E,qEAAqE;IACrE,0EAA0E;IAC1E,MAAMC,qBAAqBb,eAAeI,IAAI,IAAI;IAClD,MAAMU,kBAAkB9B,UACtB,AAACgB,eAAee,EAAE,IAChB9B,YAAY4B;IAGhB,MAAMxB,0BAA0BM,MAAME,eAAe,OAAO;QAC1DmB,YAAYF;QACZG,MAAM,AAACjB,eAAeiB,IAAI,IAA2B;QACrDC,qBAAqBpB,cAAcM,IAAI;QACvCe,uBAAuB,GAAGN,mBAAmB,IAAI,CAAC;QAClD,0EAA0E;QAC1E,uEAAuE;QACvE,cAAc;QACdO,kBAAkBb,kBAAkB;IACtC;IAEA,6DAA6D;IAC7DrB,8BACES,MACAE,cAAcO,IAAI,EAClBb,mDACAO,cAAcuB,IAAI,EAClBP;IAGF,MAAM/B,gCAAgCY,MAAM;QAC1CJ;KACD;IAED,MAAMV,qBAAqBc;IAE3B,OAAO,IACLb,oBAAoBa,MAAMC,QAAQ0B,yBAAyB,EAAE;YAC3DC,WAAW;gBAAC;aAAa;QAC3B;AACJ,EAAE;AAEF,eAAe7B,yCAAyC"}
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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"}
@@ -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,3 @@
1
+ {
2
+ "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"
3
+ }
@@ -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, Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -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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"}
@@ -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,3 @@
1
+ {
2
+ "description": "Remove pip from vended Python agent / MCP server images so its vulnerable vendored packages are not present at runtime"
3
+ }
@@ -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,3 @@
1
+ {
2
+ "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"
3
+ }
@@ -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, Tree } from '@nx/devkit';
6
+ export default function migration(tree: Tree): Promise<MigrationReturnObject>;
@@ -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: 'GATEWAY_IAM_ROLE',
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', {