@aws/nx-plugin 1.0.0-rc.17 → 1.0.0-rc.19
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/generators.json +9 -1
- package/package.json +1 -1
- package/src/agentcore-gateway/attach-upstream.d.ts +33 -0
- package/src/agentcore-gateway/attach-upstream.js +41 -0
- package/src/agentcore-gateway/attach-upstream.js.map +1 -0
- package/src/agentcore-gateway/files/project/serve-local.ts.template +3 -2
- package/src/agentcore-gateway/gateway-connection/generator.d.ts +21 -0
- package/src/agentcore-gateway/gateway-connection/generator.js +102 -0
- package/src/agentcore-gateway/gateway-connection/generator.js.map +1 -0
- package/src/agentcore-gateway/gateway-connection/schema.d.ts +12 -0
- package/src/agentcore-gateway/gateway-connection/schema.json +26 -0
- package/src/agentcore-gateway/mcp-connection/generator.js +8 -24
- package/src/agentcore-gateway/mcp-connection/generator.js.map +1 -1
- package/src/connection/generator.js +66 -63
- package/src/connection/generator.js.map +1 -1
- package/src/license/generator.js +8 -19
- package/src/license/generator.js.map +1 -1
- package/src/license/known-exceptions.d.ts +0 -1
- package/src/license/known-exceptions.js +1 -8
- package/src/license/known-exceptions.js.map +1 -1
- package/src/py/agent/__snapshots__/generator.spec.ts.snap +15 -6
- package/src/py/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +201 -0
- package/src/py/agent/a2a-connection/files/agent-connection/app/{__targetAgentSnakeCase___client.py.template → __targetAgentSnakeCase___client_strands.py.template} +7 -5
- package/src/py/agent/a2a-connection/generator.js +6 -5
- package/src/py/agent/a2a-connection/generator.js.map +1 -1
- package/src/py/agent/files/http/main.py.template +2 -2
- package/src/py/agent/gateway-connection/__snapshots__/generator.spec.ts.snap +197 -0
- package/src/py/agent/gateway-connection/files/agent-connection/app/{__gatewaySnakeCase___client.py.template → __gatewaySnakeCase___client_strands.py.template} +6 -6
- package/src/py/agent/gateway-connection/generator.js +5 -5
- package/src/py/agent/gateway-connection/generator.js.map +1 -1
- package/src/py/agent/generator.js +3 -5
- package/src/py/agent/generator.js.map +1 -1
- package/src/py/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +157 -79
- package/src/py/agent/mcp-connection/files/agent-connection/app/{__mcpServerSnakeCase___client.py.template → __mcpServerSnakeCase___client_strands.py.template} +7 -5
- package/src/py/agent/mcp-connection/generator.js +5 -5
- package/src/py/agent/mcp-connection/generator.js.map +1 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +37 -3
- package/src/py/fast-api/files/app/__name__/main.py.template +4 -1
- package/src/py/mcp-server/__snapshots__/generator.spec.ts.snap +13 -4
- package/src/smithy/project/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/smithy/project/files/src/operations/echo.smithy.template +1 -0
- package/src/terraform/project/files/application/src/main.tf.template +6 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +35 -4
- package/src/trpc/backend/files/src/procedures/echo.ts.template +1 -1
- package/src/trpc/backend/files/src/schema/echo.ts.template +2 -2
- package/src/ts/agent/__snapshots__/generator.spec.ts.snap +14 -5
- package/src/ts/agent/a2a-connection/__snapshots__/generator.spec.ts.snap +256 -0
- package/src/ts/agent/a2a-connection/files/agent-connection/app/{__targetAgentKebabCase__-client.ts.template → __targetAgentKebabCase__-client-strands.ts.template} +5 -5
- package/src/ts/agent/a2a-connection/generator.js +3 -3
- package/src/ts/agent/a2a-connection/generator.js.map +1 -1
- package/src/ts/agent/files/http/router.ts.template +1 -1
- package/src/ts/agent/gateway-connection/__snapshots__/generator.spec.ts.snap +220 -0
- package/src/ts/agent/gateway-connection/files/agent-connection/app/{__gatewayKebabCase__-client.ts.template → __gatewayKebabCase__-client-strands.ts.template} +5 -5
- package/src/ts/agent/gateway-connection/generator.js +3 -3
- package/src/ts/agent/gateway-connection/generator.js.map +1 -1
- package/src/ts/agent/generator.js +3 -0
- package/src/ts/agent/generator.js.map +1 -1
- package/src/ts/agent/mcp-connection/__snapshots__/generator.spec.ts.snap +183 -110
- package/src/ts/agent/mcp-connection/files/agent-connection/app/{__mcpServerKebabCase__-client.ts.template → __mcpServerKebabCase__-client-strands.ts.template} +5 -5
- package/src/ts/agent/mcp-connection/generator.js +3 -3
- package/src/ts/agent/mcp-connection/generator.js.map +1 -1
- package/src/ts/mcp-server/__snapshots__/generator.spec.ts.snap +13 -4
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +64 -24
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +13 -4
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.terraform.spec.ts.snap +9 -3
- package/src/utils/agent-connection/agent-connection.d.ts +50 -24
- package/src/utils/agent-connection/agent-connection.js +137 -45
- package/src/utils/agent-connection/agent-connection.js.map +1 -1
- package/src/utils/agent-connection/files/core-a2a/agentcore-a2a-client-config.ts.template +93 -0
- package/src/utils/agent-connection/files/core-auth/agentcore-endpoints.ts.template +37 -0
- package/src/utils/agent-connection/files/core-auth/agentcore-fetch.ts.template +54 -0
- package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-transport.ts.template +41 -0
- package/src/utils/agent-connection/files/core-mcp/agentcore-mcp-transport.ts.template +70 -0
- package/src/utils/agent-connection/files/core-shared/agentcore-transport.ts.template +39 -0
- package/src/utils/agent-connection/files/core-strands/a2a/agentcore-a2a-client-strands.ts.template +61 -0
- package/src/utils/agent-connection/files/core-strands/gateway/agentcore-gateway-mcp-client-strands.ts.template +26 -0
- package/src/utils/agent-connection/files/core-strands/mcp/agentcore-mcp-client-strands.ts.template +27 -0
- package/src/utils/agent-connection/files/py-core-a2a/agentcore_a2a_client_config.py.template +43 -0
- package/src/utils/agent-connection/files/py-core-auth/agentcore_endpoints.py.template +47 -0
- package/src/utils/agent-connection/files/py-core-auth/auth/session.py.template +51 -0
- package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_transport.py.template +28 -0
- package/src/utils/agent-connection/files/py-core-mcp/agentcore_mcp_transport.py.template +34 -0
- package/src/utils/agent-connection/files/py-core-shared/agentcore_transport.py.template +40 -0
- package/src/utils/agent-connection/files/py-core-strands/a2a/agentcore_a2a_client_strands.py.template +66 -0
- package/src/utils/agent-connection/files/py-core-strands/gateway/agentcore_gateway_mcp_client_strands.py.template +22 -0
- package/src/utils/agent-connection/files/py-core-strands/mcp/agentcore_mcp_client_strands.py.template +30 -0
- package/src/utils/agent-core-constructs/files/cdk/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.ts.template +3 -0
- package/src/utils/agent-core-constructs/files/cdk/core/agentcore-gateway/agentcore-gateway.ts.template +110 -28
- package/src/utils/agent-core-constructs/files/terraform/app/agentcore-gateway/__nameKebabCase__/__nameKebabCase__.tf.template +95 -2
- package/src/utils/agent-core-constructs/files/terraform/core/agent-core/runtime.tf.template +13 -4
- package/src/utils/api-constructs/files/cdk/core/api/http/http-api.ts.template +9 -0
- package/src/utils/api-constructs/files/cdk/core/api/rest/rest-api.ts.template +23 -1
- package/src/utils/files/terraform/src/core/runtime-config/appconfig-deployment/appconfig-deployment.tf.template +9 -3
- package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +7 -2
- package/src/utils/identity-constructs/files/terraform/core/user-identity/add-callback-url/add-callback-url.tf.template +9 -3
- package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +22 -8
- package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +10 -4
- package/src/utils/versions.d.ts +1 -1
- package/src/utils/versions.js +1 -1
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +13 -4
- package/src/utils/agent-connection/files/core-a2a/agentcore-a2a-client.ts.template +0 -123
- package/src/utils/agent-connection/files/core-gateway/agentcore-gateway-mcp-client.ts.template +0 -78
- package/src/utils/agent-connection/files/core-mcp/agentcore-mcp-client.ts.template +0 -128
- package/src/utils/agent-connection/files/py-core-a2a/agentcore_a2a_client.py.template +0 -120
- package/src/utils/agent-connection/files/py-core-gateway/agentcore_gateway_mcp_client.py.template +0 -78
- package/src/utils/agent-connection/files/py-core-mcp/agentcore_mcp_client.py.template +0 -89
- /package/src/utils/agent-connection/files/{core-runtime-config/model-errors.ts.template → core-strands/base/model-errors-strands.ts.template} +0 -0
- /package/src/utils/agent-connection/files/{core-runtime-config/with-session-id.ts.template → core-strands/base/with-session-id-strands.ts.template} +0 -0
- /package/src/utils/agent-connection/files/{py-core-runtime-config/model_errors.py.template → py-core-strands/base/model_errors_strands.py.template} +0 -0
- /package/src/utils/agent-connection/files/{py-core-runtime-config/with_session_id.py.template → py-core-strands/base/with_session_id_strands.py.template} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from .agentcore_endpoints import mcp_url_from_arn, region_from_arn
|
|
4
|
+
from .agentcore_transport import (
|
|
5
|
+
TransportFactory,
|
|
6
|
+
jwt_transport,
|
|
7
|
+
no_auth_transport,
|
|
8
|
+
sigv4_transport,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentCoreMCPTransport:
|
|
13
|
+
"""Factory for MCP transport factories that connect to an AgentCore runtime."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def with_iam_auth(agent_runtime_arn: str) -> TransportFactory:
|
|
17
|
+
"""SigV4-authenticated transport for a Bedrock AgentCore runtime."""
|
|
18
|
+
return sigv4_transport(
|
|
19
|
+
mcp_url_from_arn(agent_runtime_arn), region_from_arn(agent_runtime_arn)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def with_jwt_auth(
|
|
24
|
+
agent_runtime_arn: str, access_token_provider: Callable[[], str]
|
|
25
|
+
) -> TransportFactory:
|
|
26
|
+
"""Bearer-authenticated transport for a Bedrock AgentCore runtime."""
|
|
27
|
+
return jwt_transport(
|
|
28
|
+
mcp_url_from_arn(agent_runtime_arn), access_token_provider
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def without_auth(url: str) -> TransportFactory:
|
|
33
|
+
"""Plain-HTTP transport — for local dev."""
|
|
34
|
+
return no_auth_transport(url)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
|
|
6
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
7
|
+
|
|
8
|
+
from .auth.session import jwt_auth, plain_auth, sigv4_auth
|
|
9
|
+
|
|
10
|
+
# Agent frameworks wrap a transport *factory* — a no-arg callable returning a
|
|
11
|
+
# streamable-HTTP transport context manager. Typed as ``Any`` so this layer
|
|
12
|
+
# stays free of any framework-specific transport type.
|
|
13
|
+
TransportFactory = Callable[[], Any]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _factory(url: str, auth) -> TransportFactory:
|
|
17
|
+
return lambda: streamablehttp_client(
|
|
18
|
+
url,
|
|
19
|
+
auth=auth,
|
|
20
|
+
timeout=120,
|
|
21
|
+
terminate_on_close=False,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def sigv4_transport(url: str, region: str) -> TransportFactory:
|
|
26
|
+
"""SigV4-signed transport factory for a resolved AgentCore endpoint."""
|
|
27
|
+
credentials = boto3.Session(region_name=region).get_credentials()
|
|
28
|
+
return _factory(url, sigv4_auth(credentials, region))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def jwt_transport(
|
|
32
|
+
url: str, access_token_provider: Callable[[], str]
|
|
33
|
+
) -> TransportFactory:
|
|
34
|
+
"""Bearer-token transport factory for a resolved AgentCore endpoint."""
|
|
35
|
+
return _factory(url, jwt_auth(access_token_provider))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def no_auth_transport(url: str) -> TransportFactory:
|
|
39
|
+
"""Plain-HTTP transport factory — for local dev."""
|
|
40
|
+
return _factory(url, plain_auth())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from strands.agent.a2a_agent import A2AAgent
|
|
4
|
+
|
|
5
|
+
from .agentcore_a2a_client_config import AgentCoreA2aClientConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _build(
|
|
9
|
+
config: tuple, *, name: str | None, description: str | None
|
|
10
|
+
) -> A2AAgent:
|
|
11
|
+
url, client_config = config
|
|
12
|
+
kwargs: dict = {"endpoint": url, "client_config": client_config}
|
|
13
|
+
if name:
|
|
14
|
+
kwargs["name"] = name
|
|
15
|
+
if description:
|
|
16
|
+
kwargs["description"] = description
|
|
17
|
+
return A2AAgent(**kwargs)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentCoreA2aClientStrands:
|
|
21
|
+
"""Factory for Strands A2A clients that connect to an AgentCore runtime."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def with_iam_auth(
|
|
25
|
+
agent_runtime_arn: str,
|
|
26
|
+
*,
|
|
27
|
+
name: str | None = None,
|
|
28
|
+
description: str | None = None,
|
|
29
|
+
) -> A2AAgent:
|
|
30
|
+
"""SigV4-authenticated client for a Bedrock AgentCore runtime."""
|
|
31
|
+
return _build(
|
|
32
|
+
AgentCoreA2aClientConfig.with_iam_auth(agent_runtime_arn),
|
|
33
|
+
name=name,
|
|
34
|
+
description=description,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def with_jwt_auth(
|
|
39
|
+
agent_runtime_arn: str,
|
|
40
|
+
access_token_provider: Callable[[], str],
|
|
41
|
+
*,
|
|
42
|
+
name: str | None = None,
|
|
43
|
+
description: str | None = None,
|
|
44
|
+
) -> A2AAgent:
|
|
45
|
+
"""Bearer-authenticated client for a Bedrock AgentCore runtime."""
|
|
46
|
+
return _build(
|
|
47
|
+
AgentCoreA2aClientConfig.with_jwt_auth(
|
|
48
|
+
agent_runtime_arn, access_token_provider
|
|
49
|
+
),
|
|
50
|
+
name=name,
|
|
51
|
+
description=description,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def without_auth(
|
|
56
|
+
url: str,
|
|
57
|
+
*,
|
|
58
|
+
name: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
) -> A2AAgent:
|
|
61
|
+
"""Plain-HTTP client — for local dev."""
|
|
62
|
+
return _build(
|
|
63
|
+
AgentCoreA2aClientConfig.without_auth(url),
|
|
64
|
+
name=name,
|
|
65
|
+
description=description,
|
|
66
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from strands.tools.mcp.mcp_client import MCPClient
|
|
2
|
+
|
|
3
|
+
from .agentcore_gateway_mcp_transport import AgentCoreGatewayMCPTransport
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentCoreGatewayMCPClientStrands:
|
|
7
|
+
"""Factory for Strands MCP clients that connect to an AgentCore Gateway."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def with_iam_auth(
|
|
11
|
+
gateway_url: str,
|
|
12
|
+
region: str | None = None,
|
|
13
|
+
) -> MCPClient:
|
|
14
|
+
"""Create a gateway MCP client authenticated with IAM SigV4."""
|
|
15
|
+
return MCPClient(
|
|
16
|
+
AgentCoreGatewayMCPTransport.with_iam_auth(gateway_url, region)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def without_auth(gateway_url: str) -> MCPClient:
|
|
21
|
+
"""Plain-HTTP gateway client, for the local gateway started by serve-local."""
|
|
22
|
+
return MCPClient(AgentCoreGatewayMCPTransport.without_auth(gateway_url))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from strands.tools.mcp.mcp_client import MCPClient
|
|
4
|
+
|
|
5
|
+
from .agentcore_mcp_transport import AgentCoreMCPTransport
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentCoreMCPClientStrands:
|
|
9
|
+
"""Factory for Strands MCP clients that connect to an AgentCore runtime."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def with_iam_auth(agent_runtime_arn: str) -> MCPClient:
|
|
13
|
+
"""SigV4-authenticated client for a Bedrock AgentCore runtime."""
|
|
14
|
+
return MCPClient(AgentCoreMCPTransport.with_iam_auth(agent_runtime_arn))
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def with_jwt_auth(
|
|
18
|
+
agent_runtime_arn: str, access_token_provider: Callable[[], str]
|
|
19
|
+
) -> MCPClient:
|
|
20
|
+
"""Bearer-authenticated client for a Bedrock AgentCore runtime."""
|
|
21
|
+
return MCPClient(
|
|
22
|
+
AgentCoreMCPTransport.with_jwt_auth(
|
|
23
|
+
agent_runtime_arn, access_token_provider
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def without_auth(url: str) -> MCPClient:
|
|
29
|
+
"""Plain-HTTP client — for local dev."""
|
|
30
|
+
return MCPClient(AgentCoreMCPTransport.without_auth(url))
|
|
@@ -18,6 +18,9 @@ import { findWorkspaceRoot } from '../../../core/workspace.js';
|
|
|
18
18
|
<%_ } _%>
|
|
19
19
|
*/
|
|
20
20
|
export class <%- nameClassName %> extends AgentCoreGateway {
|
|
21
|
+
/** Default Gateway target name when added to another Gateway. */
|
|
22
|
+
public readonly gatewayName = '<%- nameKebabCase %>';
|
|
23
|
+
|
|
21
24
|
constructor(scope: Construct, id: string) {
|
|
22
25
|
super(scope, id, {
|
|
23
26
|
<%_ if (cedarPolicy) { _%>
|
|
@@ -187,34 +187,7 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
187
187
|
cdk.Fn.split('/', cdk.Fn.join('%3A', cdk.Fn.split(':', arn))),
|
|
188
188
|
);
|
|
189
189
|
const endpoint = `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations?qualifier=DEFAULT`;
|
|
190
|
-
|
|
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
|
-
);
|
|
190
|
+
const target = this.addTarget(props.gatewayTargetName, endpoint);
|
|
218
191
|
|
|
219
192
|
// Grant the gateway role invoke access to the target runtime before the
|
|
220
193
|
// target is created — AgentCore probes the target during creation, which
|
|
@@ -253,6 +226,115 @@ export class AgentCoreGateway extends Construct implements iam.IGrantable {
|
|
|
253
226
|
}
|
|
254
227
|
target.node.addDependency(probe.trigger);
|
|
255
228
|
|
|
229
|
+
return target;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add another Gateway as a target of this Gateway, exposing its tools
|
|
234
|
+
* under `<targetName>___<innerTargetName>___<toolName>` names.
|
|
235
|
+
*
|
|
236
|
+
* The target name defaults to the gateway construct's `gatewayName`
|
|
237
|
+
* (its project's class name in kebab-case, e.g. `MyGateway` ->
|
|
238
|
+
* `my-gateway`). It prefixes Cedar action names and the gateway's tool
|
|
239
|
+
* names, so renaming it later is a breaking change for consumers.
|
|
240
|
+
*
|
|
241
|
+
* Both gateways evaluate their own Cedar policies: this gateway
|
|
242
|
+
* authorizes the prefixed action, then the target gateway authorizes the
|
|
243
|
+
* inner action for this gateway's role.
|
|
244
|
+
*/
|
|
245
|
+
public addGateway(
|
|
246
|
+
gateway: Construct & {
|
|
247
|
+
readonly gateway: agentcore.Gateway;
|
|
248
|
+
readonly gatewayName: string;
|
|
249
|
+
},
|
|
250
|
+
props?: {
|
|
251
|
+
gatewayTargetName?: string;
|
|
252
|
+
},
|
|
253
|
+
): agentcore.CfnGatewayTarget {
|
|
254
|
+
const target = this.addGatewayTarget({
|
|
255
|
+
gatewayTargetName: props?.gatewayTargetName ?? gateway.gatewayName,
|
|
256
|
+
// The Gateway L2 only populates gatewayUrl when created from its own
|
|
257
|
+
// props; fall back to the CloudFormation attribute otherwise.
|
|
258
|
+
gatewayUrl:
|
|
259
|
+
gateway.gateway.gatewayUrl ??
|
|
260
|
+
(gateway.gateway.node.defaultChild as agentcore.CfnGateway)
|
|
261
|
+
.attrGatewayUrl,
|
|
262
|
+
gatewayArn: gateway.gateway.gatewayArn,
|
|
263
|
+
});
|
|
264
|
+
// AgentCore fetches the target gateway's tools during target creation,
|
|
265
|
+
// so the target gateway and all of its own targets must exist first.
|
|
266
|
+
// Construct dependencies resolve at synth time, covering targets added
|
|
267
|
+
// to the target gateway after this call.
|
|
268
|
+
target.node.addDependency(gateway);
|
|
269
|
+
return target;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Register another Gateway's MCP endpoint as a target of this Gateway.
|
|
274
|
+
* Prefer {@link addGateway} when the construct is in scope — it also
|
|
275
|
+
* orders this gateway's target after the target gateway's own targets,
|
|
276
|
+
* so its tools are registered before they are fetched.
|
|
277
|
+
*/
|
|
278
|
+
public addGatewayTarget(props: {
|
|
279
|
+
gatewayTargetName: string;
|
|
280
|
+
gatewayUrl: string;
|
|
281
|
+
gatewayArn: string;
|
|
282
|
+
}): agentcore.CfnGatewayTarget {
|
|
283
|
+
const target = this.addTarget(props.gatewayTargetName, props.gatewayUrl);
|
|
284
|
+
|
|
285
|
+
// Grant the gateway role invoke access to the target gateway before the
|
|
286
|
+
// target is created — AgentCore fetches the target's tools during
|
|
287
|
+
// creation, which fails if the permission is not yet in place.
|
|
288
|
+
const grant = this.gateway.role.addToPrincipalPolicy(
|
|
289
|
+
new iam.PolicyStatement({
|
|
290
|
+
actions: ['bedrock-agentcore:InvokeGateway'],
|
|
291
|
+
resources: [props.gatewayArn],
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
if (grant.policyDependable) {
|
|
295
|
+
target.node.addDependency(grant.policyDependable);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return target;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create a Gateway target for an MCP endpoint, with outbound calls signed
|
|
303
|
+
* using this gateway's own execution role.
|
|
304
|
+
*/
|
|
305
|
+
private addTarget(
|
|
306
|
+
gatewayTargetName: string,
|
|
307
|
+
endpoint: string,
|
|
308
|
+
): agentcore.CfnGatewayTarget {
|
|
309
|
+
// The construct id is derived from the target name so the two change
|
|
310
|
+
// together — target names are unique per gateway, and replacing a target
|
|
311
|
+
// under an unchanged name fails with AlreadyExists.
|
|
312
|
+
const target = new agentcore.CfnGatewayTarget(
|
|
313
|
+
this,
|
|
314
|
+
`Target-${gatewayTargetName}`,
|
|
315
|
+
{
|
|
316
|
+
gatewayIdentifier: this.gateway.gatewayId,
|
|
317
|
+
name: gatewayTargetName,
|
|
318
|
+
targetConfiguration: {
|
|
319
|
+
mcp: {
|
|
320
|
+
mcpServer: {
|
|
321
|
+
endpoint,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
credentialProviderConfigurations: [
|
|
326
|
+
{
|
|
327
|
+
credentialProviderType: 'GATEWAY_IAM_ROLE',
|
|
328
|
+
credentialProvider: {
|
|
329
|
+
iamCredentialProvider: {
|
|
330
|
+
service: 'bedrock-agentcore',
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
256
338
|
// Policies referencing this target's actions (e.g.
|
|
257
339
|
// AgentCore::Action::"<targetName>___<tool>") only validate once the
|
|
258
340
|
// target has registered them, so create policies after targets.
|
|
@@ -28,6 +28,12 @@ variable "additional_iam_policy_statements" {
|
|
|
28
28
|
}))
|
|
29
29
|
default = []
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
variable "tool_dependencies" {
|
|
33
|
+
description = "Target ids whose tools must be queryable before `gateway_url` is consumable. Set this to the gateway's own target ids (e.g. from agentcore-gateway#mcp-connection) when another gateway aggregates this one: the readiness probe waits until this gateway serves those tools, since a parent gateway fetches them at target creation."
|
|
34
|
+
type = list(string)
|
|
35
|
+
default = []
|
|
36
|
+
}
|
|
31
37
|
<%_ if (cedarPolicy) { _%>
|
|
32
38
|
|
|
33
39
|
variable "policy_dependencies" {
|
|
@@ -210,6 +216,91 @@ resource "aws_bedrockagentcore_policy" "policies" {
|
|
|
210
216
|
}
|
|
211
217
|
<%_ } _%>
|
|
212
218
|
|
|
219
|
+
# A parent gateway fetches this gateway's tools when it registers it as a
|
|
220
|
+
# target, so this gateway must already serve them — its own targets register
|
|
221
|
+
# their tools asynchronously after creation. Probe `tools/list` over SigV4
|
|
222
|
+
# until the configured `tool_dependencies` are served, and route `gateway_url`
|
|
223
|
+
# through this resource so a parent gateway target waits for readiness. Only
|
|
224
|
+
# runs when this gateway is aggregated by another (empty `tool_dependencies`).
|
|
225
|
+
resource "null_resource" "gateway_ready" {
|
|
226
|
+
count = length(var.tool_dependencies) > 0 ? 1 : 0
|
|
227
|
+
|
|
228
|
+
triggers = {
|
|
229
|
+
gateway_url = aws_bedrockagentcore_gateway.this.gateway_url
|
|
230
|
+
region = local.aws_region
|
|
231
|
+
tool_dependencies = join(",", var.tool_dependencies)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
provisioner "local-exec" {
|
|
235
|
+
command = <<-EOT
|
|
236
|
+
uv run --with boto3 --with httpx --with mcp python -c "
|
|
237
|
+
import asyncio
|
|
238
|
+
import os
|
|
239
|
+
import time
|
|
240
|
+
|
|
241
|
+
import boto3
|
|
242
|
+
import httpx
|
|
243
|
+
from botocore.auth import SigV4Auth
|
|
244
|
+
from botocore.awsrequest import AWSRequest
|
|
245
|
+
from mcp import ClientSession
|
|
246
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
247
|
+
|
|
248
|
+
gateway_url = os.environ['GATEWAY_URL']
|
|
249
|
+
region = os.environ['REGION']
|
|
250
|
+
credentials = boto3.Session(region_name=region).get_credentials()
|
|
251
|
+
signer = SigV4Auth(credentials, 'bedrock-agentcore', region)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SigV4Auth_(httpx.Auth):
|
|
255
|
+
requires_request_body = True
|
|
256
|
+
|
|
257
|
+
def auth_flow(self, request):
|
|
258
|
+
aws_request = AWSRequest(
|
|
259
|
+
method=request.method,
|
|
260
|
+
url=str(request.url),
|
|
261
|
+
data=request.content,
|
|
262
|
+
headers=dict(request.headers),
|
|
263
|
+
)
|
|
264
|
+
signer.add_auth(aws_request)
|
|
265
|
+
request.headers.update(dict(aws_request.headers))
|
|
266
|
+
yield request
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def list_tools():
|
|
270
|
+
async with streamablehttp_client(
|
|
271
|
+
gateway_url, auth=SigV4Auth_(), timeout=120
|
|
272
|
+
) as (read, write, _):
|
|
273
|
+
async with ClientSession(read, write) as session:
|
|
274
|
+
await session.initialize()
|
|
275
|
+
result = await session.list_tools()
|
|
276
|
+
return [t.name for t in result.tools]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# The built-in semantic-search tool is always present, so require at least one
|
|
280
|
+
# more tool before declaring the gateway ready to serve its aggregated targets.
|
|
281
|
+
deadline = time.time() + 5 * 60
|
|
282
|
+
while True:
|
|
283
|
+
try:
|
|
284
|
+
tools = asyncio.run(list_tools())
|
|
285
|
+
if len([t for t in tools if t != 'x_amz_bedrock_agentcore_search']) > 0:
|
|
286
|
+
break
|
|
287
|
+
last = f'gateway serves no aggregated tools yet (got {tools})'
|
|
288
|
+
except Exception as e:
|
|
289
|
+
last = str(e)
|
|
290
|
+
if time.time() >= deadline:
|
|
291
|
+
raise SystemExit(f'Gateway {gateway_url} not ready: {last}')
|
|
292
|
+
time.sleep(5)
|
|
293
|
+
|
|
294
|
+
print(f'Gateway {gateway_url} is serving tools')
|
|
295
|
+
"
|
|
296
|
+
EOT
|
|
297
|
+
environment = {
|
|
298
|
+
GATEWAY_URL = self.triggers.gateway_url
|
|
299
|
+
REGION = self.triggers.region
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
213
304
|
# Add gateway URL to runtime config
|
|
214
305
|
module "add_gateway_url_to_runtime_config" {
|
|
215
306
|
source = "../../../core/runtime-config/entry"
|
|
@@ -232,8 +323,10 @@ output "gateway_arn" {
|
|
|
232
323
|
}
|
|
233
324
|
|
|
234
325
|
output "gateway_url" {
|
|
235
|
-
description = "MCP endpoint URL of the Bedrock AgentCore Gateway"
|
|
236
|
-
value
|
|
326
|
+
description = "MCP endpoint URL of the Bedrock AgentCore Gateway. When `tool_dependencies` is set, this value flows through the readiness probe so consumers (e.g. a parent gateway target) wait until the gateway serves its aggregated tools."
|
|
327
|
+
value = length(var.tool_dependencies) > 0 ? (
|
|
328
|
+
null_resource.gateway_ready[0].triggers.gateway_url
|
|
329
|
+
) : aws_bedrockagentcore_gateway.this.gateway_url
|
|
237
330
|
}
|
|
238
331
|
|
|
239
332
|
output "gateway_role_arn" {
|
|
@@ -281,14 +281,19 @@ resource "null_resource" "docker_publish" {
|
|
|
281
281
|
provisioner "local-exec" {
|
|
282
282
|
command = <<-EOT
|
|
283
283
|
# Get ECR login token
|
|
284
|
-
aws ecr get-login-password --region $
|
|
284
|
+
aws ecr get-login-password --region "$AWS_REGION" | <%= containers %> login --username AWS --password-stdin "$REPOSITORY_URL"
|
|
285
285
|
|
|
286
286
|
# Tag the image
|
|
287
|
-
<%= containers %> tag $
|
|
287
|
+
<%= containers %> tag "$DOCKER_IMAGE_TAG" "$REPOSITORY_URL:latest"
|
|
288
288
|
|
|
289
289
|
# Push the image
|
|
290
|
-
<%= containers %> push $
|
|
290
|
+
<%= containers %> push "$REPOSITORY_URL:latest"
|
|
291
291
|
EOT
|
|
292
|
+
environment = {
|
|
293
|
+
AWS_REGION = local.aws_region
|
|
294
|
+
REPOSITORY_URL = self.triggers.repository_url
|
|
295
|
+
DOCKER_IMAGE_TAG = self.triggers.docker_image_tag
|
|
296
|
+
}
|
|
292
297
|
}
|
|
293
298
|
|
|
294
299
|
depends_on = [aws_ecr_repository_policy.agent_core_ecr_policy]
|
|
@@ -350,10 +355,11 @@ resource "null_resource" "runtime_ready" {
|
|
|
350
355
|
uv run --with boto3 python -c "
|
|
351
356
|
import boto3
|
|
352
357
|
import json
|
|
358
|
+
import os
|
|
353
359
|
import time
|
|
354
360
|
import uuid
|
|
355
361
|
|
|
356
|
-
runtime_arn =
|
|
362
|
+
runtime_arn = os.environ['RUNTIME_ARN']
|
|
357
363
|
client = boto3.client('bedrock-agentcore')
|
|
358
364
|
initialize_request = json.dumps({
|
|
359
365
|
'jsonrpc': '2.0',
|
|
@@ -385,6 +391,9 @@ while True:
|
|
|
385
391
|
print(f'Runtime {runtime_arn} is serving MCP')
|
|
386
392
|
"
|
|
387
393
|
EOT
|
|
394
|
+
environment = {
|
|
395
|
+
RUNTIME_ARN = self.triggers.runtime_arn
|
|
396
|
+
}
|
|
388
397
|
}
|
|
389
398
|
}
|
|
390
399
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
HttpMethod,
|
|
13
13
|
HttpStage,
|
|
14
14
|
LogGroupLogDestination,
|
|
15
|
+
ThrottleSettings,
|
|
15
16
|
} from 'aws-cdk-lib/aws-apigatewayv2';
|
|
16
17
|
import { LogGroup } from 'aws-cdk-lib/aws-logs';
|
|
17
18
|
import { suppressRules } from '../checkov.js';
|
|
@@ -38,6 +39,12 @@ export interface HttpApiProps<
|
|
|
38
39
|
* Map of operation names to their API Gateway integrations
|
|
39
40
|
*/
|
|
40
41
|
readonly integrations: TIntegrations;
|
|
42
|
+
/**
|
|
43
|
+
* Default throttling limits applied to the API's default stage.
|
|
44
|
+
*
|
|
45
|
+
* @default { rateLimit: 10000, burstLimit: 5000 }
|
|
46
|
+
*/
|
|
47
|
+
readonly throttle?: ThrottleSettings;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/**
|
|
@@ -71,6 +78,7 @@ export class HttpApi<
|
|
|
71
78
|
apiName,
|
|
72
79
|
operations,
|
|
73
80
|
integrations,
|
|
81
|
+
throttle = { rateLimit: 10000, burstLimit: 5000 },
|
|
74
82
|
...props
|
|
75
83
|
}: HttpApiProps<TIntegrations, TOperation>,
|
|
76
84
|
) {
|
|
@@ -98,6 +106,7 @@ export class HttpApi<
|
|
|
98
106
|
this.defaultStage = new HttpStage(this, 'DefaultStage', {
|
|
99
107
|
httpApi: this.api,
|
|
100
108
|
autoDeploy: true,
|
|
109
|
+
throttle,
|
|
101
110
|
accessLogSettings: {
|
|
102
111
|
destination: new LogGroupLogDestination(accessLogGroup),
|
|
103
112
|
},
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RestApiProps as _RestApiProps,
|
|
7
7
|
IResource,
|
|
8
8
|
Stage,
|
|
9
|
+
ThrottleSettings,
|
|
9
10
|
} from 'aws-cdk-lib/aws-apigateway';
|
|
10
11
|
import { IAspect, RemovalPolicy } from 'aws-cdk-lib';
|
|
11
12
|
import {
|
|
@@ -52,6 +53,12 @@ export interface RestApiProps<
|
|
|
52
53
|
* @default true
|
|
53
54
|
*/
|
|
54
55
|
readonly enableWaf?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Default throttling limits applied to all methods on the API's default stage.
|
|
58
|
+
*
|
|
59
|
+
* @default { rateLimit: 10000, burstLimit: 5000 }
|
|
60
|
+
*/
|
|
61
|
+
readonly throttle?: ThrottleSettings;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
/**
|
|
@@ -86,6 +93,7 @@ export class RestApi<
|
|
|
86
93
|
operations,
|
|
87
94
|
integrations,
|
|
88
95
|
enableWaf = true,
|
|
96
|
+
throttle = { rateLimit: 10000, burstLimit: 5000 },
|
|
89
97
|
...props
|
|
90
98
|
}: RestApiProps<TIntegrations, TOperation>,
|
|
91
99
|
) {
|
|
@@ -93,7 +101,21 @@ export class RestApi<
|
|
|
93
101
|
this.integrations = integrations;
|
|
94
102
|
|
|
95
103
|
// Create the API Gateway REST API
|
|
96
|
-
this.api = new _RestApi(this, 'Api',
|
|
104
|
+
this.api = new _RestApi(this, 'Api', {
|
|
105
|
+
...props,
|
|
106
|
+
deployOptions: {
|
|
107
|
+
...props.deployOptions,
|
|
108
|
+
methodOptions: {
|
|
109
|
+
...props.deployOptions?.methodOptions,
|
|
110
|
+
// Default throttling applied to all resource paths and methods
|
|
111
|
+
'/*/*': {
|
|
112
|
+
throttlingRateLimit: throttle.rateLimit,
|
|
113
|
+
throttlingBurstLimit: throttle.burstLimit,
|
|
114
|
+
...props.deployOptions?.methodOptions?.['/*/*'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
97
119
|
|
|
98
120
|
if (enableWaf) {
|
|
99
121
|
this.webAcl = new CfnWebACL(this, 'WebAcl', {
|
|
@@ -65,12 +65,13 @@ resource "null_resource" "aggregate_namespace" {
|
|
|
65
65
|
command = <<-EOT
|
|
66
66
|
uv run python -c "
|
|
67
67
|
import json
|
|
68
|
+
import os
|
|
68
69
|
import pathlib
|
|
69
70
|
import sys
|
|
70
71
|
|
|
71
|
-
namespace =
|
|
72
|
-
entries_dir = pathlib.Path(
|
|
73
|
-
output_file = pathlib.Path(
|
|
72
|
+
namespace = os.environ['NAMESPACE']
|
|
73
|
+
entries_dir = pathlib.Path(os.environ['ENTRIES_DIR']) / namespace
|
|
74
|
+
output_file = pathlib.Path(os.environ['CONFIG_DIR']) / f'{namespace}.json'
|
|
74
75
|
|
|
75
76
|
def deep_merge(target: dict, source: dict) -> dict:
|
|
76
77
|
for k, v in source.items():
|
|
@@ -103,6 +104,11 @@ output_file.write_text(json.dumps(merged, indent=2))
|
|
|
103
104
|
print(f'Aggregated {namespace}.json with {len(merged)} section(s)')
|
|
104
105
|
"
|
|
105
106
|
EOT
|
|
107
|
+
environment = {
|
|
108
|
+
NAMESPACE = each.key
|
|
109
|
+
ENTRIES_DIR = local.entries_dir
|
|
110
|
+
CONFIG_DIR = local.config_dir
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -38,11 +38,12 @@ resource "null_resource" "aggregate_namespace" {
|
|
|
38
38
|
command = <<-EOT
|
|
39
39
|
uv run python -c "
|
|
40
40
|
import json
|
|
41
|
+
import os
|
|
41
42
|
import pathlib
|
|
42
43
|
import sys
|
|
43
44
|
|
|
44
|
-
entries_dir = pathlib.Path(
|
|
45
|
-
output_file = pathlib.Path(
|
|
45
|
+
entries_dir = pathlib.Path(os.environ['ENTRIES_DIR'])
|
|
46
|
+
output_file = pathlib.Path(os.environ['NAMESPACE_PATH'])
|
|
46
47
|
|
|
47
48
|
def deep_merge(target: dict, source: dict) -> dict:
|
|
48
49
|
for k, v in source.items():
|
|
@@ -71,6 +72,10 @@ output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
71
72
|
output_file.write_text(json.dumps(merged, indent=2))
|
|
72
73
|
"
|
|
73
74
|
EOT
|
|
75
|
+
environment = {
|
|
76
|
+
ENTRIES_DIR = local.entries_dir
|
|
77
|
+
NAMESPACE_PATH = local.namespace_path
|
|
78
|
+
}
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|