@aws/nx-plugin 1.0.0-rc.11 → 1.0.0-rc.13

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 (105) hide show
  1. package/LICENSE-THIRD-PARTY +81 -3
  2. package/generators.json +31 -1
  3. package/package.json +2 -2
  4. package/sdk/agentcore-gateway.d.ts +6 -0
  5. package/sdk/agentcore-gateway.js +11 -0
  6. package/sdk/agentcore-gateway.js.map +1 -0
  7. package/sdk/license.d.ts +4 -4
  8. package/sdk/license.js +5 -4
  9. package/sdk/license.js.map +1 -1
  10. package/sdk/py.d.ts +6 -6
  11. package/sdk/py.js +7 -7
  12. package/sdk/py.js.map +1 -1
  13. package/sdk/ts.d.ts +18 -14
  14. package/sdk/ts.js +38 -32
  15. package/sdk/ts.js.map +1 -1
  16. package/src/agentcore-gateway/files/cedar/policies/README.md +52 -0
  17. package/src/agentcore-gateway/files/cedar/policies/permit-all.cedar +11 -0
  18. package/src/agentcore-gateway/files/project/serve-local.ts.template +133 -0
  19. package/src/agentcore-gateway/generator.d.ts +25 -0
  20. package/src/agentcore-gateway/generator.js +129 -0
  21. package/src/agentcore-gateway/generator.js.map +1 -0
  22. package/src/agentcore-gateway/mcp-connection/generator.d.ts +19 -0
  23. package/src/agentcore-gateway/mcp-connection/generator.js +79 -0
  24. package/src/agentcore-gateway/mcp-connection/generator.js.map +1 -0
  25. package/src/agentcore-gateway/mcp-connection/schema.d.ts +12 -0
  26. package/src/agentcore-gateway/mcp-connection/schema.json +26 -0
  27. package/src/agentcore-gateway/schema.d.ts +16 -0
  28. package/src/agentcore-gateway/schema.json +65 -0
  29. package/src/connection/generator.d.ts +1 -1
  30. package/src/connection/generator.js +64 -46
  31. package/src/connection/generator.js.map +1 -1
  32. package/src/infra/app/__snapshots__/generator.spec.ts.snap +9 -9
  33. package/src/mcp-server/schema-registry.d.ts +9 -3
  34. package/src/mcp-server/schema-registry.js +22 -6
  35. package/src/mcp-server/schema-registry.js.map +1 -1
  36. package/src/preset/__snapshots__/generator.spec.ts.snap +3 -1
  37. package/src/py/agent/__snapshots__/generator.spec.ts.snap +629 -7
  38. package/src/py/agent/a2a-connection/files/agent-connection/app/__targetAgentSnakeCase___client.py.template +9 -3
  39. package/src/py/agent/a2a-connection/generator.js +1 -1
  40. package/src/py/agent/a2a-connection/generator.js.map +1 -1
  41. package/src/py/agent/files/http/init.py.template +5 -23
  42. package/src/py/agent/gateway-connection/files/agent-connection/app/__gatewaySnakeCase___client.py.template +35 -0
  43. package/src/py/agent/gateway-connection/generator.d.ts +10 -0
  44. package/src/py/agent/gateway-connection/generator.js +106 -0
  45. package/src/py/agent/gateway-connection/generator.js.map +1 -0
  46. package/src/py/agent/gateway-connection/schema.d.ts +12 -0
  47. package/src/py/agent/gateway-connection/schema.json +26 -0
  48. package/src/py/agent/generator.js +27 -22
  49. package/src/py/agent/generator.js.map +1 -1
  50. package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +8 -4
  51. package/src/py/agent/mcp-connection/files/agent-connection/app/__mcpServerSnakeCase___client.py.template +9 -3
  52. package/src/py/agent/mcp-connection/generator.js +2 -72
  53. package/src/py/agent/mcp-connection/generator.js.map +1 -1
  54. package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +5 -22
  55. package/src/py/fast-api/files/app/__name__/init.py.template +5 -22
  56. package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +58 -3
  57. package/src/ts/agent/__snapshots__/generator.spec.ts.snap +619 -2
  58. package/src/ts/agent/a2a-connection/files/agent-connection/app/__targetAgentKebabCase__-client.ts.template +10 -6
  59. package/src/ts/agent/gateway-connection/files/agent-connection/app/__gatewayKebabCase__-client.ts.template +30 -0
  60. package/src/ts/agent/gateway-connection/generator.d.ts +10 -0
  61. package/src/ts/agent/gateway-connection/generator.js +106 -0
  62. package/src/ts/agent/gateway-connection/generator.js.map +1 -0
  63. package/src/ts/agent/gateway-connection/schema.d.ts +12 -0
  64. package/src/ts/agent/gateway-connection/schema.json +26 -0
  65. package/src/ts/agent/generator.js +23 -19
  66. package/src/ts/agent/generator.js.map +1 -1
  67. package/src/ts/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +9 -4
  68. package/src/ts/agent/mcp-connection/files/agent-connection/app/__mcpServerKebabCase__-client.ts.template +9 -6
  69. package/src/ts/agent/mcp-connection/generator.js +1 -13
  70. package/src/ts/agent/mcp-connection/generator.js.map +1 -1
  71. package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +58 -3
  72. package/src/ts/nx-generator/generator.js +29 -0
  73. package/src/ts/nx-generator/generator.js.map +1 -1
  74. package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +86 -39
  75. package/src/utils/agent-chat/agent-chat.d.ts +31 -0
  76. package/src/utils/agent-chat/agent-chat.js +30 -0
  77. package/src/utils/agent-chat/agent-chat.js.map +1 -0
  78. package/src/utils/agent-chat/files/a2a/chat.ts.template +33 -0
  79. package/src/utils/agent-chat/files/ag-ui/chat.ts.template +17 -0
  80. package/src/utils/agent-chat/files/common/agentcore.ts.template +81 -0
  81. package/src/utils/agent-chat/files/http-py/chat.ts.template +43 -0
  82. package/src/utils/agent-chat/files/http-ts/chat.ts.template +54 -0
  83. package/src/utils/agent-connection/agent-connection.d.ts +17 -0
  84. package/src/utils/agent-connection/agent-connection.js +94 -0
  85. package/src/utils/agent-connection/agent-connection.js.map +1 -1
  86. package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-client.ts.template +78 -0
  87. package/src/utils/agent-connection/files/core-runtime-config/runtime-config.ts.template +22 -32
  88. package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_client.py.template +78 -0
  89. package/src/utils/agent-connection/files/py-core-runtime-config/runtime_config.py.template +5 -18
  90. package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +8 -0
  91. package/src/utils/agent-core-constructs/agent-core-constructs.js +54 -1
  92. package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
  93. package/src/utils/agent-core-constructs/files/cdk/app/agent-core/__nameKebabCase__/__nameKebabCase__.ts.template +4 -0
  94. package/src/utils/agent-core-constructs/files/cdk/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.ts.template +37 -0
  95. package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +293 -0
  96. package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/readiness-probe/index.js.template +55 -0
  97. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.tf.template +242 -0
  98. package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/render-cedar.cjs.template +13 -0
  99. package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +55 -2
  100. package/src/utils/agent-core-constructs/files/terraform/core/agentcore-gateway/gateway.tf.template +14 -0
  101. package/src/utils/versions.d.ts +46 -44
  102. package/src/utils/versions.js +45 -43
  103. package/src/utils/versions.js.map +1 -1
  104. package/src/py/agent/scripts/http/chat.ts.template +0 -38
  105. package/src/ts/agent/scripts/http/chat.ts.template +0 -35
