diff --git a/agentcore-gateway-eventbridge-cdk/README.md b/agentcore-gateway-eventbridge-cdk/README.md new file mode 100644 index 000000000..a875c6a1e --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/README.md @@ -0,0 +1,103 @@ +# Amazon Bedrock AgentCore Runtime to Amazon EventBridge via AgentCore Gateway + +This pattern demonstrates how an AI agent on **AgentCore Runtime** emits structured business events to **EventBridge** through a governed **AgentCore Gateway MCP tool**, authenticated with **IAM (SigV4)**. The Gateway provides governance, observability, and schema control over what the agent can emit — without the agent needing direct access to the EventBridge SDK. + +The CDK stack is **fully self-contained**: it builds and deploys the agent container, the Gateway with its Lambda tool backend, and an EventBridge custom bus. + +![Architecture](architecture.png) + +``` +Strands Agent (AgentCore Runtime) + │ MCP Streamable HTTP, SigV4-signed + ▼ +AgentCore Gateway (authorizerType=AWS_IAM) + │ emit_event tool + ▼ +Lambda tool backend (validates + PutEvents) + │ + ▼ +EventBridge Custom Bus +``` + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/ + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Why route through AgentCore Gateway instead of calling EventBridge directly? + +In a mesh of many agents, Gateway is a governed chokepoint between agent reasoning and infrastructure side effects: + +- **Single point of schema enforcement** — one tool definition constrains what every agent in the mesh can emit. +- **Centralized rate limiting across the fleet** — throughput budgets are enforced at the Gateway, not per agent. +- **Blast radius containment** — only the Gateway's backend touches EventBridge; a misbehaving agent can't take down the bus. +- **Credential isolation** — agents authenticate with scoped, revocable identities +- **Tool discovery in the mesh** — agents find the `emit_event` capability over MCP without any hardcoded SDK dependency. +- **Observability without per-agent instrumentation** — every tool invocation is logged centrally, out of the box. +- **Policy evolution without redeployment** — schema tightening, freezes, or scope changes ship at the Gateway, not in agent code. + +The tradeoff is added network latency per emission, which is generally negligible for asynchronous event-driven workflows. + +## How it works + +1. The agent (Strands, Claude Haiku 4.5) connects to the AgentCore Gateway via the **MCP Streamable HTTP transport** (2025-03-26 spec). +2. The Gateway's inbound authorization is **`AWS_IAM`** — every request must carry a valid AWS SigV4 signature (service `bedrock-agentcore`). The Runtime's execution role is granted `bedrock-agentcore:InvokeGateway` scoped to the Gateway ARN. +3. **No MCP client SDK signs streamable-HTTP requests with SigV4 natively.** This pattern signs requests manually: [`agent-code/sigv4.py`](agent-code/sigv4.py) wraps `botocore.auth.SigV4Auth` as an `httpx.Auth` implementation and passes it to `streamablehttp_client(url, auth=sigv4_auth)`. +4. The Gateway exposes an `emit_event` tool backed by a Lambda function. +5. When the agent decides to emit an event, it calls `emit_event` with `source`, `detail_type`, and `detail`. +6. The Lambda validates the source prefix (`agent.*` only) and calls `events:PutEvents` on the custom bus. + +## Prerequisites + +- [AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) with sufficient permissions +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cli.html) installed and configured +- [Node.js 20+](https://nodejs.org/en/download/) and npm +- [AWS CDK CLI](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (`npm i -g aws-cdk`), bootstrapped in the target account/region +- [Docker](https://docs.docker.com/get-docker/) installed and running +- Access to the Amazon Bedrock Claude Haiku 4.5 model (enable in the Amazon Bedrock console) + +## Deployment + +```bash +git clone https://github.com/aws-samples/serverless-patterns +cd serverless-patterns/agentcore-gateway-eventbridge-cdk/cdk +npm install +cdk deploy +``` + +Note the stack outputs — in particular `AgentRuntimeArn`. + +## Testing + +Invoke the agent with a prompt that triggers the `emit_event` tool: + +```bash +RUNTIME_ARN="" + +aws bedrock-agentcore invoke-agent-runtime \ + --agent-runtime-arn "$RUNTIME_ARN" \ + --qualifier DEFAULT \ + --runtime-session-id "test-session-$(uuidgen | tr -d '-')" \ + --payload '{"prompt": "Use the emit_event tool to emit an event with source=agent.claims-processor, detail_type=ClaimApproved, detail={claimId: CLM-001, decision: approved, confidence: 0.94}"}' \ + --region us-east-1 +``` + +Verify success in the Runtime's CloudWatch Logs (`/aws/bedrock-agentcore/runtimes/-DEFAULT`): + +- `POST https:///mcp "HTTP/1.1 200 OK"` confirms the SigV4-signed request to the Gateway succeeded. +- The agent's response includes the EventBridge `Event ID` and a `Failed Count: 0`. + +Common failure causes: +- `403 Forbidden` from the Gateway → the Runtime role is missing `bedrock-agentcore:InvokeGateway` on the Gateway ARN, or the SigV4 signature is malformed (check that the `connection` header was stripped before signing). +- `AttributeError` on tool listing → ensure `strands-agents` and `mcp` package versions are compatible (see `agent-code/requirements.txt`). + +## Cleanup + +```bash +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/.dockerignore b/agentcore-gateway-eventbridge-cdk/agent-code/.dockerignore new file mode 100644 index 000000000..caf4e5aa4 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.git +.gitignore +.venv diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/.gitignore b/agentcore-gateway-eventbridge-cdk/agent-code/.gitignore new file mode 100644 index 000000000..309de5763 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +.venv diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/Dockerfile b/agentcore-gateway-eventbridge-cdk/agent-code/Dockerfile new file mode 100644 index 000000000..acc74f75f --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/Dockerfile @@ -0,0 +1,20 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +RUN useradd -m -u 1000 bedrock_agentcore +USER bedrock_agentcore + +EXPOSE 8080 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/ping || exit 1 + +CMD ["python", "agent.py"] diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/agent.py b/agentcore-gateway-eventbridge-cdk/agent-code/agent.py new file mode 100644 index 000000000..8061cd741 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/agent.py @@ -0,0 +1,67 @@ +""" +Strands Agent on AgentCore Runtime that connects to an AgentCore Gateway +to discover and use the emit_event MCP tool. + +The agent connects to the Gateway using the MCP Streamable HTTP transport. +Authentication: the Gateway's authorizerType is AWS_IAM, so every MCP +request must be signed with SigV4 (service "bedrock-agentcore"). The +Runtime's execution role is granted bedrock-agentcore:InvokeGateway +scoped to this Gateway's ARN. See sigv4.py for the signing implementation +— no MCP client SDK signs streamable-HTTP requests natively, so this is +done manually by wrapping botocore's SigV4Auth as an httpx.Auth. +""" +import os +import logging + +from bedrock_agentcore import BedrockAgentCoreApp +from strands import Agent +from strands.tools.mcp import MCPClient +from mcp.client.streamable_http import streamablehttp_client + +from sigv4 import SigV4HTTPXAuth + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = BedrockAgentCoreApp() + +GATEWAY_URL = os.environ.get("GATEWAY_MCP_URL", "") +AWS_REGION = os.environ.get("AWS_REGION", "us-east-1") + + +def create_mcp_client(): + """Create a fresh, SigV4-authenticated MCP client per invocation.""" + if not GATEWAY_URL: + return None + sigv4_auth = SigV4HTTPXAuth(region=AWS_REGION) + return MCPClient(lambda: streamablehttp_client(GATEWAY_URL, auth=sigv4_auth)) + + +@app.entrypoint +def invoke(payload: dict) -> dict: + """Process a request and let the agent decide whether to emit events.""" + prompt = payload.get("prompt", "No prompt provided.") + logger.info("Received prompt: %s", prompt[:200]) + + try: + mcp_client = create_mcp_client() + if mcp_client: + with mcp_client: + agent = Agent( + model="us.anthropic.claude-haiku-4-5-20251001-v1:0", + tools=mcp_client.list_tools_sync(), + ) + result = agent(prompt) + else: + agent = Agent(model="us.anthropic.claude-haiku-4-5-20251001-v1:0") + result = agent(prompt) + + logger.info("Agent completed: %s", str(result)[:500]) + return {"status": "completed", "result": str(result)[:2000]} + except Exception as e: + logger.exception("Agent invocation failed") + return {"status": "error", "error": str(e)[:500]} + + +if __name__ == "__main__": + app.run() diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/requirements.txt b/agentcore-gateway-eventbridge-cdk/agent-code/requirements.txt new file mode 100644 index 000000000..d4c4ad71a --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/requirements.txt @@ -0,0 +1,2 @@ +strands-agents==1.50.2 +bedrock-agentcore==1.18.1 diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/sigv4.py b/agentcore-gateway-eventbridge-cdk/agent-code/sigv4.py new file mode 100644 index 000000000..53b10f104 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/sigv4.py @@ -0,0 +1,42 @@ +""" +SigV4 authentication for the MCP streamable-HTTP transport. + +The MCP Python SDK's streamablehttp_client is a plain httpx-based client +with no native AWS SigV4 support. AgentCore Gateway's AWS_IAM inbound +authorizer requires each HTTP request to be signed with SigV4 (service +"bedrock-agentcore"). This wraps botocore's SigV4Auth as an httpx.Auth +so it can be passed directly to streamablehttp_client's `auth=` parameter. + +Reference pattern: awslabs/agentcore-samples gatewaylabproject/streamable_http_sigv4.py +""" +import boto3 +import httpx +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest + + +class SigV4HTTPXAuth(httpx.Auth): + """httpx.Auth implementation that signs requests with AWS SigV4.""" + + def __init__(self, region: str, service: str = "bedrock-agentcore"): + session = boto3.Session() + credentials = session.get_credentials() + if credentials is None: + raise RuntimeError("No AWS credentials available to sign Gateway requests") + self._signer = SigV4Auth(credentials, service, region) + + def auth_flow(self, request: httpx.Request): + headers = dict(request.headers) + # The "connection" header is not part of the canonical request and + # including it breaks the signature validation on the server side. + headers.pop("connection", None) + + aws_request = AWSRequest( + method=request.method, + url=str(request.url), + data=request.content, + headers=headers, + ) + self._signer.add_auth(aws_request) + request.headers.update(dict(aws_request.headers)) + yield request diff --git a/agentcore-gateway-eventbridge-cdk/architecture.png b/agentcore-gateway-eventbridge-cdk/architecture.png new file mode 100644 index 000000000..453183d5b Binary files /dev/null and b/agentcore-gateway-eventbridge-cdk/architecture.png differ diff --git a/agentcore-gateway-eventbridge-cdk/cdk/.gitignore b/agentcore-gateway-eventbridge-cdk/cdk/.gitignore new file mode 100644 index 000000000..459d58545 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/.gitignore @@ -0,0 +1,7 @@ +node_modules +cdk.out +*.js +!jest.config.js +*.d.ts +.cdk.staging +*.tsbuildinfo diff --git a/agentcore-gateway-eventbridge-cdk/cdk/bin/app.ts b/agentcore-gateway-eventbridge-cdk/cdk/bin/app.ts new file mode 100644 index 000000000..9e4a4a004 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/bin/app.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { AgentCoreGatewayEventBridgeStack } from '../lib/agentcore-gateway-eventbridge-stack'; + +const app = new cdk.App(); + +new AgentCoreGatewayEventBridgeStack(app, 'AgentCoreGatewayEventBridgeStack', { + description: + 'ServerlessLand pattern: AgentCore Runtime agent emits events to EventBridge via AgentCore Gateway MCP tool', + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/agentcore-gateway-eventbridge-cdk/cdk/cdk.json b/agentcore-gateway-eventbridge-cdk/cdk/cdk.json new file mode 100644 index 000000000..020a631ce --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/cdk.json @@ -0,0 +1,21 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "node_modules", + "cdk.out" + ] + }, + "context": { + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true + } +} diff --git a/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts b/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts new file mode 100644 index 000000000..43b6e1b39 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts @@ -0,0 +1,199 @@ +import * as path from 'path'; +import * as cdk from 'aws-cdk-lib'; +import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore'; +import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Construct } from 'constructs'; + +/** + * AgentCore Runtime → AgentCore Gateway → EventBridge (Outbound pattern) + * + * An AI agent on AgentCore Runtime emits structured business events to + * EventBridge through a governed AgentCore Gateway MCP tool. + * + * Flow: + * Strands Agent (Runtime) → emit_event tool (Gateway MCP, Streamable HTTP) + * → Lambda tool backend → events:PutEvents → Custom Bus + * + * Why Gateway over direct SDK: + * - Governance: Gateway tool definition constrains allowed event schemas + * - Observability: Gateway logs every tool invocation automatically + * - Rate limiting: Gateway enforces per-tool rate limits + * - Schema evolution: Update Gateway tool definition only, no agent redeploy + * - Multi-agent consistency: All agents use the same governed tool + * + * Auth model: + * - Runtime → Gateway: authorizerType=AWS_IAM (SigV4). The Runtime's + * execution role is granted bedrock-agentcore:InvokeGateway scoped to + * this Gateway's ARN. The agent code signs each MCP HTTP request with + * SigV4 (service "bedrock-agentcore") since no MCP client SDK does this + * natively for the streamable-HTTP transport — see agent-code/agent.py. + * - Runtime invocation (external caller): IAM SigV4 — caller needs + * bedrock-agentcore:InvokeAgentRuntime permission. + * - Gateway → Lambda: Gateway IAM role has lambda:InvokeFunction, scoped + * to the specific Lambda ARN. + */ +export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // ------------------------------------------------------------------- + // 1. EventBridge Custom Bus + // ------------------------------------------------------------------- + const eventBus = new events.EventBus(this, 'AgentEventBus', { + eventBusName: 'agent-outbound-events', + }); + + // ------------------------------------------------------------------- + // 2. Lambda Tool Backend (emit_event) + // ------------------------------------------------------------------- + const emitEventFn = new lambda.Function(this, 'EmitEventFunction', { + functionName: 'agentcore-emit-event', + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'handler.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'src', 'emit_event')), + environment: { + EVENT_BUS_NAME: eventBus.eventBusName, + ALLOWED_SOURCES: 'agent.', + }, + timeout: cdk.Duration.seconds(10), + }); + + eventBus.grantPutEventsTo(emitEventFn); + + // ------------------------------------------------------------------- + // 3. AgentCore Gateway (MCP server with Lambda target, IAM/SigV4 auth) + // ------------------------------------------------------------------- + const gatewayRole = new iam.Role(this, 'GatewayRole', { + assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com', { + conditions: { + StringEquals: { 'aws:SourceAccount': this.account }, + ArnLike: { 'aws:SourceArn': `arn:aws:bedrock-agentcore:${this.region}:${this.account}:*` }, + }, + }), + inlinePolicies: { + GatewayPolicy: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: [emitEventFn.functionArn], + }), + new iam.PolicyStatement({ + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`], + }), + ], + }), + }, + }); + + const gateway = new bedrockagentcore.CfnGateway(this, 'EventEmitterGateway', { + name: 'event-emitter-gateway', + authorizerType: 'AWS_IAM', + protocolType: 'MCP', + protocolConfiguration: { + mcp: { + supportedVersions: ['2025-03-26'], + }, + }, + roleArn: gatewayRole.roleArn, + description: 'MCP Gateway (IAM/SigV4 auth) exposing emit_event tool for agents to publish events to EventBridge', + }); + + emitEventFn.addPermission('GatewayInvoke', { + principal: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'), + sourceArn: gateway.attrGatewayArn, + }); + + new bedrockagentcore.CfnGatewayTarget(this, 'EmitEventTarget', { + gatewayIdentifier: gateway.attrGatewayIdentifier, + name: 'emit-event-target', + description: 'Lambda tool backend that publishes events to EventBridge', + credentialProviderConfigurations: [ + { credentialProviderType: 'GATEWAY_IAM_ROLE' }, + ], + targetConfiguration: { + mcp: { + lambda: { + lambdaArn: emitEventFn.functionArn, + toolSchema: { + inlinePayload: [ + { + name: 'emit_event', + description: 'Emit a structured business event to the EventBridge bus. Use this to publish results, decisions, or state changes that other systems or agents should react to.', + inputSchema: { + type: 'object', + properties: { + source: { type: 'string', description: "Event source identifier. Must start with 'agent.'" }, + detail_type: { type: 'string', description: "Event type (e.g. 'ClaimApproved', 'RiskAssessed')" }, + detail: { type: 'object', description: 'Event payload with business data' }, + }, + required: ['source', 'detail_type', 'detail'], + }, + }, + ], + }, + }, + }, + }, + }); + + // ------------------------------------------------------------------- + // 4. AgentCore Runtime (self-contained agent) + // ------------------------------------------------------------------- + const agentImage = new ecrAssets.DockerImageAsset(this, 'AgentImage', { + directory: path.join(__dirname, '..', '..', 'agent-code'), + platform: ecrAssets.Platform.LINUX_ARM64, + }); + + const agentRuntimeName = 'agentcore_gateway_eventbridge_demo'; + + const agentRuntimeRole = new iam.Role(this, 'AgentRuntimeRole', { + assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com', { + conditions: { + StringEquals: { 'aws:SourceAccount': this.account }, + ArnLike: { 'aws:SourceArn': `arn:aws:bedrock-agentcore:${this.region}:${this.account}:*` }, + }, + }), + inlinePolicies: { + AgentRuntimePolicy: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ sid: 'ECRImageAccess', actions: ['ecr:BatchGetImage', 'ecr:GetDownloadUrlForLayer'], resources: [agentImage.repository.repositoryArn] }), + new iam.PolicyStatement({ sid: 'ECRTokenAccess', actions: ['ecr:GetAuthorizationToken'], resources: ['*'] }), + new iam.PolicyStatement({ actions: ['logs:DescribeLogStreams', 'logs:CreateLogGroup', 'logs:DescribeLogGroups'], resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/*`] }), + new iam.PolicyStatement({ actions: ['logs:CreateLogStream', 'logs:PutLogEvents'], resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*`] }), + new iam.PolicyStatement({ actions: ['xray:PutTraceSegments', 'xray:PutTelemetryRecords', 'xray:GetSamplingRules', 'xray:GetSamplingTargets'], resources: ['*'] }), + new iam.PolicyStatement({ actions: ['cloudwatch:PutMetricData'], resources: ['*'], conditions: { StringEquals: { 'cloudwatch:namespace': 'bedrock-agentcore' } } }), + new iam.PolicyStatement({ sid: 'GetAgentAccessToken', actions: ['bedrock-agentcore:GetWorkloadAccessToken', 'bedrock-agentcore:GetWorkloadAccessTokenForJWT', 'bedrock-agentcore:GetWorkloadAccessTokenForUserId'], resources: [`arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default`, `arn:aws:bedrock-agentcore:${this.region}:${this.account}:workload-identity-directory/default/workload-identity/${agentRuntimeName}-*`] }), + new iam.PolicyStatement({ sid: 'BedrockModelInvocation', actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'], resources: ['arn:aws:bedrock:*::foundation-model/*', `arn:aws:bedrock:${this.region}:${this.account}:*`] }), + new iam.PolicyStatement({ + sid: 'InvokeGateway', + actions: ['bedrock-agentcore:InvokeGateway'], + resources: [gateway.attrGatewayArn], + }), + ], + }), + }, + }); + + const agentRuntime = new bedrockagentcore.CfnRuntime(this, 'AgentRuntime', { + agentRuntimeName, + agentRuntimeArtifact: { containerConfiguration: { containerUri: agentImage.imageUri } }, + networkConfiguration: { networkMode: 'PUBLIC' }, + roleArn: agentRuntimeRole.roleArn, + environmentVariables: { GATEWAY_MCP_URL: gateway.attrGatewayUrl }, + }); + + // ------------------------------------------------------------------- + // Outputs + // ------------------------------------------------------------------- + new cdk.CfnOutput(this, 'GatewayUrl', { value: gateway.attrGatewayUrl, description: 'AgentCore Gateway MCP endpoint URL' }); + new cdk.CfnOutput(this, 'GatewayId', { value: gateway.attrGatewayIdentifier, description: 'Gateway identifier' }); + new cdk.CfnOutput(this, 'GatewayArn', { value: gateway.attrGatewayArn, description: 'Gateway ARN' }); + new cdk.CfnOutput(this, 'AgentRuntimeId', { value: agentRuntime.attrAgentRuntimeId, description: 'AgentCore Runtime ID' }); + new cdk.CfnOutput(this, 'AgentRuntimeArn', { value: agentRuntime.attrAgentRuntimeArn, description: 'AgentCore Runtime ARN (use for SigV4 invocation)' }); + new cdk.CfnOutput(this, 'EventBusName', { value: eventBus.eventBusName, description: 'EventBridge custom bus for agent-emitted events' }); + } +} diff --git a/agentcore-gateway-eventbridge-cdk/cdk/package.json b/agentcore-gateway-eventbridge-cdk/cdk/package.json new file mode 100644 index 000000000..7ab4012ce --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/package.json @@ -0,0 +1,25 @@ +{ + "name": "agentcore-gateway-eventbridge-cdk", + "version": "1.0.0", + "description": "AgentCore Runtime agent emits events to EventBridge via AgentCore Gateway MCP tool", + "bin": { + "app": "bin/app.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk", + "deploy": "cdk deploy", + "destroy": "cdk destroy" + }, + "devDependencies": { + "@types/node": "20.14.9", + "aws-cdk": "2.1136.0", + "ts-node": "10.9.2", + "typescript": "5.5.3" + }, + "dependencies": { + "aws-cdk-lib": "2.264.0", + "constructs": "10.8.1" + } +} diff --git a/agentcore-gateway-eventbridge-cdk/cdk/tsconfig.json b/agentcore-gateway-eventbridge-cdk/cdk/tsconfig.json new file mode 100644 index 000000000..b1eaa510e --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["es2022"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/agentcore-gateway-eventbridge-cdk/example-pattern.json b/agentcore-gateway-eventbridge-cdk/example-pattern.json new file mode 100644 index 000000000..868ca015d --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/example-pattern.json @@ -0,0 +1,123 @@ +{ + "title": "Amazon Bedrock AgentCore Runtime to Amazon EventBridge via AgentCore Gateway", + "description": "An AI agent on AgentCore Runtime emits business events to EventBridge through a governed AgentCore Gateway MCP tool, authenticated with IAM SigV4.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "patternArch": { + "icon1": { + "x": 10, + "y": 50, + "service": "bedrock", + "label": "AgentCore Runtime" + }, + "icon2": { + "x": 35, + "y": 20, + "service": "bedrock", + "label": "Amazon Bedrock" + }, + "icon3": { + "x": 40, + "y": 50, + "service": "bedrock", + "label": "AgentCore Gateway" + }, + "icon4": { + "x": 70, + "y": 50, + "service": "lambda", + "label": "emit_event Lambda" + }, + "icon5": { + "x": 95, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon1", + "to": "icon3" + }, + "line3": { + "from": "icon3", + "to": "icon4" + }, + "line4": { + "from": "icon4", + "to": "icon5" + } + }, + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows an AI agent emitting structured business events to Amazon EventBridge through a governed Amazon Bedrock AgentCore Gateway MCP tool.", + "The agent runs on AgentCore Runtime and connects to the Gateway using the MCP Streamable HTTP transport (2025-03-26 spec). The Gateway's inbound authorization is AWS_IAM, so every request must carry a valid AWS SigV4 signature for the bedrock-agentcore service.", + "No MCP client SDK signs streamable-HTTP requests with SigV4 natively, so this pattern signs requests manually: a small helper wraps botocore's SigV4Auth as an httpx.Auth implementation, passed directly to the MCP client's transport.", + "The Gateway exposes an emit_event tool backed by an AWS Lambda function. When the agent calls emit_event with a source, detail type, and payload, the Lambda validates the source prefix (only agent.* allowed) and publishes to EventBridge via PutEvents.", + "IAM permissions follow least privilege: the Runtime execution role has bedrock-agentcore:InvokeGateway scoped to the specific Gateway ARN, the Gateway role can only invoke the specific Lambda, and the Lambda can only PutEvents to the specific bus." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/agentcore-gateway-eventbridge-cdk", + "templateURL": "serverless-patterns/agentcore-gateway-eventbridge-cdk", + "projectFolder": "agentcore-gateway-eventbridge-cdk", + "templateFile": "cdk/lib/agentcore-gateway-eventbridge-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock AgentCore Gateway", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html" + }, + { + "text": "Set up inbound authorization for your gateway (IAM/SigV4)", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-inbound-auth.html" + }, + { + "text": "Amazon Bedrock AgentCore Runtime", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" + }, + { + "text": "MCP Streamable HTTP Transport", + "link": "https://modelcontextprotocol.io/specification/2025-03-26/basic/transports" + }, + { + "text": "Amazon EventBridge PutEvents", + "link": "https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEvents.html" + } + ] + }, + "deploy": { + "text": [ + "cd cdk", + "npm install", + "cdk deploy" + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo README.md for detailed testing instructions.", + "Sample invocation payload: {\"prompt\":\"Use the emit_event tool to emit an event with source=agent.claims-processor, detail_type=ClaimApproved, detail={claimId: CLM-001, decision: approved, confidence: 0.94}\"}", + "After invoking, check the Runtime's CloudWatch Logs for a 200 OK from the Gateway MCP endpoint and the EventBridge event ID returned by the Lambda." + ] + }, + "cleanup": { + "headline": "Cleanup", + "text": ["cd cdk", "cdk destroy"] + }, + "authors": [ + { + "name": "Antoine Boucherie", + "bio": "Principal Solutions Architect, AWS Global Financial Services", + "linkedin": "antoineboucherie" + } + ] +} diff --git a/agentcore-gateway-eventbridge-cdk/src/emit_event/handler.py b/agentcore-gateway-eventbridge-cdk/src/emit_event/handler.py new file mode 100644 index 000000000..e03d545ee --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/src/emit_event/handler.py @@ -0,0 +1,73 @@ +""" +AgentCore Gateway tool backend: emit_event + +Receives tool invocations from the AgentCore Gateway (MCP Lambda target), +validates the event payload, and publishes to an EventBridge custom bus. + +The Gateway provides governance: schema validation on the tool input, +JWT authentication, rate limiting, and observability. This Lambda focuses +on the EventBridge integration and source-prefix enforcement. +""" +import boto3 +import json +import os + +events_client = boto3.client("events") +ALLOWED_SOURCES = os.environ.get("ALLOWED_SOURCES", "agent.").split(",") +EVENT_BUS_NAME = os.environ["EVENT_BUS_NAME"] + + +def handler(event, context): + """Handle tool invocation from AgentCore Gateway.""" + # Gateway sends the tool input as the Lambda event body + body = event if isinstance(event, dict) and "source" in event else json.loads(event.get("body", "{}")) + + source = body.get("source", "") + detail_type = body.get("detail_type", "") + detail = body.get("detail", {}) + notify = body.get("notify", False) + + # Validate required fields + if not source or not detail_type or not detail: + return { + "statusCode": 400, + "body": json.dumps({"error": "source, detail_type, and detail are required"}), + } + + # Validate source prefix (governance: agents can only emit from allowed namespaces) + if not any(source.startswith(prefix) for prefix in ALLOWED_SOURCES): + return { + "statusCode": 403, + "body": json.dumps( + {"error": f"Source must start with one of: {ALLOWED_SOURCES}"} + ), + } + + # Add notify flag to detail for downstream rule filtering + if notify: + detail["notify"] = True + + # Emit to EventBridge + response = events_client.put_events( + Entries=[ + { + "Source": source, + "DetailType": detail_type, + "Detail": json.dumps(detail), + "EventBusName": EVENT_BUS_NAME, + } + ] + ) + + failed_count = response["FailedEntryCount"] + + return { + "statusCode": 200 if failed_count == 0 else 207, + "body": json.dumps( + { + "success": failed_count == 0, + "failed_count": failed_count, + "event_id": response["Entries"][0].get("EventId", ""), + } + ), + }