diff --git a/agentcore-gateway-eventbridge-cdk.json b/agentcore-gateway-eventbridge-cdk.json new file mode 100644 index 000000000..868ca015d --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk.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/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", ""), + } + ), + } diff --git a/eventbridge-apidestination-agentcore-cdk.json b/eventbridge-apidestination-agentcore-cdk.json new file mode 100644 index 000000000..afbb29288 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk.json @@ -0,0 +1,120 @@ +{ + "title": "Amazon EventBridge API Destination to Amazon Bedrock AgentCore Runtime", + "description": "Invoke an AgentCore Runtime agent directly from EventBridge without any Lambda glue code, using an API Destination with Cognito M2M OAuth.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "eventbridge-api-destination", + "label": "API Destination" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "bedrock", + "label": "Amazon Bedrock AgentCore Runtime" + }, + "icon4": { + "x": 50, + "y": 20, + "service": "cognito", + "label": "Amazon Cognito (M2M OAuth)" + }, + "icon5": { + "x": 50, + "y": 80, + "service": "sqs", + "label": "Dead-letter queue" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon4", + "to": "icon2" + }, + "line4": { + "from": "icon2", + "to": "icon5" + } + }, + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows a Lambda-less, event-driven invocation of an Amazon Bedrock AgentCore Runtime agent. The CDK stack is self-contained: it builds and deploys the AgentCore Runtime (from a bundled agent container image) along with all the EventBridge plumbing.", + "An EventBridge rule matches events on a custom bus and routes them to an API Destination whose HTTPS endpoint is the AgentCore Runtime InvokeAgentRuntime API.", + "The EventBridge Connection authenticates using the OAuth client_credentials flow against an Amazon Cognito user pool (machine-to-machine). The AgentCore Runtime validates the resulting JWT via its inbound identity (customJwtAuthorizer) configuration, which the stack wires to the same Cognito user pool at creation time.", + "Because API Destinations enforce a 5-second response timeout, the AgentCore Runtime is invoked in asynchronous mode: the agent entrypoint acknowledges the request immediately and continues processing the event in the background.", + "The endpoint URL uses the agent runtime ID plus an accountId query parameter instead of a URL-encoded ARN, because API Destinations automatically decode percent-encoded sequences in target URLs.", + "Deliveries that fail after retries are captured in an SQS dead-letter queue for inspection and redrive.", + "IAM permissions follow least privilege: the AgentCore Runtime execution role grants only ecr:BatchGetImage and ecr:GetDownloadUrlForLayer to the specific ECR repository, scoped CloudWatch Logs and X-Ray permissions, and bedrock:InvokeModel for foundation models. The EventBridge rule's target role has only events:InvokeApiDestination on the specific API Destination. The SQS DLQ enforces SSL in transit." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-apidestination-agentcore-cdk", + "templateURL": "serverless-patterns/eventbridge-apidestination-agentcore-cdk", + "projectFolder": "eventbridge-apidestination-agentcore-cdk", + "templateFile": "cdk/lib/eventbridge-agentcore-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon EventBridge API Destinations", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html" + }, + { + "text": "Amazon Bedrock AgentCore Runtime", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" + }, + { + "text": "AgentCore Runtime inbound JWT authorizer", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html" + }, + { + "text": "Cognito machine-to-machine authorization (client credentials grant)", + "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html" + } + ] + }, + "deploy": { + "text": [ + "cd cdk", + "npm install", + "cdk deploy" + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo README.md for detailed testing instructions.", + "Sample test event payload: {\"EventBusName\":\"agentcore-events\",\"Source\":\"demo.orders\",\"DetailType\":\"OrderCreated\",\"Detail\":\"{\\\"orderId\\\":\\\"12345\\\",\\\"prompt\\\":\\\"Summarize this order and flag any anomalies.\\\"}\"}" + ] + }, + "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/eventbridge-apidestination-agentcore-cdk/README.md b/eventbridge-apidestination-agentcore-cdk/README.md new file mode 100644 index 000000000..d2d1d21dc --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/README.md @@ -0,0 +1,134 @@ +# Amazon EventBridge API Destination to Amazon Bedrock AgentCore Runtime + +This pattern demonstrates **Lambda-less, event-driven invocation of an AI agent**: an EventBridge rule delivers events directly to an Amazon Bedrock AgentCore Runtime endpoint via an API Destination, authenticated with Cognito machine-to-machine (M2M) OAuth. No Lambda function, no glue code. + +The CDK stack is **fully self-contained** — it builds and deploys the AgentCore Runtime (from the bundled `agent-code/` Docker image) alongside the EventBridge plumbing, so a single `cdk deploy` gives you a working, testable pattern. + +![Architecture](architecture.png) + +``` +EventBridge Rule ──▶ API Destination (HTTPS + OAuth) ──▶ AgentCore Runtime + │ │ │ +custom event bus Connection: Cognito async processing +(demo.orders) client_credentials JWT (ack < 5s, work in + background) +``` + +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. + +## How it works + +1. An event (e.g. `source: demo.orders`, `detail-type: OrderCreated`) is published to a custom event bus. +2. An EventBridge rule matches the event and forwards it to an **API Destination** whose endpoint is the AgentCore Runtime `InvokeAgentRuntime` HTTPS API. +3. The API Destination's **Connection** obtains an OAuth access token from a **Cognito user pool token endpoint** using the `client_credentials` grant, and attaches it as a Bearer token. +4. The AgentCore Runtime validates the JWT against the Cognito user pool (inbound identity / `customJwtAuthorizer`), **acknowledges the request within 5 seconds**, and processes the event **asynchronously**. +5. Failed deliveries (after 3 retries) are sent to an SQS dead-letter queue. + +## Key technical details + +### 1. The 5-second timeout → async execution + +EventBridge API Destinations enforce a hard **5-second response timeout**. Agent reasoning takes much longer than that. The AgentCore Runtime therefore runs in **asynchronous mode**: the agent entrypoint returns an acknowledgment immediately (HTTP 2xx) and continues working in the background. See [`agent-code/agent.py`](agent-code/agent.py) for the implementation — it uses `asyncio.create_task` to kick off the real work, then returns `{"status": "accepted"}` well within the 5-second window. + +```python +from bedrock_agentcore import BedrockAgentCoreApp +import asyncio + +app = BedrockAgentCoreApp() + +@app.entrypoint +async def invoke(payload): + # Kick off long-running agent work in the background + asyncio.create_task(process_event(payload)) + # Acknowledge within the 5-second API Destination timeout + return {"status": "accepted"} +``` + +### 2. The URL-encoding gotcha → use the agent ID, not the ARN + +API Destinations **automatically decode `%XX` sequences** in the endpoint URL. A URL-encoded runtime ARN in the path (containing `:` and `/`) gets decoded back and breaks the request signature/routing. + +The fix: use the **agent runtime ID in the path** and pass the **account ID as a query parameter**. Per the AWS docs: *"When you use the agent ID instead of the full ARN, you don't need to URL-encode the identifier."* The stack derives this URL automatically from the runtime it creates (`CfnRuntime.attrAgentRuntimeId`): + +``` +https://bedrock-agentcore..amazonaws.com/runtimes//invocations?accountId=&qualifier=DEFAULT +``` + +### 3. Authentication → Cognito M2M (client_credentials) + +The stack creates: +- A **Cognito user pool** with a hosted domain (provides the `/oauth2/token` endpoint) +- A **resource server** (`agentcore`) with a custom scope (`agentcore/invoke`) +- An **app client** with a secret and the `client_credentials` grant + +The EventBridge Connection is configured with OAuth (client credentials) against the Cognito token endpoint. The AgentCore Runtime's `customJwtAuthorizer` is wired to the **same** user pool at creation time (its `discoveryUrl` and `allowedClients` reference the pool and app client this stack creates), so there is no manual post-deploy step. + +## 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 (the CDK build packages the agent into a container image) +- Access to the Amazon Bedrock model your agent uses (the bundled agent uses the [Strands](https://strandsagents.com/) default model; enable model access in the Amazon Bedrock console for your region) + +## Deployment + +1. Clone and enter the pattern directory: + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/eventbridge-apidestination-agentcore-cdk/cdk + npm install + ``` + +2. Deploy. The stack builds the agent container image, deploys the AgentCore Runtime, and wires up EventBridge — all in one command: + + ```bash + cdk deploy + ``` + +3. Note the stack outputs — in particular `EventBusName`, `AgentRuntimeId`, and `DeadLetterQueueUrl`. No further configuration is required: the runtime's JWT authorizer already trusts the Cognito app client created by this stack. + +## Testing + +Publish a test event to the custom bus (`EventBusName` output): + +```bash +aws events put-events --entries '[ + { + "EventBusName": "agentcore-events", + "Source": "demo.orders", + "DetailType": "OrderCreated", + "Detail": "{\"orderId\": \"12345\", \"prompt\": \"Summarize this order and flag any anomalies.\"}" + } +]' +``` + +Verify the invocation: + +1. **AgentCore Runtime logs** — check CloudWatch Logs for the runtime (`/aws/bedrock-agentcore/runtimes/-DEFAULT`) to see the event arrive and background processing run. +2. **Connection health** — `aws events describe-connection --name agentcore-cognito-oauth` should show `AUTHORIZED`. +3. **Failures** — if delivery fails after retries, events land in the DLQ: + + ```bash + aws sqs receive-message --queue-url + ``` + +Common failure causes: +- HTTP 401/403 in the DLQ → the Connection couldn't obtain or present a valid token (check the Connection status and the Cognito app client secret). +- Timeouts → the agent isn't acknowledging within 5 seconds (keep the entrypoint async; see `agent-code/agent.py`). + +## Cleanup + +```bash +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/eventbridge-apidestination-agentcore-cdk/agent-code/.dockerignore b/eventbridge-apidestination-agentcore-cdk/agent-code/.dockerignore new file mode 100644 index 000000000..caf4e5aa4 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/agent-code/.dockerignore @@ -0,0 +1,5 @@ +__pycache__ +*.pyc +.git +.gitignore +.venv diff --git a/eventbridge-apidestination-agentcore-cdk/agent-code/.gitignore b/eventbridge-apidestination-agentcore-cdk/agent-code/.gitignore new file mode 100644 index 000000000..309de5763 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/agent-code/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +.venv diff --git a/eventbridge-apidestination-agentcore-cdk/agent-code/Dockerfile b/eventbridge-apidestination-agentcore-cdk/agent-code/Dockerfile new file mode 100644 index 000000000..acc74f75f --- /dev/null +++ b/eventbridge-apidestination-agentcore-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/eventbridge-apidestination-agentcore-cdk/agent-code/agent.py b/eventbridge-apidestination-agentcore-cdk/agent-code/agent.py new file mode 100644 index 000000000..33a486e44 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/agent-code/agent.py @@ -0,0 +1,50 @@ +""" +Minimal async AgentCore Runtime entrypoint for the +EventBridge API Destination -> AgentCore Runtime pattern. + +Why async: EventBridge API Destinations enforce a hard 5-second response +timeout on the target endpoint. Agent reasoning (an LLM call via Strands) +routinely takes longer than that, so this entrypoint acknowledges the +request immediately (HTTP 2xx, well under 5s) and continues the actual +agent work in a background asyncio task. + +This is intentionally minimal so the pattern deploys and can be tested +end-to-end. Swap the Strands `Agent()` call for your own tools/model +config as needed. +""" +import asyncio +import logging + +from bedrock_agentcore import BedrockAgentCoreApp +from strands import Agent + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = BedrockAgentCoreApp() +agent = Agent(model="us.anthropic.claude-haiku-4-5-20251001-v1:0") + + +async def process_event(payload: dict) -> None: + """Runs the actual agent reasoning after the HTTP response has + already been returned to EventBridge. Errors here are logged only: + there is no caller left to report back to.""" + prompt = payload.get("prompt", "Summarize this event.") + order_id = payload.get("orderId", "unknown") + try: + result = agent(prompt) + logger.info("orderId=%s agent result: %s", order_id, result) + except Exception: + logger.exception("orderId=%s agent invocation failed", order_id) + + +@app.entrypoint +async def invoke(payload: dict) -> dict: + # Fire-and-forget the real work so we can return well within the + # API Destination's 5-second timeout. + asyncio.create_task(process_event(payload)) + return {"status": "accepted", "orderId": payload.get("orderId")} + + +if __name__ == "__main__": + app.run() diff --git a/eventbridge-apidestination-agentcore-cdk/agent-code/requirements.txt b/eventbridge-apidestination-agentcore-cdk/agent-code/requirements.txt new file mode 100644 index 000000000..d4c4ad71a --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/agent-code/requirements.txt @@ -0,0 +1,2 @@ +strands-agents==1.50.2 +bedrock-agentcore==1.18.1 diff --git a/eventbridge-apidestination-agentcore-cdk/architecture.png b/eventbridge-apidestination-agentcore-cdk/architecture.png new file mode 100644 index 000000000..1dc63d8ee Binary files /dev/null and b/eventbridge-apidestination-agentcore-cdk/architecture.png differ diff --git a/eventbridge-apidestination-agentcore-cdk/cdk/.gitignore b/eventbridge-apidestination-agentcore-cdk/cdk/.gitignore new file mode 100644 index 000000000..459d58545 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/cdk/.gitignore @@ -0,0 +1,7 @@ +node_modules +cdk.out +*.js +!jest.config.js +*.d.ts +.cdk.staging +*.tsbuildinfo diff --git a/eventbridge-apidestination-agentcore-cdk/cdk/bin/app.ts b/eventbridge-apidestination-agentcore-cdk/cdk/bin/app.ts new file mode 100644 index 000000000..f92ea1154 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/cdk/bin/app.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { EventBridgeAgentCoreStack } from '../lib/eventbridge-agentcore-stack'; + +const app = new cdk.App(); + +new EventBridgeAgentCoreStack(app, 'EventBridgeAgentCoreStack', { + description: + 'ServerlessLand pattern: EventBridge API Destination -> AgentCore Runtime (Lambda-less event-driven agent invocation)', + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/eventbridge-apidestination-agentcore-cdk/cdk/cdk.json b/eventbridge-apidestination-agentcore-cdk/cdk/cdk.json new file mode 100644 index 000000000..020a631ce --- /dev/null +++ b/eventbridge-apidestination-agentcore-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/eventbridge-apidestination-agentcore-cdk/cdk/lib/eventbridge-agentcore-stack.ts b/eventbridge-apidestination-agentcore-cdk/cdk/lib/eventbridge-agentcore-stack.ts new file mode 100644 index 000000000..169c3782f --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/cdk/lib/eventbridge-agentcore-stack.ts @@ -0,0 +1,328 @@ +import * as path from 'path'; +import * as cdk from 'aws-cdk-lib'; +import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore'; +import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { Construct } from 'constructs'; + +/** + * EventBridge API Destination -> Amazon Bedrock AgentCore Runtime + * "Lambda-less" event-driven agent invocation. + * + * Flow: + * EventBridge Rule -> API Destination (HTTPS, OAuth via Cognito M2M) + * -> AgentCore Runtime InvokeAgentRuntime endpoint (async processing) + * + * This stack is fully self-contained: it builds and deploys the AgentCore + * Runtime (from the bundled agent-code/ Docker image) alongside the + * EventBridge plumbing, so `cdk deploy` produces a working, testable + * pattern with no manual "update the runtime's authorizer" step. + * + * Key design decisions: + * + * 1. ASYNC execution: API Destinations enforce a hard 5-second timeout on + * target responses. Agent reasoning takes far longer than 5 seconds, so + * the AgentCore Runtime must acknowledge the request immediately (HTTP 2xx) + * and continue processing in the background (async invocation mode). + * See agent-code/agent.py for the entrypoint implementation. + * + * 2. URL encoding gotcha: API Destinations automatically decode %XX sequences + * in the endpoint URL. A URL-encoded runtime ARN in the path (which + * contains ":" and "/") gets decoded back and breaks the request. We + * therefore use the plain agent runtime ID (CfnRuntime.attrAgentRuntimeId) + * in the path and pass the account ID as a query parameter — no URL + * encoding needed. + * + * 3. Auth: Cognito User Pool with a Resource Server (client_credentials + * grant). The EventBridge Connection fetches OAuth tokens from the Cognito + * token endpoint; the AgentCore Runtime validates the JWT via its inbound + * identity (customJwtAuthorizer), configured at creation time against the + * same Cognito user pool — no two-step deploy required. + */ +export class EventBridgeAgentCoreStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // ------------------------------------------------------------------- + // 1. Cognito User Pool for machine-to-machine (M2M) authentication + // ------------------------------------------------------------------- + const userPool = new cognito.UserPool(this, 'AgentAuthUserPool', { + userPoolName: 'agentcore-m2m-pool', + selfSignUpEnabled: false, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + // Hosted domain is required so the OAuth2 token endpoint exists. + const userPoolDomain = userPool.addDomain('AgentAuthDomain', { + cognitoDomain: { + // Domain prefix must be globally unique per region. + domainPrefix: `agentcore-invoke-${this.account}`, + }, + }); + + // Resource server defines the custom scope granted to the M2M client. + const invokeScope = new cognito.ResourceServerScope({ + scopeName: 'invoke', + scopeDescription: 'Invoke the AgentCore Runtime', + }); + + const resourceServer = userPool.addResourceServer('AgentResourceServer', { + identifier: 'agentcore', + userPoolResourceServerName: 'agentcore', + scopes: [invokeScope], + }); + + // App client using the client_credentials grant (M2M, no user login). + const appClient = userPool.addClient('EventBridgeM2MClient', { + userPoolClientName: 'eventbridge-connection-client', + generateSecret: true, + oAuth: { + flows: { + clientCredentials: true, + }, + scopes: [cognito.OAuthScope.resourceServer(resourceServer, invokeScope)], + }, + authFlows: { + userSrp: false, + userPassword: false, + }, + }); + + const tokenEndpoint = `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com/oauth2/token`; + + // ------------------------------------------------------------------- + // 2. EventBridge Connection (OAuth client_credentials -> Cognito) + // ------------------------------------------------------------------- + const connection = new events.Connection(this, 'AgentCoreConnection', { + connectionName: 'agentcore-cognito-oauth', + description: + 'OAuth client_credentials connection to Cognito for AgentCore Runtime invocation', + authorization: events.Authorization.oauth({ + authorizationEndpoint: tokenEndpoint, + clientId: appClient.userPoolClientId, + clientSecret: appClient.userPoolClientSecret, + httpMethod: events.HttpMethod.POST, + bodyParameters: { + grant_type: events.HttpParameter.fromString('client_credentials'), + scope: events.HttpParameter.fromString('agentcore/invoke'), + }, + }), + }); + + // ------------------------------------------------------------------- + // 3. AgentCore Runtime — build from the bundled agent-code/ Dockerfile + // and deploy it, with its JWT authorizer pointed at the Cognito + // user pool created above. No separate "bring your own runtime" + // step: this stack is self-contained end to end. + // ------------------------------------------------------------------- + const agentImage = new ecrAssets.DockerImageAsset(this, 'AgentImage', { + directory: path.join(__dirname, '..', '..', 'agent-code'), + platform: ecrAssets.Platform.LINUX_ARM64, + }); + + const agentRuntimeName = 'eventbridge_apidestination_agentcore_demo'; + + // NOTE: these statements are attached as an INLINE policy on the role + // (not via role.addToPolicy, which would create a separate + // AWS::IAM::Policy resource). The CfnRuntime below only references the + // role's ARN, so CloudFormation would not otherwise wait for a separate + // policy to attach before creating the runtime — and the runtime + // assumes this role immediately to validate the ECR image. Keeping the + // permissions inline makes them part of the AWS::IAM::Role resource that + // the runtime depends on, avoiding an IAM propagation race. + 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'], + // ecr:GetAuthorizationToken does not support resource-level permissions. + resources: ['*'], + }), + new iam.PolicyStatement({ + actions: ['logs:DescribeLogStreams', 'logs:CreateLogGroup'], + resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:/aws/bedrock-agentcore/runtimes/*`], + }), + new iam.PolicyStatement({ + actions: ['logs:DescribeLogGroups'], + resources: [`arn:aws:logs:${this.region}:${this.account}:log-group:*`], + }), + 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'], + // X-Ray actions do not support resource-level permissions. + 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}:*`], + }), + ], + }), + }, + }); + + const discoveryUrl = `https://cognito-idp.${this.region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`; + + const agentRuntime = new bedrockagentcore.CfnRuntime(this, 'AgentRuntime', { + agentRuntimeName, + agentRuntimeArtifact: { + containerConfiguration: { + containerUri: agentImage.imageUri, + }, + }, + networkConfiguration: { + networkMode: 'PUBLIC', + }, + roleArn: agentRuntimeRole.roleArn, + authorizerConfiguration: { + customJwtAuthorizer: { + discoveryUrl, + allowedClients: [appClient.userPoolClientId], + }, + }, + }); + + // ------------------------------------------------------------------- + // 4. API Destination -> AgentCore Runtime InvokeAgentRuntime endpoint + // ------------------------------------------------------------------- + // NOTE: plain agent runtime ID in the path + accountId as a query + // parameter. Do NOT use the URL-encoded full ARN — API Destinations + // decode %XX sequences in the URL and would corrupt it. + const invocationEndpoint = + `https://bedrock-agentcore.${this.region}.amazonaws.com` + + `/runtimes/${agentRuntime.attrAgentRuntimeId}/invocations` + + `?accountId=${this.account}&qualifier=DEFAULT`; + + const apiDestination = new events.ApiDestination(this, 'AgentCoreApiDestination', { + apiDestinationName: 'agentcore-runtime-invoke', + connection, + endpoint: invocationEndpoint, + httpMethod: events.HttpMethod.POST, + rateLimitPerSecond: 10, + description: + 'Invokes the AgentCore Runtime asynchronously (runtime must ack within 5s)', + }); + + // ------------------------------------------------------------------- + // 5. Event bus, DLQ, and rule + // ------------------------------------------------------------------- + const eventBus = new events.EventBus(this, 'AgentEventBus', { + eventBusName: 'agentcore-events', + }); + + // Failed deliveries (after retries) land here for inspection/redrive. + const dlq = new sqs.Queue(this, 'DeliveryDlq', { + queueName: 'agentcore-invoke-dlq', + retentionPeriod: cdk.Duration.days(14), + enforceSSL: true, + }); + + const rule = new events.Rule(this, 'InvokeAgentRule', { + ruleName: 'invoke-agentcore-on-order-event', + eventBus, + description: 'Routes order events to the AgentCore Runtime via API Destination', + eventPattern: { + source: ['demo.orders'], + detailType: ['OrderCreated'], + }, + }); + + rule.addTarget( + new targets.ApiDestination(apiDestination, { + deadLetterQueue: dlq, + retryAttempts: 3, + maxEventAge: cdk.Duration.minutes(10), + // Shape the payload the agent receives. AgentCore Runtime expects a + // JSON body; the "prompt" key is what a typical agent entrypoint + // reads. Adjust to match your agent's input contract. + event: events.RuleTargetInput.fromObject({ + prompt: events.EventField.fromPath('$.detail.prompt'), + orderId: events.EventField.fromPath('$.detail.orderId'), + eventId: events.EventField.eventId, + source: events.EventField.source, + }), + }) + ); + + // ------------------------------------------------------------------- + // Outputs + // ------------------------------------------------------------------- + new cdk.CfnOutput(this, 'EventBusName', { + value: eventBus.eventBusName, + description: 'Custom event bus to publish test events to', + }); + + new cdk.CfnOutput(this, 'ApiDestinationEndpoint', { + value: invocationEndpoint, + description: 'AgentCore Runtime invocation URL used by the API Destination', + }); + + new cdk.CfnOutput(this, 'CognitoTokenEndpoint', { + value: tokenEndpoint, + description: 'OAuth2 token endpoint used by the EventBridge Connection', + }); + + new cdk.CfnOutput(this, 'CognitoDiscoveryUrl', { + value: discoveryUrl, + description: 'OIDC discovery URL used by the AgentCore Runtime customJwtAuthorizer', + }); + + new cdk.CfnOutput(this, 'CognitoAppClientId', { + value: appClient.userPoolClientId, + description: 'App client ID trusted by the AgentCore Runtime customJwtAuthorizer', + }); + + new cdk.CfnOutput(this, 'AgentRuntimeArn', { + value: agentRuntime.attrAgentRuntimeArn, + description: 'ARN of the deployed AgentCore Runtime', + }); + + new cdk.CfnOutput(this, 'AgentRuntimeId', { + value: agentRuntime.attrAgentRuntimeId, + description: 'ID of the deployed AgentCore Runtime (used in the invocation URL)', + }); + + new cdk.CfnOutput(this, 'DeadLetterQueueUrl', { + value: dlq.queueUrl, + description: 'SQS DLQ for failed deliveries to the API Destination', + }); + } +} diff --git a/eventbridge-apidestination-agentcore-cdk/cdk/package.json b/eventbridge-apidestination-agentcore-cdk/cdk/package.json new file mode 100644 index 000000000..050a618fa --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/cdk/package.json @@ -0,0 +1,25 @@ +{ + "name": "eventbridge-apidestination-agentcore-cdk", + "version": "1.0.0", + "description": "EventBridge API Destination to Amazon Bedrock AgentCore Runtime - Lambda-less event-driven agent invocation", + "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/eventbridge-apidestination-agentcore-cdk/cdk/tsconfig.json b/eventbridge-apidestination-agentcore-cdk/cdk/tsconfig.json new file mode 100644 index 000000000..b1eaa510e --- /dev/null +++ b/eventbridge-apidestination-agentcore-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/eventbridge-apidestination-agentcore-cdk/example-pattern.json b/eventbridge-apidestination-agentcore-cdk/example-pattern.json new file mode 100644 index 000000000..afbb29288 --- /dev/null +++ b/eventbridge-apidestination-agentcore-cdk/example-pattern.json @@ -0,0 +1,120 @@ +{ + "title": "Amazon EventBridge API Destination to Amazon Bedrock AgentCore Runtime", + "description": "Invoke an AgentCore Runtime agent directly from EventBridge without any Lambda glue code, using an API Destination with Cognito M2M OAuth.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "eventbridge-api-destination", + "label": "API Destination" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "bedrock", + "label": "Amazon Bedrock AgentCore Runtime" + }, + "icon4": { + "x": 50, + "y": 20, + "service": "cognito", + "label": "Amazon Cognito (M2M OAuth)" + }, + "icon5": { + "x": 50, + "y": 80, + "service": "sqs", + "label": "Dead-letter queue" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon4", + "to": "icon2" + }, + "line4": { + "from": "icon2", + "to": "icon5" + } + }, + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows a Lambda-less, event-driven invocation of an Amazon Bedrock AgentCore Runtime agent. The CDK stack is self-contained: it builds and deploys the AgentCore Runtime (from a bundled agent container image) along with all the EventBridge plumbing.", + "An EventBridge rule matches events on a custom bus and routes them to an API Destination whose HTTPS endpoint is the AgentCore Runtime InvokeAgentRuntime API.", + "The EventBridge Connection authenticates using the OAuth client_credentials flow against an Amazon Cognito user pool (machine-to-machine). The AgentCore Runtime validates the resulting JWT via its inbound identity (customJwtAuthorizer) configuration, which the stack wires to the same Cognito user pool at creation time.", + "Because API Destinations enforce a 5-second response timeout, the AgentCore Runtime is invoked in asynchronous mode: the agent entrypoint acknowledges the request immediately and continues processing the event in the background.", + "The endpoint URL uses the agent runtime ID plus an accountId query parameter instead of a URL-encoded ARN, because API Destinations automatically decode percent-encoded sequences in target URLs.", + "Deliveries that fail after retries are captured in an SQS dead-letter queue for inspection and redrive.", + "IAM permissions follow least privilege: the AgentCore Runtime execution role grants only ecr:BatchGetImage and ecr:GetDownloadUrlForLayer to the specific ECR repository, scoped CloudWatch Logs and X-Ray permissions, and bedrock:InvokeModel for foundation models. The EventBridge rule's target role has only events:InvokeApiDestination on the specific API Destination. The SQS DLQ enforces SSL in transit." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-apidestination-agentcore-cdk", + "templateURL": "serverless-patterns/eventbridge-apidestination-agentcore-cdk", + "projectFolder": "eventbridge-apidestination-agentcore-cdk", + "templateFile": "cdk/lib/eventbridge-agentcore-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon EventBridge API Destinations", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html" + }, + { + "text": "Amazon Bedrock AgentCore Runtime", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" + }, + { + "text": "AgentCore Runtime inbound JWT authorizer", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/inbound-jwt-authorizer.html" + }, + { + "text": "Cognito machine-to-machine authorization (client credentials grant)", + "link": "https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html" + } + ] + }, + "deploy": { + "text": [ + "cd cdk", + "npm install", + "cdk deploy" + ] + }, + "testing": { + "headline": "Testing", + "text": [ + "See the GitHub repo README.md for detailed testing instructions.", + "Sample test event payload: {\"EventBusName\":\"agentcore-events\",\"Source\":\"demo.orders\",\"DetailType\":\"OrderCreated\",\"Detail\":\"{\\\"orderId\\\":\\\"12345\\\",\\\"prompt\\\":\\\"Summarize this order and flag any anomalies.\\\"}\"}" + ] + }, + "cleanup": { + "headline": "Cleanup", + "text": ["cd cdk", "cdk destroy"] + }, + "authors": [ + { + "name": "Antoine Boucherie", + "bio": "Principal Solutions Architect, AWS Global Financial Services", + "linkedin": "antoineboucherie" + } + ] +}