@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.
- package/LICENSE-THIRD-PARTY +81 -3
- package/generators.json +31 -1
- package/package.json +2 -2
- package/sdk/agentcore-gateway.d.ts +6 -0
- package/sdk/agentcore-gateway.js +11 -0
- package/sdk/agentcore-gateway.js.map +1 -0
- package/sdk/license.d.ts +4 -4
- package/sdk/license.js +5 -4
- package/sdk/license.js.map +1 -1
- package/sdk/py.d.ts +6 -6
- package/sdk/py.js +7 -7
- package/sdk/py.js.map +1 -1
- package/sdk/ts.d.ts +18 -14
- package/sdk/ts.js +38 -32
- package/sdk/ts.js.map +1 -1
- package/src/agentcore-gateway/files/cedar/policies/README.md +52 -0
- package/src/agentcore-gateway/files/cedar/policies/permit-all.cedar +11 -0
- package/src/agentcore-gateway/files/project/serve-local.ts.template +133 -0
- package/src/agentcore-gateway/generator.d.ts +25 -0
- package/src/agentcore-gateway/generator.js +129 -0
- package/src/agentcore-gateway/generator.js.map +1 -0
- package/src/agentcore-gateway/mcp-connection/generator.d.ts +19 -0
- package/src/agentcore-gateway/mcp-connection/generator.js +79 -0
- package/src/agentcore-gateway/mcp-connection/generator.js.map +1 -0
- package/src/agentcore-gateway/mcp-connection/schema.d.ts +12 -0
- package/src/agentcore-gateway/mcp-connection/schema.json +26 -0
- package/src/agentcore-gateway/schema.d.ts +16 -0
- package/src/agentcore-gateway/schema.json +65 -0
- package/src/connection/generator.d.ts +1 -1
- package/src/connection/generator.js +64 -46
- package/src/connection/generator.js.map +1 -1
- package/src/infra/app/__snapshots__/generator.spec.ts.snap +9 -9
- package/src/mcp-server/schema-registry.d.ts +9 -3
- package/src/mcp-server/schema-registry.js +22 -6
- package/src/mcp-server/schema-registry.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +3 -1
- package/src/py/agent/__snapshots__/generator.spec.ts.snap +629 -7
- package/src/py/agent/a2a-connection/files/agent-connection/app/__targetAgentSnakeCase___client.py.template +9 -3
- package/src/py/agent/a2a-connection/generator.js +1 -1
- package/src/py/agent/a2a-connection/generator.js.map +1 -1
- package/src/py/agent/files/http/init.py.template +5 -23
- package/src/py/agent/gateway-connection/files/agent-connection/app/__gatewaySnakeCase___client.py.template +35 -0
- package/src/py/agent/gateway-connection/generator.d.ts +10 -0
- package/src/py/agent/gateway-connection/generator.js +106 -0
- package/src/py/agent/gateway-connection/generator.js.map +1 -0
- package/src/py/agent/gateway-connection/schema.d.ts +12 -0
- package/src/py/agent/gateway-connection/schema.json +26 -0
- package/src/py/agent/generator.js +27 -22
- package/src/py/agent/generator.js.map +1 -1
- package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +8 -4
- package/src/py/agent/mcp-connection/files/agent-connection/app/__mcpServerSnakeCase___client.py.template +9 -3
- package/src/py/agent/mcp-connection/generator.js +2 -72
- package/src/py/agent/mcp-connection/generator.js.map +1 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +5 -22
- package/src/py/fast-api/files/app/__name__/init.py.template +5 -22
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +58 -3
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +619 -2
- package/src/ts/agent/a2a-connection/files/agent-connection/app/__targetAgentKebabCase__-client.ts.template +10 -6
- package/src/ts/agent/gateway-connection/files/agent-connection/app/__gatewayKebabCase__-client.ts.template +30 -0
- package/src/ts/agent/gateway-connection/generator.d.ts +10 -0
- package/src/ts/agent/gateway-connection/generator.js +106 -0
- package/src/ts/agent/gateway-connection/generator.js.map +1 -0
- package/src/ts/agent/gateway-connection/schema.d.ts +12 -0
- package/src/ts/agent/gateway-connection/schema.json +26 -0
- package/src/ts/agent/generator.js +23 -19
- package/src/ts/agent/generator.js.map +1 -1
- package/src/ts/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +9 -4
- package/src/ts/agent/mcp-connection/files/agent-connection/app/__mcpServerKebabCase__-client.ts.template +9 -6
- package/src/ts/agent/mcp-connection/generator.js +1 -13
- package/src/ts/agent/mcp-connection/generator.js.map +1 -1
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +58 -3
- package/src/ts/nx-generator/generator.js +29 -0
- package/src/ts/nx-generator/generator.js.map +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +86 -39
- package/src/utils/agent-chat/agent-chat.d.ts +31 -0
- package/src/utils/agent-chat/agent-chat.js +30 -0
- package/src/utils/agent-chat/agent-chat.js.map +1 -0
- package/src/utils/agent-chat/files/a2a/chat.ts.template +33 -0
- package/src/utils/agent-chat/files/ag-ui/chat.ts.template +17 -0
- package/src/utils/agent-chat/files/common/agentcore.ts.template +81 -0
- package/src/utils/agent-chat/files/http-py/chat.ts.template +43 -0
- package/src/utils/agent-chat/files/http-ts/chat.ts.template +54 -0
- package/src/utils/agent-connection/agent-connection.d.ts +17 -0
- package/src/utils/agent-connection/agent-connection.js +94 -0
- package/src/utils/agent-connection/agent-connection.js.map +1 -1
- package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-client.ts.template +78 -0
- package/src/utils/agent-connection/files/core-runtime-config/runtime-config.ts.template +22 -32
- package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_client.py.template +78 -0
- package/src/utils/agent-connection/files/py-core-runtime-config/runtime_config.py.template +5 -18
- package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +8 -0
- package/src/utils/agent-core-constructs/agent-core-constructs.js +54 -1
- package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
- package/src/utils/agent-core-constructs/files/cdk/app/agent-core/__nameKebabCase__/__nameKebabCase__.ts.template +4 -0
- package/src/utils/agent-core-constructs/files/cdk/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.ts.template +37 -0
- package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +293 -0
- package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/readiness-probe/index.js.template +55 -0
- package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.tf.template +242 -0
- package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/render-cedar.cjs.template +13 -0
- package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +55 -2
- package/src/utils/agent-core-constructs/files/terraform/core/agentcore-gateway/gateway.tf.template +14 -0
- package/src/utils/versions.d.ts +46 -44
- package/src/utils/versions.js +45 -43
- package/src/utils/versions.js.map +1 -1
- package/src/py/agent/scripts/http/chat.ts.template +0 -38
- 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" {
|