@@ -0,0 +1,293 @@
1
+ import * as cdk from 'aws-cdk-lib';
2
+ import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore';
3
+ import * as iam from 'aws-cdk-lib/aws-iam';
4
+ import * as lambda from 'aws-cdk-lib/aws-lambda';
5
+ import * as triggers from 'aws-cdk-lib/triggers';
6
+ import { Construct } from 'constructs';
7
+ import ejs from 'ejs';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as url from 'node:url';
11
+
12
+ /**
13
+ * Props for the AgentCoreGateway construct.
14
+ */
15
+ export interface AgentCoreGatewayProps {
16
+ /**
17
+ * Directory of Cedar policy files (`.cedar`, nested folders supported).
18
+ * When set, a `PolicyEngine` running in `ENFORCE` mode is attached to the
19
+ * gateway, with one `Policy` per file. Files are EJS templates rendered at
20
+ * synth time with `gatewayArn` and `accountId` in scope.
21
+ */
22
+ readonly cedarPolicyPath?: string;
23
+ /**
24
+ * Additional variables available to the Cedar policy EJS templates.
25
+ */
26
+ readonly cedarPolicyVariables?: Record<string, string>;
27
+ }
28
+
29
+ /**
30
+ * AgentCore Gateway wired for MCP protocol with IAM inbound auth, exposing
31
+ * attached MCP servers' tools under `<targetName>___<toolName>` names.
32
+ */
33
+ export class AgentCoreGateway extends Construct implements iam.IGrantable {
34
+ public readonly gateway: agentcore.Gateway;
35
+ public readonly policyEngine?: agentcore.CfnPolicyEngine;
36
+ private readonly policies: agentcore.CfnPolicy[] = [];
37
+ private targetReadinessProbe?: triggers.TriggerFunction;
38
+ private readonly targetRuntimeArns: string[] = [];
39
+
40
+ constructor(scope: Construct, id: string, props?: AgentCoreGatewayProps) {
41
+ super(scope, id);
42
+
43
+ this.gateway = new agentcore.Gateway(this, 'Gateway', {
44
+ protocolConfiguration: new agentcore.McpProtocolConfiguration({
45
+ searchType: agentcore.McpGatewaySearchType.SEMANTIC,
46
+ supportedVersions: [agentcore.MCPProtocolVersion.MCP_2025_03_26],
47
+ }),
48
+ authorizerConfiguration: agentcore.GatewayAuthorizer.usingAwsIam(),
49
+ });
50
+
51
+ if (props?.cedarPolicyPath) {
52
+ this.policyEngine = new agentcore.CfnPolicyEngine(this, 'PolicyEngine', {
53
+ name: cdk.Names.uniqueResourceName(this, { maxLength: 48 }),
54
+ description: `Cedar policy engine for ${id}`,
55
+ });
56
+
57
+ // The stable Gateway L2 does not yet expose policy engine
58
+ // configuration, so attach it via the underlying CloudFormation
59
+ // resource.
60
+ const cfnGateway = this.gateway.node.defaultChild as agentcore.CfnGateway;
61
+ cfnGateway.policyEngineConfiguration = {
62
+ arn: this.policyEngine.attrPolicyEngineArn,
63
+ mode: 'ENFORCE',
64
+ };
65
+
66
+ // Permissions the gateway role needs to evaluate policies, including
67
+ // CheckAuthorizePermissions, which the CloudFormation handler's
68
+ // readiness probe requires during gateway creation. Authorization
69
+ // actions are evaluated against both the policy engine ARN and the
70
+ // gateway's own ARN (which cannot be referenced here without a circular
71
+ // dependency, so it is scoped to this account and region's gateways).
72
+ // The gateway must depend on the policy update — access is validated at
73
+ // creation.
74
+ const policyGrant = this.gateway.role.addToPrincipalPolicy(
75
+ new iam.PolicyStatement({
76
+ actions: [
77
+ 'bedrock-agentcore:GetPolicyEngine',
78
+ 'bedrock-agentcore:AuthorizeAction',
79
+ 'bedrock-agentcore:PartiallyAuthorizeActions',
80
+ 'bedrock-agentcore:CheckAuthorizePermissions',
81
+ ],
82
+ resources: [
83
+ this.policyEngine.attrPolicyEngineArn,
84
+ cdk.Stack.of(this).formatArn({
85
+ service: 'bedrock-agentcore',
86
+ resource: 'gateway',
87
+ resourceName: '*',
88
+ }),
89
+ ],
90
+ }),
91
+ );
92
+ if (policyGrant.policyDependable) {
93
+ cfnGateway.node.addDependency(policyGrant.policyDependable);
94
+ }
95
+
96
+ const policiesDir = props.cedarPolicyPath;
97
+ if (fs.existsSync(policiesDir)) {
98
+ for (const file of fs
99
+ .readdirSync(policiesDir, { recursive: true, encoding: 'utf-8' })
100
+ .filter((f) => f.endsWith('.cedar'))) {
101
+ const raw = fs.readFileSync(path.join(policiesDir, file), 'utf-8');
102
+ const statement = cdk.Lazy.string({
103
+ produce: () =>
104
+ ejs.render(raw, {
105
+ gatewayArn: cdk.Token.asString(this.gateway.gatewayArn),
106
+ accountId: cdk.Token.asString(cdk.Stack.of(this).account),
107
+ ...props.cedarPolicyVariables,
108
+ }),
109
+ });
110
+ const policyId = `Policy-${file.replace(/\.cedar$/, '').replace(/[\\/]/g, '-')}`;
111
+ const policy = new agentcore.CfnPolicy(this, policyId, {
112
+ name: policyId,
113
+ policyEngineId: this.policyEngine.attrPolicyEngineId,
114
+ definition: { cedar: { statement } },
115
+ description: `Policy loaded from ${file}`,
116
+ // The Cedar analyzer rejects legitimate policies as findings
117
+ // (e.g. a forbid disabling one tool for every caller is "Overly
118
+ // Restrictive"), so deploy regardless and rely on the engine's
119
+ // ENFORCE mode.
120
+ validationMode: 'IGNORE_ALL_FINDINGS',
121
+ });
122
+ // Policy names are unique account-wide, so derive the name from
123
+ // the construct path to avoid collisions with other gateways.
124
+ policy.name = cdk.Names.uniqueResourceName(policy, {
125
+ maxLength: 48,
126
+ });
127
+ this.policies.push(policy);
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * The principal to grant permissions to.
135
+ */
136
+ public get grantPrincipal(): iam.IPrincipal {
137
+ return this.gateway.role.grantPrincipal;
138
+ }
139
+
140
+ /**
141
+ * Grants an IAM principal permission to invoke this gateway.
142
+ */
143
+ public grantInvokeAccess(grantee: iam.IGrantable) {
144
+ this.gateway.grantInvoke(grantee);
145
+ }
146
+
147
+ /**
148
+ * Add an MCP server construct as a target of this Gateway.
149
+ *
150
+ * The target name defaults to the MCP server construct's `mcpServerName`
151
+ * (its project's class name in kebab-case, e.g. `TsMcp` -> `ts-mcp`),
152
+ * which is stable across stacks and unique across projects in the
153
+ * workspace. It prefixes Cedar action names
154
+ * (`AgentCore::Action::"<targetName>___<toolName>"`) and the gateway's
155
+ * tool names (`<targetName>___<toolName>`), so renaming it later is a
156
+ * breaking change for consumers.
157
+ */
158
+ public addMcpServer(
159
+ server: Construct & {
160
+ readonly agentCoreRuntime: agentcore.Runtime;
161
+ readonly mcpServerName: string;
162
+ },
163
+ props?: {
164
+ gatewayTargetName?: string;
165
+ },
166
+ ): agentcore.CfnGatewayTarget {
167
+ return this.addMcpServerTarget({
168
+ gatewayTargetName: props?.gatewayTargetName ?? server.mcpServerName,
169
+ mcpServerRuntimeArn: server.agentCoreRuntime.agentRuntimeArn,
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Register an MCP server runtime ARN as a Gateway target. Prefer
175
+ * {@link addMcpServer} when the construct is in scope.
176
+ */
177
+ public addMcpServerTarget(props: {
178
+ gatewayTargetName: string;
179
+ mcpServerRuntimeArn: string;
180
+ }): agentcore.CfnGatewayTarget {
181
+ const region = cdk.Stack.of(this).region;
182
+ // The endpoint must URL-encode the runtime ARN (':' -> '%3A', '/' -> '%2F').
183
+ // The ARN is a CDK token, so encode at deploy time via Fn.join/Fn.split.
184
+ const arn = props.mcpServerRuntimeArn;
185
+ const encodedArn = cdk.Fn.join(
186
+ '%2F',
187
+ cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', arn))),
188
+ );
189
+ const endpoint = `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations?qualifier=DEFAULT`;
190
+ // The construct id is derived from the target name so the two change
191
+ // together — target names are unique per gateway, and replacing a target
192
+ // under an unchanged name fails with AlreadyExists.
193
+ const target = new agentcore.CfnGatewayTarget(
194
+ this,
195
+ `Target-${props.gatewayTargetName}`,
196
+ {
197
+ gatewayIdentifier: this.gateway.gatewayId,
198
+ name: props.gatewayTargetName,
199
+ targetConfiguration: {
200
+ mcp: {
201
+ mcpServer: {
202
+ endpoint,
203
+ },
204
+ },
205
+ },
206
+ credentialProviderConfigurations: [
207
+ {
208
+ credentialProviderType: 'GATEWAY_IAM_ROLE',
209
+ credentialProvider: {
210
+ iamCredentialProvider: {
211
+ service: 'bedrock-agentcore',
212
+ },
213
+ },
214
+ },
215
+ ],
216
+ },
217
+ );
218
+
219
+ // Grant the gateway role invoke access to the target runtime before the
220
+ // target is created — AgentCore probes the target during creation, which
221
+ // stalls if the permission is not yet in place.
222
+ const grant = this.gateway.role.addToPrincipalPolicy(
223
+ new iam.PolicyStatement({
224
+ actions: [
225
+ 'bedrock-agentcore:InvokeAgentRuntime',
226
+ 'bedrock-agentcore:InvokeAgentRuntimeWithWebSocketStream',
227
+ ],
228
+ resources: [
229
+ props.mcpServerRuntimeArn,
230
+ `${props.mcpServerRuntimeArn}/*`,
231
+ ],
232
+ }),
233
+ );
234
+ if (grant.policyDependable) {
235
+ target.node.addDependency(grant.policyDependable);
236
+ }
237
+
238
+ // Create the target only after the readiness probe has confirmed the
239
+ // runtime serves MCP.
240
+ this.targetRuntimeArns.push(props.mcpServerRuntimeArn);
241
+ const probe = this.getOrCreateTargetReadinessProbe();
242
+ const probeGrant = probe.grantPrincipal.addToPrincipalPolicy(
243
+ new iam.PolicyStatement({
244
+ actions: ['bedrock-agentcore:InvokeAgentRuntime'],
245
+ resources: [
246
+ props.mcpServerRuntimeArn,
247
+ `${props.mcpServerRuntimeArn}/*`,
248
+ ],
249
+ }),
250
+ );
251
+ if (probeGrant.policyDependable) {
252
+ probe.trigger.node.addDependency(probeGrant.policyDependable);
253
+ }
254
+ target.node.addDependency(probe.trigger);
255
+
256
+ // Policies referencing this target's actions (e.g.
257
+ // AgentCore::Action::"<targetName>___<tool>") only validate once the
258
+ // target has registered them, so create policies after targets.
259
+ for (const policy of this.policies) {
260
+ policy.node.addDependency(target);
261
+ }
262
+
263
+ return target;
264
+ }
265
+
266
+ /**
267
+ * AgentCore fetches each target's tools during target creation, which can
268
+ * race the MCP runtime's first cold start on a fresh deploy and fail the
269
+ * target. This trigger probes each runtime until it serves MCP, and targets
270
+ * are created after it succeeds.
271
+ */
272
+ private getOrCreateTargetReadinessProbe(): triggers.TriggerFunction {
273
+ this.targetReadinessProbe ??= new triggers.TriggerFunction(
274
+ this,
275
+ 'TargetReadinessProbe',
276
+ {
277
+ runtime: lambda.Runtime.NODEJS_22_X,
278
+ handler: 'index.handler',
279
+ timeout: cdk.Duration.seconds(210),
280
+ code: lambda.Code.fromAsset(
281
+ url.fileURLToPath(new URL('./readiness-probe', import.meta.url)),
282
+ ),
283
+ environment: {
284
+ RUNTIME_ARNS: cdk.Lazy.string({
285
+ produce: () => this.targetRuntimeArns.join(','),
286
+ }),
287
+ },
288
+ },
289
+ );
290
+ return this.targetReadinessProbe;
291
+ }
292
+ }
293
+
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Readiness probe for AgentCore Gateway MCP targets.
3
+ *
4
+ * AgentCore fetches each target's tools during target creation, which can
5
+ * race the MCP runtime's first cold start on a fresh deploy. This handler
6
+ * invokes each runtime with an MCP initialize request — the invocation
7
+ * blocks through the cold start — retrying for up to 3 minutes.
8
+ */
9
+ const {
10
+ BedrockAgentCoreClient,
11
+ InvokeAgentRuntimeCommand,
12
+ } = require('@aws-sdk/client-bedrock-agentcore');
13
+
14
+ const client = new BedrockAgentCoreClient();
15
+
16
+ const DEADLINE_MS = 3 * 60 * 1000;
17
+ const RETRY_INTERVAL_MS = 5 * 1000;
18
+
19
+ const INITIALIZE_REQUEST = JSON.stringify({
20
+ jsonrpc: '2.0',
21
+ id: 1,
22
+ method: 'initialize',
23
+ params: {
24
+ protocolVersion: '2025-03-26',
25
+ capabilities: {},
26
+ clientInfo: { name: 'readiness-probe', version: '1.0' },
27
+ },
28
+ });
29
+
30
+ const probe = async (agentRuntimeArn) => {
31
+ const deadline = Date.now() + DEADLINE_MS;
32
+ for (;;) {
33
+ try {
34
+ return await client.send(
35
+ new InvokeAgentRuntimeCommand({
36
+ agentRuntimeArn,
37
+ runtimeSessionId: 'readiness-probe-' + crypto.randomUUID(),
38
+ contentType: 'application/json',
39
+ accept: 'application/json, text/event-stream',
40
+ payload: Buffer.from(INITIALIZE_REQUEST),
41
+ }),
42
+ );
43
+ } catch (e) {
44
+ if (Date.now() >= deadline) {
45
+ throw e;
46
+ }
47
+ await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS));
48
+ }
49
+ }
50
+ };
51
+
52
+ exports.handler = async () => {
53
+ const arns = (process.env.RUNTIME_ARNS ?? '').split(',').filter(Boolean);
54
+ await Promise.all(arns.map(probe));
55
+ };
@@ -0,0 +1,242 @@
1
+ terraform {
2
+ required_version = ">= 1.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = ">= 6.50"
8
+ }
9
+ <%_ if (cedarPolicy) { _%>
10
+ external = {
11
+ source = "hashicorp/external"
12
+ version = ">= 2.0"
13
+ }
14
+ <%_ } _%>
15
+ random = {
16
+ source = "hashicorp/random"
17
+ version = ">= 3.0"
18
+ }
19
+ }
20
+ }
21
+
22
+ variable "additional_iam_policy_statements" {
23
+ description = "Additional IAM policy statements to attach to the gateway service role. Connection generators (e.g. agentcore-gateway#mcp-connection) append `InvokeAgentRuntime*` entries here."
24
+ type = list(object({
25
+ Effect = string
26
+ Action = list(string)
27
+ Resource = list(string)
28
+ }))
29
+ default = []
30
+ }
31
+ <%_ if (cedarPolicy) { _%>
32
+
33
+ variable "policy_dependencies" {
34
+ description = "Values the Cedar policies must wait for, e.g. gateway target ids. Policies referencing a target's actions only validate once the target has registered them."
35
+ type = list(string)
36
+ default = []
37
+ }
38
+ <%_ } _%>
39
+
40
+ data "aws_caller_identity" "current" {}
41
+ data "aws_region" "current" {}
42
+
43
+ locals {
44
+ aws_account_id = data.aws_caller_identity.current.account_id
45
+ aws_region = data.aws_region.current.id
46
+ <%_ if (cedarPolicy) { _%>
47
+ policies_dir = "${path.module}/../../../../../../../<%- projectDirectory %>/policies"
48
+ <%_ } _%>
49
+ }
50
+
51
+ resource "random_id" "unique_suffix" {
52
+ byte_length = 4
53
+ }
54
+
55
+ # IAM role for the gateway service
56
+ resource "aws_iam_role" "gateway_role" {
57
+ name = "<%- nameClassName %>-GatewayRole-${random_id.unique_suffix.hex}"
58
+
59
+ assume_role_policy = jsonencode({
60
+ Version = "2012-10-17"
61
+ Statement = [
62
+ {
63
+ Sid = "AgentCoreGatewayAssumeRolePolicy"
64
+ Effect = "Allow"
65
+ Principal = {
66
+ Service = "bedrock-agentcore.amazonaws.com"
67
+ }
68
+ Action = "sts:AssumeRole"
69
+ Condition = {
70
+ StringEquals = {
71
+ "aws:SourceAccount" = local.aws_account_id
72
+ }
73
+ ArnLike = {
74
+ "aws:SourceArn" = "arn:aws:bedrock-agentcore:${local.aws_region}:${local.aws_account_id}:*"
75
+ }
76
+ }
77
+ }
78
+ ]
79
+ })
80
+ }
81
+
82
+ <%_ if (cedarPolicy) { _%>
83
+ # Inline policy giving the gateway role the permissions it needs:
84
+ # - Policy evaluation permissions, including CheckAuthorizePermissions
85
+ # which the gateway readiness probe requires during creation.
86
+ # Authorization actions are evaluated against both the policy engine ARN
87
+ # and the gateway's own ARN (which cannot be referenced here without a
88
+ # dependency cycle, so it is scoped to this account and region's gateways)
89
+ # - Additional statements appended by connection generators (e.g. InvokeAgentRuntime*)
90
+ resource "aws_iam_role_policy" "gateway_role_policy" {
91
+ name = "<%- nameClassName %>-GatewayPolicy"
92
+ role = aws_iam_role.gateway_role.id
93
+
94
+ policy = jsonencode({
95
+ Version = "2012-10-17"
96
+ Statement = concat([
97
+ {
98
+ Sid = "PolicyEvaluation"
99
+ Effect = "Allow"
100
+ Action = [
101
+ "bedrock-agentcore:GetPolicyEngine",
102
+ "bedrock-agentcore:AuthorizeAction",
103
+ "bedrock-agentcore:PartiallyAuthorizeActions",
104
+ "bedrock-agentcore:CheckAuthorizePermissions",
105
+ ]
106
+ Resource = [
107
+ aws_bedrockagentcore_policy_engine.this.policy_engine_arn,
108
+ "arn:aws:bedrock-agentcore:${local.aws_region}:${local.aws_account_id}:gateway/*",
109
+ ]
110
+ }
111
+ ], var.additional_iam_policy_statements)
112
+ })
113
+ }
114
+ <%_ } else { _%>
115
+ # Inline policy for statements appended by connection generators
116
+ # (e.g. InvokeAgentRuntime*)
117
+ resource "aws_iam_role_policy" "gateway_role_policy" {
118
+ count = length(var.additional_iam_policy_statements) > 0 ? 1 : 0
119
+ name = "<%- nameClassName %>-GatewayPolicy"
120
+ role = aws_iam_role.gateway_role.id
121
+
122
+ policy = jsonencode({
123
+ Version = "2012-10-17"
124
+ Statement = var.additional_iam_policy_statements
125
+ })
126
+ }
127
+ <%_ } _%>
128
+ <%_ if (cedarPolicy) { _%>
129
+
130
+ # Policy engine (name must match ^[A-Za-z][A-Za-z0-9_]*$ — no hyphens)
131
+ resource "aws_bedrockagentcore_policy_engine" "this" {
132
+ name = "<%- nameClassName %>_PolicyEngine_${random_id.unique_suffix.hex}"
133
+ description = "Cedar policy engine for <%- nameClassName %>"
134
+ }
135
+ <%_ } _%>
136
+
137
+ resource "aws_bedrockagentcore_gateway" "this" {
138
+ name = "<%- nameClassName %>-${random_id.unique_suffix.hex}"
139
+ description = "AgentCore Gateway for <%- nameClassName %>"
140
+ role_arn = aws_iam_role.gateway_role.arn
141
+
142
+ protocol_type = "MCP"
143
+ protocol_configuration {
144
+ mcp {
145
+ search_type = "SEMANTIC"
146
+ supported_versions = ["2025-03-26"]
147
+ }
148
+ }
149
+
150
+ authorizer_type = "AWS_IAM"
151
+ <%_ if (cedarPolicy) { _%>
152
+
153
+ policy_engine_configuration {
154
+ arn = aws_bedrockagentcore_policy_engine.this.policy_engine_arn
155
+ mode = "ENFORCE"
156
+ }
157
+
158
+ depends_on = [aws_iam_role_policy.gateway_role_policy]
159
+ <%_ } _%>
160
+ }
161
+ <%_ if (cedarPolicy) { _%>
162
+
163
+ # Anchor for the policy_dependencies variable — depends_on only accepts
164
+ # static references, so the policies depend on this resource instead.
165
+ resource "terraform_data" "policy_dependencies" {
166
+ input = var.policy_dependencies
167
+ }
168
+
169
+ # Policies are EJS templates rendered by render-cedar.cjs with `gatewayArn`
170
+ # and `accountId` in scope.
171
+ data "external" "rendered_policies" {
172
+ for_each = fileset(local.policies_dir, "**/*.cedar")
173
+
174
+ program = ["node", "${path.module}/render-cedar.cjs"]
175
+ query = {
176
+ template = "${local.policies_dir}/${each.value}"
177
+ gatewayArn = aws_bedrockagentcore_gateway.this.gateway_arn
178
+ accountId = local.aws_account_id
179
+ }
180
+ }
181
+
182
+ # One Policy per .cedar file in the project's policies/ directory.
183
+ #
184
+ # Policy resource names are derived from the file basename and normalised
185
+ # to PascalCase. The AgentCore Policy resource `name` regex requires
186
+ # `^[A-Za-z][A-Za-z0-9_]*$` — no hyphens — so the join() reassembles the
187
+ # basename with each segment Title-cased.
188
+ resource "aws_bedrockagentcore_policy" "policies" {
189
+ for_each = fileset(local.policies_dir, "**/*.cedar")
190
+
191
+ policy_engine_id = aws_bedrockagentcore_policy_engine.this.policy_engine_id
192
+ # Policy names are unique account-wide, so append this gateway's unique
193
+ # suffix to avoid collisions with other gateways.
194
+ name = join("", concat([
195
+ for word in split("-", replace(replace(trimsuffix(each.value, ".cedar"), "_", "-"), "/", "-")) :
196
+ "${upper(substr(word, 0, 1))}${substr(word, 1, length(word))}"
197
+ ], ["_${random_id.unique_suffix.hex}"]))
198
+ description = "Policy loaded from ${each.value}"
199
+ # The Cedar analyzer rejects legitimate policies as findings (e.g. a forbid
200
+ # disabling one tool for every caller is "Overly Restrictive"), so deploy
201
+ # regardless and rely on the engine's ENFORCE mode.
202
+ validation_mode = "IGNORE_ALL_FINDINGS"
203
+ definition {
204
+ cedar {
205
+ statement = data.external.rendered_policies[each.value].result.rendered
206
+ }
207
+ }
208
+
209
+ depends_on = [terraform_data.policy_dependencies]
210
+ }
211
+ <%_ } _%>
212
+
213
+ # Add gateway URL to runtime config
214
+ module "add_gateway_url_to_runtime_config" {
215
+ source = "../../../core/runtime-config/entry"
216
+
217
+ namespace = "agentcore"
218
+ key = "gateways"
219
+ value = { "<%- nameClassName %>" = aws_bedrockagentcore_gateway.this.gateway_url }
220
+
221
+ depends_on = [aws_bedrockagentcore_gateway.this]
222
+ }
223
+
224
+ output "gateway_id" {
225
+ description = "ID of the Bedrock AgentCore Gateway"
226
+ value = aws_bedrockagentcore_gateway.this.gateway_id
227
+ }
228
+
229
+ output "gateway_arn" {
230
+ description = "ARN of the Bedrock AgentCore Gateway"
231
+ value = aws_bedrockagentcore_gateway.this.gateway_arn
232
+ }
233
+
234
+ output "gateway_url" {
235
+ description = "MCP endpoint URL of the Bedrock AgentCore Gateway"
236
+ value = aws_bedrockagentcore_gateway.this.gateway_url
237
+ }
238
+
239
+ output "gateway_role_arn" {
240
+ description = "ARN of the IAM role assumed by the gateway service"
241
+ value = aws_iam_role.gateway_role.arn
242
+ }
@@ -0,0 +1,13 @@
1
+ // Renders a Cedar policy EJS template for the <%- nameClassName %> Terraform
2
+ // module. Invoked by the `external` data source with JSON on stdin:
3
+ // { "template": "<path>", "gatewayArn": "...", "accountId": "..." }
4
+ const ejs = require('ejs');
5
+ const fs = require('fs');
6
+
7
+ let input = '';
8
+ process.stdin.on('data', (chunk) => (input += chunk));
9
+ process.stdin.on('end', () => {
10
+ const { template, ...vars } = JSON.parse(input);
11
+ const rendered = ejs.render(fs.readFileSync(template, 'utf-8'), vars);
12
+ process.stdout.write(JSON.stringify({ rendered }));
13
+ });
@@ -335,6 +335,59 @@ resource "aws_bedrockagentcore_agent_runtime" "agent_runtime" {
335
335
  ]
336
336
  }
337
337
 
338
+ # MCP consumers (e.g. gateway targets, which fetch tools at creation) can race
339
+ # the runtime's first cold start, so probe it until it serves MCP. The ARN
340
+ # output is routed through this resource so consumers wait for readiness.
341
+ resource "null_resource" "runtime_ready" {
342
+ count = var.server_protocol == "MCP" ? 1 : 0
343
+
344
+ triggers = {
345
+ runtime_arn = aws_bedrockagentcore_agent_runtime.agent_runtime.agent_runtime_arn
346
+ }
347
+
348
+ provisioner "local-exec" {
349
+ command = <<-EOT
350
+ uv run --with boto3 python -c "
351
+ import boto3
352
+ import json
353
+ import time
354
+ import uuid
355
+
356
+ runtime_arn = '${self.triggers.runtime_arn}'
357
+ client = boto3.client('bedrock-agentcore')
358
+ initialize_request = json.dumps({
359
+ 'jsonrpc': '2.0',
360
+ 'id': 1,
361
+ 'method': 'initialize',
362
+ 'params': {
363
+ 'protocolVersion': '2025-03-26',
364
+ 'capabilities': {},
365
+ 'clientInfo': {'name': 'readiness-probe', 'version': '1.0'},
366
+ },
367
+ })
368
+
369
+ deadline = time.time() + 3 * 60
370
+ while True:
371
+ try:
372
+ client.invoke_agent_runtime(
373
+ agentRuntimeArn=runtime_arn,
374
+ runtimeSessionId=f'readiness-probe-{uuid.uuid4()}',
375
+ contentType='application/json',
376
+ accept='application/json, text/event-stream',
377
+ payload=initialize_request,
378
+ )
379
+ break
380
+ except Exception:
381
+ if time.time() >= deadline:
382
+ raise
383
+ time.sleep(5)
384
+
385
+ print(f'Runtime {runtime_arn} is serving MCP')
386
+ "
387
+ EOT
388
+ }
389
+ }
390
+
338
391
  # Outputs
339
392
  output "agent_core_runtime_role_arn" {
340
393
  description = "ARN of the Agent Core Runtime IAM role"
@@ -352,8 +405,8 @@ output "agent_runtime_name" {
352
405
  }
353
406
 
354
407
  output "agent_core_runtime_arn" {
355
- description = "ARN of the Bedrock Agent Core runtime"
356
- value = aws_bedrockagentcore_agent_runtime.agent_runtime.agent_runtime_arn
408
+ description = "ARN of the Bedrock Agent Core runtime. For MCP runtimes the value flows through the readiness probe, so consumers wait until the runtime serves MCP."
409
+ value = var.server_protocol == "MCP" ? null_resource.runtime_ready[0].triggers.runtime_arn : aws_bedrockagentcore_agent_runtime.agent_runtime.agent_runtime_arn
357
410
  }
358
411
 
359
412
  output "agent_runtime_id" {
@@ -0,0 +1,14 @@
1
+ terraform {
2
+ required_version = ">= 1.0"
3
+
4
+ required_providers {
5
+ aws = {
6
+ source = "hashicorp/aws"
7
+ version = ">= 6.50"
8
+ }
9
+ random = {
10
+ source = "hashicorp/random"
11
+ version = ">= 3.0"
12
+ }
13
+ }
14
+ }