From ae029e1ba58c87c92bde9fd54dcbe9e972bd9cb7 Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 11:12:34 +0800 Subject: [PATCH 1/6] Add Amazon EventBridge API Destination to Amazon Bedrock AgentCore Runtime pattern --- eventbridge-apidestination-agentcore-cdk.json | 120 +++++++ .../README.md | 134 +++++++ .../agent-code/.dockerignore | 5 + .../agent-code/.gitignore | 3 + .../agent-code/Dockerfile | 20 ++ .../agent-code/agent.py | 50 +++ .../agent-code/requirements.txt | 2 + .../cdk/.gitignore | 7 + .../cdk/bin/app.ts | 14 + .../cdk/cdk.json | 21 ++ .../cdk/lib/eventbridge-agentcore-stack.ts | 328 ++++++++++++++++++ .../cdk/package.json | 25 ++ .../cdk/tsconfig.json | 24 ++ .../example-pattern.json | 120 +++++++ 14 files changed, 873 insertions(+) create mode 100644 eventbridge-apidestination-agentcore-cdk.json create mode 100644 eventbridge-apidestination-agentcore-cdk/README.md create mode 100644 eventbridge-apidestination-agentcore-cdk/agent-code/.dockerignore create mode 100644 eventbridge-apidestination-agentcore-cdk/agent-code/.gitignore create mode 100644 eventbridge-apidestination-agentcore-cdk/agent-code/Dockerfile create mode 100644 eventbridge-apidestination-agentcore-cdk/agent-code/agent.py create mode 100644 eventbridge-apidestination-agentcore-cdk/agent-code/requirements.txt create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/.gitignore create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/bin/app.ts create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/cdk.json create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/lib/eventbridge-agentcore-stack.ts create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/package.json create mode 100644 eventbridge-apidestination-agentcore-cdk/cdk/tsconfig.json create mode 100644 eventbridge-apidestination-agentcore-cdk/example-pattern.json diff --git a/eventbridge-apidestination-agentcore-cdk.json b/eventbridge-apidestination-agentcore-cdk.json new file mode 100644 index 0000000000..afbb292882 --- /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 0000000000..d2d1d21dce --- /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 0000000000..caf4e5aa45 --- /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 0000000000..309de5763b --- /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 0000000000..acc74f75fc --- /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 0000000000..33a486e446 --- /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 0000000000..d4c4ad71a8 --- /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/cdk/.gitignore b/eventbridge-apidestination-agentcore-cdk/cdk/.gitignore new file mode 100644 index 0000000000..459d585459 --- /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 0000000000..f92ea11543 --- /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 0000000000..020a631ce8 --- /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 0000000000..169c3782f0 --- /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 0000000000..050a618faa --- /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 0000000000..b1eaa510e9 --- /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 0000000000..afbb292882 --- /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" + } + ] +} From e993affc42ad7aa9c7614b50ca0c9f66ba13ca2f Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 16:06:02 +0800 Subject: [PATCH 2/6] Add AgentCore Runtime to EventBridge via AgentCore Gateway pattern --- agentcore-gateway-eventbridge-cdk.json | 71 +++++ agentcore-gateway-eventbridge-cdk/README.md | 104 ++++++++ .../agent-code/.dockerignore | 5 + .../agent-code/.gitignore | 3 + .../agent-code/Dockerfile | 20 ++ .../agent-code/agent.py | 59 +++++ .../agent-code/requirements.txt | 2 + .../cdk/.gitignore | 7 + .../cdk/bin/app.ts | 14 + .../cdk/cdk.json | 21 ++ .../agentcore-gateway-eventbridge-stack.ts | 249 ++++++++++++++++++ .../cdk/package.json | 25 ++ .../cdk/tsconfig.json | 24 ++ .../example-pattern.json | 71 +++++ .../src/emit_event/handler.py | 73 +++++ 15 files changed, 748 insertions(+) create mode 100644 agentcore-gateway-eventbridge-cdk.json create mode 100644 agentcore-gateway-eventbridge-cdk/README.md create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/.dockerignore create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/.gitignore create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/Dockerfile create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/agent.py create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/requirements.txt create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/.gitignore create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/bin/app.ts create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/cdk.json create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/package.json create mode 100644 agentcore-gateway-eventbridge-cdk/cdk/tsconfig.json create mode 100644 agentcore-gateway-eventbridge-cdk/example-pattern.json create mode 100644 agentcore-gateway-eventbridge-cdk/src/emit_event/handler.py diff --git a/agentcore-gateway-eventbridge-cdk.json b/agentcore-gateway-eventbridge-cdk.json new file mode 100644 index 0000000000..a8929429c1 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk.json @@ -0,0 +1,71 @@ +{ + "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, with schema validation and fan-out to DynamoDB and SNS.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "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 exposes an emit_event tool backed by an AWS Lambda function.", + "When the agent decides to emit an event, it 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.", + "EventBridge routes agent events to downstream consumers: a DynamoDB table (decision log with 30-day TTL) and an SNS topic (for events with notify=true).", + "The Gateway provides governance: the tool schema constrains what the agent can emit, rate limiting prevents abuse, and every invocation is logged automatically. Schema changes require only a Gateway update, not an agent redeploy.", + "IAM permissions follow least privilege: the Gateway role can only invoke the specific Lambda, the Lambda can only PutEvents to the specific bus, and the Runtime execution role is scoped to ECR, logs, and Bedrock model access." + ] + }, + "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": "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.", + "Invoke the agent with a prompt asking it to use emit_event, then verify the event in DynamoDB: aws dynamodb scan --table-name agent-decision-log --region us-east-1" + ] + }, + "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 0000000000..2339692b67 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/README.md @@ -0,0 +1,104 @@ +# 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**. The Gateway provides governance, observability, rate limiting, 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, an EventBridge custom bus, and downstream consumers (DynamoDB decision log + SNS notifications). + +![Architecture](architecture.png) + +``` +Strands Agent (AgentCore Runtime) + → emit_event tool (AgentCore Gateway, MCP Streamable HTTP) + → Lambda tool backend (validates + PutEvents) + → EventBridge Custom Bus + ├── Rule 1: agent.* → DynamoDB (decision log) + └── Rule 2: notify=true → SNS topic +``` + +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 Gateway over direct SDK? + +| Concern | Direct SDK | Via AgentCore Gateway | +|---------|-----------|----------------------| +| Governance | Agent can emit any event schema | Gateway tool definition constrains allowed schemas | +| Observability | Must instrument yourself | Gateway logs every tool invocation automatically | +| Rate limiting | None by default | Gateway enforces per-tool rate limits | +| Schema evolution | Redeploy agent container to change | Update Gateway tool definition only | +| Multi-agent consistency | Each agent implements PutEvents differently | All agents use the same governed tool | + +## 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 exposes an `emit_event` tool backed by a Lambda function. +3. When the agent decides to emit an event, it calls `emit_event` with `source`, `detail_type`, `detail`, and optionally `notify`. +4. The Lambda validates the source prefix (`agent.*` only) and calls `events:PutEvents`. +5. EventBridge routes the event via two rules: + - All `agent.*` events → DynamoDB (decision log with 30-day TTL) + - Events with `detail.notify=true` → SNS topic + +## 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` and `DecisionLogTableName`. + +## 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-$(date +%s)" \ + --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}, notify=true"}' \ + --region us-east-1 +``` + +Then verify the event landed in DynamoDB: + +```bash +aws dynamodb scan --table-name agent-decision-log --region us-east-1 +``` + +Expected item: +```json +{ + "pk": "agent.claims-processor#ClaimApproved", + "sk": "2026-08-14T08:00:25Z", + "detail": "{\"claimId\": \"CLM-001\", \"decision\": \"approved\", \"confidence\": 0.94, \"notify\": true}", + "eventId": "...", + "ttl": 1726300825 +} +``` + +## 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 0000000000..caf4e5aa45 --- /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 0000000000..309de5763b --- /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 0000000000..acc74f75fc --- /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 0000000000..ca08ae3b9a --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/agent-code/agent.py @@ -0,0 +1,59 @@ +""" +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 Streamable HTTP MCP transport. +Authentication: authorizerType=NONE on the Gateway trusts callers within +the same account via the AgentCore platform's workload identity. +""" +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 + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = BedrockAgentCoreApp() + +GATEWAY_URL = os.environ.get("GATEWAY_MCP_URL", "") + + +def create_mcp_client(): + """Create a fresh MCP client instance per invocation.""" + if not GATEWAY_URL: + return None + return MCPClient(lambda: streamablehttp_client(GATEWAY_URL)) + + +@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 0000000000..d4c4ad71a8 --- /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/cdk/.gitignore b/agentcore-gateway-eventbridge-cdk/cdk/.gitignore new file mode 100644 index 0000000000..459d585459 --- /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 0000000000..9e4a4a0047 --- /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 0000000000..020a631ce8 --- /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 0000000000..665c13c4b9 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts @@ -0,0 +1,249 @@ +import * as path from 'path'; +import * as cdk from 'aws-cdk-lib'; +import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as eventsTargets from 'aws-cdk-lib/aws-events-targets'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as sns from 'aws-cdk-lib/aws-sns'; +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 + * → Fan-out rules → DynamoDB (decision log) + SNS (notifications) + * + * 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=NONE (same-account trust via + * AgentCore workload identity). The Gateway and Runtime are both + * managed by AgentCore within the same account. + * - Runtime invocation (external caller): IAM SigV4 — caller needs + * bedrock-agentcore:InvokeAgentRuntime permission. + * - Gateway → Lambda: Gateway IAM role has lambda:InvokeFunction. + */ +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) + // ------------------------------------------------------------------- + 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: 'NONE', + protocolType: 'MCP', + protocolConfiguration: { + mcp: { + supportedVersions: ['2025-03-26'], + }, + }, + roleArn: gatewayRole.roleArn, + description: 'MCP Gateway 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' }, + notify: { type: 'boolean', description: 'If true, downstream rules also send an SNS notification' }, + }, + 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}:*`] }), + ], + }), + }, + }); + + 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 }, + }); + + // ------------------------------------------------------------------- + // 5. Downstream Consumers (fan-out from EventBridge) + // ------------------------------------------------------------------- + const decisionLog = new dynamodb.Table(this, 'AgentDecisionLog', { + tableName: 'agent-decision-log', + partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + timeToLiveAttribute: 'ttl', + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const notifyTopic = new sns.Topic(this, 'AgentNotifyTopic', { + topicName: 'agent-event-notifications', + }); + + const allAgentEventsRule = new events.Rule(this, 'AllAgentEventsRule', { + ruleName: 'agent-events-to-dynamodb', + eventBus, + eventPattern: { source: [{ prefix: 'agent.' }] as any }, + }); + + const logWriterFn = new lambda.Function(this, 'LogWriterFunction', { + functionName: 'agent-decision-log-writer', + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'index.handler', + code: lambda.Code.fromInline(` +import boto3, json, time, os +table = boto3.resource('dynamodb').Table(os.environ['TABLE_NAME']) +def handler(event, ctx): + table.put_item(Item={ + 'pk': f"{event['source']}#{event['detail-type']}", + 'sk': event['time'], + 'detail': json.dumps(event['detail']), + 'eventId': event['id'], + 'ttl': int(time.time()) + 30*86400 + }) + return {'statusCode': 200} +`), + environment: { TABLE_NAME: decisionLog.tableName }, + timeout: cdk.Duration.seconds(10), + }); + decisionLog.grantWriteData(logWriterFn); + allAgentEventsRule.addTarget(new eventsTargets.LambdaFunction(logWriterFn)); + + const notifyRule = new events.Rule(this, 'NotifyRule', { + ruleName: 'agent-notify-events-to-sns', + eventBus, + eventPattern: { source: [{ prefix: 'agent.' }] as any, detail: { notify: [true] } }, + }); + notifyRule.addTarget(new eventsTargets.SnsTopic(notifyTopic)); + + // ------------------------------------------------------------------- + // 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, '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' }); + new cdk.CfnOutput(this, 'DecisionLogTableName', { value: decisionLog.tableName, description: 'DynamoDB table storing agent decision log' }); + new cdk.CfnOutput(this, 'NotifyTopicArn', { value: notifyTopic.topicArn, description: 'SNS topic for agent event notifications' }); + } +} diff --git a/agentcore-gateway-eventbridge-cdk/cdk/package.json b/agentcore-gateway-eventbridge-cdk/cdk/package.json new file mode 100644 index 0000000000..7ab4012ce4 --- /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 0000000000..b1eaa510e9 --- /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 0000000000..a8929429c1 --- /dev/null +++ b/agentcore-gateway-eventbridge-cdk/example-pattern.json @@ -0,0 +1,71 @@ +{ + "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, with schema validation and fan-out to DynamoDB and SNS.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "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 exposes an emit_event tool backed by an AWS Lambda function.", + "When the agent decides to emit an event, it 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.", + "EventBridge routes agent events to downstream consumers: a DynamoDB table (decision log with 30-day TTL) and an SNS topic (for events with notify=true).", + "The Gateway provides governance: the tool schema constrains what the agent can emit, rate limiting prevents abuse, and every invocation is logged automatically. Schema changes require only a Gateway update, not an agent redeploy.", + "IAM permissions follow least privilege: the Gateway role can only invoke the specific Lambda, the Lambda can only PutEvents to the specific bus, and the Runtime execution role is scoped to ECR, logs, and Bedrock model access." + ] + }, + "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": "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.", + "Invoke the agent with a prompt asking it to use emit_event, then verify the event in DynamoDB: aws dynamodb scan --table-name agent-decision-log --region us-east-1" + ] + }, + "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 0000000000..e03d545ee4 --- /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", ""), + } + ), + } From 3cd68e0492016457ce41c163b6799a3a0080f5ab Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 20:47:00 +0800 Subject: [PATCH 3/6] Simplify AgentCore Gateway pattern: IAM/SigV4 auth, drop DynamoDB/SNS fan-out --- agentcore-gateway-eventbridge-cdk.json | 17 ++-- agentcore-gateway-eventbridge-cdk/README.md | 56 ++++++------- .../agent-code/agent.py | 18 ++-- .../agent-code/sigv4.py | 42 ++++++++++ .../agentcore-gateway-eventbridge-stack.ts | 82 ++++--------------- .../example-pattern.json | 17 ++-- 6 files changed, 116 insertions(+), 116 deletions(-) create mode 100644 agentcore-gateway-eventbridge-cdk/agent-code/sigv4.py diff --git a/agentcore-gateway-eventbridge-cdk.json b/agentcore-gateway-eventbridge-cdk.json index a8929429c1..54f4ff6d9c 100644 --- a/agentcore-gateway-eventbridge-cdk.json +++ b/agentcore-gateway-eventbridge-cdk.json @@ -1,6 +1,6 @@ { "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, with schema validation and fan-out to DynamoDB and SNS.", + "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", @@ -8,11 +8,10 @@ "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 exposes an emit_event tool backed by an AWS Lambda function.", - "When the agent decides to emit an event, it 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.", - "EventBridge routes agent events to downstream consumers: a DynamoDB table (decision log with 30-day TTL) and an SNS topic (for events with notify=true).", - "The Gateway provides governance: the tool schema constrains what the agent can emit, rate limiting prevents abuse, and every invocation is logged automatically. Schema changes require only a Gateway update, not an agent redeploy.", - "IAM permissions follow least privilege: the Gateway role can only invoke the specific Lambda, the Lambda can only PutEvents to the specific bus, and the Runtime execution role is scoped to ECR, logs, and Bedrock model access." + "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": { @@ -29,6 +28,10 @@ "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" @@ -54,7 +57,7 @@ "headline": "Testing", "text": [ "See the GitHub repo README.md for detailed testing instructions.", - "Invoke the agent with a prompt asking it to use emit_event, then verify the event in DynamoDB: aws dynamodb scan --table-name agent-decision-log --region us-east-1" + "Invoke the agent with a prompt asking it to use emit_event, then check the Runtime's CloudWatch Logs for a 200 OK from the Gateway and the EventBridge event ID." ] }, "cleanup": { diff --git a/agentcore-gateway-eventbridge-cdk/README.md b/agentcore-gateway-eventbridge-cdk/README.md index 2339692b67..5787065b7d 100644 --- a/agentcore-gateway-eventbridge-cdk/README.md +++ b/agentcore-gateway-eventbridge-cdk/README.md @@ -1,18 +1,22 @@ # 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**. The Gateway provides governance, observability, rate limiting, and schema control over what the agent can emit — without the agent needing direct access to the EventBridge SDK. +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, an EventBridge custom bus, and downstream consumers (DynamoDB decision log + SNS notifications). +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) - → emit_event tool (AgentCore Gateway, MCP Streamable HTTP) - → Lambda tool backend (validates + PutEvents) - → EventBridge Custom Bus - ├── Rule 1: agent.* → DynamoDB (decision log) - └── Rule 2: notify=true → SNS topic + │ 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/ @@ -25,19 +29,17 @@ Important: this application uses various AWS services and there are costs associ |---------|-----------|----------------------| | Governance | Agent can emit any event schema | Gateway tool definition constrains allowed schemas | | Observability | Must instrument yourself | Gateway logs every tool invocation automatically | -| Rate limiting | None by default | Gateway enforces per-tool rate limits | | Schema evolution | Redeploy agent container to change | Update Gateway tool definition only | | Multi-agent consistency | Each agent implements PutEvents differently | All agents use the same governed tool | ## 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 exposes an `emit_event` tool backed by a Lambda function. -3. When the agent decides to emit an event, it calls `emit_event` with `source`, `detail_type`, `detail`, and optionally `notify`. -4. The Lambda validates the source prefix (`agent.*` only) and calls `events:PutEvents`. -5. EventBridge routes the event via two rules: - - All `agent.*` events → DynamoDB (decision log with 30-day TTL) - - Events with `detail.notify=true` → SNS topic +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 @@ -57,7 +59,7 @@ npm install cdk deploy ``` -Note the stack outputs — in particular `AgentRuntimeArn` and `DecisionLogTableName`. +Note the stack outputs — in particular `AgentRuntimeArn`. ## Testing @@ -69,27 +71,19 @@ RUNTIME_ARN="" aws bedrock-agentcore invoke-agent-runtime \ --agent-runtime-arn "$RUNTIME_ARN" \ --qualifier DEFAULT \ - --runtime-session-id "test-session-$(date +%s)" \ - --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}, notify=true"}' \ + --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 ``` -Then verify the event landed in DynamoDB: +Verify success in the Runtime's CloudWatch Logs (`/aws/bedrock-agentcore/runtimes/-DEFAULT`): -```bash -aws dynamodb scan --table-name agent-decision-log --region us-east-1 -``` +- `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`. -Expected item: -```json -{ - "pk": "agent.claims-processor#ClaimApproved", - "sk": "2026-08-14T08:00:25Z", - "detail": "{\"claimId\": \"CLM-001\", \"decision\": \"approved\", \"confidence\": 0.94, \"notify\": true}", - "eventId": "...", - "ttl": 1726300825 -} -``` +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 diff --git a/agentcore-gateway-eventbridge-cdk/agent-code/agent.py b/agentcore-gateway-eventbridge-cdk/agent-code/agent.py index ca08ae3b9a..8061cd7412 100644 --- a/agentcore-gateway-eventbridge-cdk/agent-code/agent.py +++ b/agentcore-gateway-eventbridge-cdk/agent-code/agent.py @@ -2,9 +2,13 @@ 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 Streamable HTTP MCP transport. -Authentication: authorizerType=NONE on the Gateway trusts callers within -the same account via the AgentCore platform's workload identity. +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 @@ -14,19 +18,23 @@ 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 MCP client instance per invocation.""" + """Create a fresh, SigV4-authenticated MCP client per invocation.""" if not GATEWAY_URL: return None - return MCPClient(lambda: streamablehttp_client(GATEWAY_URL)) + sigv4_auth = SigV4HTTPXAuth(region=AWS_REGION) + return MCPClient(lambda: streamablehttp_client(GATEWAY_URL, auth=sigv4_auth)) @app.entrypoint 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 0000000000..53b10f1047 --- /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/cdk/lib/agentcore-gateway-eventbridge-stack.ts b/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts index 665c13c4b9..43b6e1b39e 100644 --- a/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts +++ b/agentcore-gateway-eventbridge-cdk/cdk/lib/agentcore-gateway-eventbridge-stack.ts @@ -1,13 +1,10 @@ import * as path from 'path'; import * as cdk from 'aws-cdk-lib'; import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore'; -import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import * as ecrAssets from 'aws-cdk-lib/aws-ecr-assets'; import * as events from 'aws-cdk-lib/aws-events'; -import * as eventsTargets from 'aws-cdk-lib/aws-events-targets'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; -import * as sns from 'aws-cdk-lib/aws-sns'; import { Construct } from 'constructs'; /** @@ -19,7 +16,6 @@ import { Construct } from 'constructs'; * Flow: * Strands Agent (Runtime) → emit_event tool (Gateway MCP, Streamable HTTP) * → Lambda tool backend → events:PutEvents → Custom Bus - * → Fan-out rules → DynamoDB (decision log) + SNS (notifications) * * Why Gateway over direct SDK: * - Governance: Gateway tool definition constrains allowed event schemas @@ -29,12 +25,15 @@ import { Construct } from 'constructs'; * - Multi-agent consistency: All agents use the same governed tool * * Auth model: - * - Runtime → Gateway: authorizerType=NONE (same-account trust via - * AgentCore workload identity). The Gateway and Runtime are both - * managed by AgentCore within the same account. + * - 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. + * - 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) { @@ -65,7 +64,7 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { eventBus.grantPutEventsTo(emitEventFn); // ------------------------------------------------------------------- - // 3. AgentCore Gateway (MCP server with Lambda target) + // 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', { @@ -92,7 +91,7 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { const gateway = new bedrockagentcore.CfnGateway(this, 'EventEmitterGateway', { name: 'event-emitter-gateway', - authorizerType: 'NONE', + authorizerType: 'AWS_IAM', protocolType: 'MCP', protocolConfiguration: { mcp: { @@ -100,7 +99,7 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { }, }, roleArn: gatewayRole.roleArn, - description: 'MCP Gateway exposing emit_event tool for agents to publish events to EventBridge', + description: 'MCP Gateway (IAM/SigV4 auth) exposing emit_event tool for agents to publish events to EventBridge', }); emitEventFn.addPermission('GatewayInvoke', { @@ -130,7 +129,6 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { 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' }, - notify: { type: 'boolean', description: 'If true, downstream rules also send an SNS notification' }, }, required: ['source', 'detail_type', 'detail'], }, @@ -170,6 +168,11 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { 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], + }), ], }), }, @@ -183,67 +186,14 @@ export class AgentCoreGatewayEventBridgeStack extends cdk.Stack { environmentVariables: { GATEWAY_MCP_URL: gateway.attrGatewayUrl }, }); - // ------------------------------------------------------------------- - // 5. Downstream Consumers (fan-out from EventBridge) - // ------------------------------------------------------------------- - const decisionLog = new dynamodb.Table(this, 'AgentDecisionLog', { - tableName: 'agent-decision-log', - partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING }, - sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING }, - billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, - timeToLiveAttribute: 'ttl', - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - - const notifyTopic = new sns.Topic(this, 'AgentNotifyTopic', { - topicName: 'agent-event-notifications', - }); - - const allAgentEventsRule = new events.Rule(this, 'AllAgentEventsRule', { - ruleName: 'agent-events-to-dynamodb', - eventBus, - eventPattern: { source: [{ prefix: 'agent.' }] as any }, - }); - - const logWriterFn = new lambda.Function(this, 'LogWriterFunction', { - functionName: 'agent-decision-log-writer', - runtime: lambda.Runtime.PYTHON_3_12, - handler: 'index.handler', - code: lambda.Code.fromInline(` -import boto3, json, time, os -table = boto3.resource('dynamodb').Table(os.environ['TABLE_NAME']) -def handler(event, ctx): - table.put_item(Item={ - 'pk': f"{event['source']}#{event['detail-type']}", - 'sk': event['time'], - 'detail': json.dumps(event['detail']), - 'eventId': event['id'], - 'ttl': int(time.time()) + 30*86400 - }) - return {'statusCode': 200} -`), - environment: { TABLE_NAME: decisionLog.tableName }, - timeout: cdk.Duration.seconds(10), - }); - decisionLog.grantWriteData(logWriterFn); - allAgentEventsRule.addTarget(new eventsTargets.LambdaFunction(logWriterFn)); - - const notifyRule = new events.Rule(this, 'NotifyRule', { - ruleName: 'agent-notify-events-to-sns', - eventBus, - eventPattern: { source: [{ prefix: 'agent.' }] as any, detail: { notify: [true] } }, - }); - notifyRule.addTarget(new eventsTargets.SnsTopic(notifyTopic)); - // ------------------------------------------------------------------- // 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' }); - new cdk.CfnOutput(this, 'DecisionLogTableName', { value: decisionLog.tableName, description: 'DynamoDB table storing agent decision log' }); - new cdk.CfnOutput(this, 'NotifyTopicArn', { value: notifyTopic.topicArn, description: 'SNS topic for agent event notifications' }); } } diff --git a/agentcore-gateway-eventbridge-cdk/example-pattern.json b/agentcore-gateway-eventbridge-cdk/example-pattern.json index a8929429c1..54f4ff6d9c 100644 --- a/agentcore-gateway-eventbridge-cdk/example-pattern.json +++ b/agentcore-gateway-eventbridge-cdk/example-pattern.json @@ -1,6 +1,6 @@ { "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, with schema validation and fan-out to DynamoDB and SNS.", + "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", @@ -8,11 +8,10 @@ "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 exposes an emit_event tool backed by an AWS Lambda function.", - "When the agent decides to emit an event, it 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.", - "EventBridge routes agent events to downstream consumers: a DynamoDB table (decision log with 30-day TTL) and an SNS topic (for events with notify=true).", - "The Gateway provides governance: the tool schema constrains what the agent can emit, rate limiting prevents abuse, and every invocation is logged automatically. Schema changes require only a Gateway update, not an agent redeploy.", - "IAM permissions follow least privilege: the Gateway role can only invoke the specific Lambda, the Lambda can only PutEvents to the specific bus, and the Runtime execution role is scoped to ECR, logs, and Bedrock model access." + "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": { @@ -29,6 +28,10 @@ "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" @@ -54,7 +57,7 @@ "headline": "Testing", "text": [ "See the GitHub repo README.md for detailed testing instructions.", - "Invoke the agent with a prompt asking it to use emit_event, then verify the event in DynamoDB: aws dynamodb scan --table-name agent-decision-log --region us-east-1" + "Invoke the agent with a prompt asking it to use emit_event, then check the Runtime's CloudWatch Logs for a 200 OK from the Gateway and the EventBridge event ID." ] }, "cleanup": { From 0639933e85c36dbb1283e6159bbfe44c9dc1a784 Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 21:18:08 +0800 Subject: [PATCH 4/6] Add patternArch and sample testing payload to agentcore-gateway-eventbridge-cdk --- agentcore-gateway-eventbridge-cdk.json | 51 ++++++++++++++++++- .../example-pattern.json | 51 ++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/agentcore-gateway-eventbridge-cdk.json b/agentcore-gateway-eventbridge-cdk.json index 54f4ff6d9c..868ca015d3 100644 --- a/agentcore-gateway-eventbridge-cdk.json +++ b/agentcore-gateway-eventbridge-cdk.json @@ -4,6 +4,54 @@ "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": [ @@ -57,7 +105,8 @@ "headline": "Testing", "text": [ "See the GitHub repo README.md for detailed testing instructions.", - "Invoke the agent with a prompt asking it to use emit_event, then check the Runtime's CloudWatch Logs for a 200 OK from the Gateway and the EventBridge event ID." + "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": { diff --git a/agentcore-gateway-eventbridge-cdk/example-pattern.json b/agentcore-gateway-eventbridge-cdk/example-pattern.json index 54f4ff6d9c..868ca015d3 100644 --- a/agentcore-gateway-eventbridge-cdk/example-pattern.json +++ b/agentcore-gateway-eventbridge-cdk/example-pattern.json @@ -4,6 +4,54 @@ "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": [ @@ -57,7 +105,8 @@ "headline": "Testing", "text": [ "See the GitHub repo README.md for detailed testing instructions.", - "Invoke the agent with a prompt asking it to use emit_event, then check the Runtime's CloudWatch Logs for a 200 OK from the Gateway and the EventBridge event ID." + "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": { From 45564670cc0e88651389ca2c955211bae6ccf79f Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 21:26:04 +0800 Subject: [PATCH 5/6] Add architecture.png to both patterns and expand Why Gateway rationale --- agentcore-gateway-eventbridge-cdk/README.md | 10 +++++++++- .../architecture.png | Bin 0 -> 42314 bytes .../architecture.png | Bin 0 -> 48598 bytes 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 agentcore-gateway-eventbridge-cdk/architecture.png create mode 100644 eventbridge-apidestination-agentcore-cdk/architecture.png diff --git a/agentcore-gateway-eventbridge-cdk/README.md b/agentcore-gateway-eventbridge-cdk/README.md index 5787065b7d..fb0e3ab305 100644 --- a/agentcore-gateway-eventbridge-cdk/README.md +++ b/agentcore-gateway-eventbridge-cdk/README.md @@ -23,7 +23,15 @@ Learn more about this pattern at Serverless Land Patterns: https://serverlesslan 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 Gateway over direct SDK? +## Why route through AgentCore Gateway instead of calling EventBridge directly? + +In a mesh of tens or hundreds of agents, Gateway acts as a governed chokepoint between agent reasoning and infrastructure side effects. Three properties make it worth the extra hop: + +- **Policy-based access control** — Cedar policies attached to the Gateway evaluate every tool call against its input before it reaches the Lambda target. You can restrict which agents emit which event types, or block specific `detail_type` values during a production freeze, without redeploying any agent. +- **Per-caller rate limiting** — set different throughput budgets per agent identity so a misbehaving agent can't flood the bus while high-priority agents keep running. +- **Single IAM blast radius** — only the Gateway's Lambda backend holds `events:PutEvents`. Agents authenticate with scoped, revocable credentials (JWT or IAM/SigV4), and the Gateway centrally logs every tool invocation for audit. + +The tradeoff is roughly 200ms of added latency per emission — negligible for asynchronous event-driven workflows, and often more than offset by removing per-agent IAM churn as the mesh grows. | Concern | Direct SDK | Via AgentCore Gateway | |---------|-----------|----------------------| diff --git a/agentcore-gateway-eventbridge-cdk/architecture.png b/agentcore-gateway-eventbridge-cdk/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..453183d5bddd5edce685126927f2452272ac912d GIT binary patch literal 42314 zcmZU51ymK=_dg)r9fEKX5S8xkmQs;!=?;lYmvkcnO1DTilG5GXak(^??)ncezVE%? zf2~=p@y^USXYcdbdG`Yo6L>7_| zdC0_b;tHUF!d}34mT0uX^}?=}%;M0J2Vtf>&i!AO8wJdiyoP;Tls9HSnNH!|=Q8^3 zH6qB&t|(wBua@c1C$@{0of=9dND@G0 z_soi^T7x^?bWY3W6Ib@sH|^-5BEis|yuyDd5=9ULNb)zxQ5hjnn?eqgvc?V5Bq-i#)7EZhH_~#(WjwKVa!I97EjhC$ zTB{CgU#9>~q);eVt$#V_0Z}Xcq65GTSxkge|G)Ty9{|U!K+sO$ zKAMa8^L5_~4F*U%7y7EIuI5h;y`(e$YLAV=^qMc(HSY&g-;LjySuPi1sh-p4n&j%?W3(^aqm;-(D3fFU>+e6IK0X6;?S1~MeP`^tQXN4oPWbr z7XFl*@nJXHPi6wt>_k7mgRy41<84#zJkhMyjtqYhgFK|Nn@ilt=WhRP4r-}iV0g0I zZ(})KkV(17Wm9OeWmrvTscI=x=r%9&RCK=n6AuNA2=@7OBF?1nTLFzo@_$f4%Lvau z<%MYPFy~cV3ALqOVPc9{s$)7HE3Sq_mgCCmeQ3O)ss3%ug<2}%oL&&5*qH+6(Dg9y zaH_7I6dVoY3`r5;!uCfMZp>Ouo;-iQTYT<2cVOA7tuORjNdI6*7+mRYUpSX&**f2H zX5h@l=q4zA(L>OPiC1a;bl+AZA!HZ}#vqZNHZc=E5v2-|{u}v!hyWAn%38UU@wIf0 z_i&_xw3ZcmF;+_zt027g=Uc3ggYln-3D}6`k*K3gEwDpCj8qFM?K5vl#;DssRiL`1 z(a)`xNs9hk;*>&8wO(gL7Z%W+dp@oOnsc1RU|U=rTn${SOpDC!OH7K=SX@VuZOVP+ zQK5tvJIrfAH~p;bfsVKVxR!Vv9De^um@LQ(ZN;C%%Ol-X$PQN#KDD*N^2oJW4BROM z3Cs<|71IbpX^)7j?ZCZoHcUXj(20B>4XRJrrMyCS!Wj*``Ng?q>$A-OgZP`b3uz_p z9EnWEsJkl8i(`>-+10!WP$V~Kn20HuK=6c+iDs;f@qiU8g}~U%>Qa|*^5KN|fj3h~DcjS;;tjOq@~2$`aj*p}1WcSRUs3Xvt|E8MpRH1sxQ_K! zb7i!+7ZGAxZn5B`ZYuY(*zdKgcqd8UPNGt~eiKJ}pw>Udf(Uai{vxM@ts-BFBNRyb zdQtzaHOxie4?b4hag_&srXTTcC2}QkNGDDpxL21K7i^`RKD4vUo-7mVSQUPa%0$s_ zZ%0CDdQ?s+D!Q$7m~^?%iF+FY7BqkI5hXfcI>&Tg)g6(INAhCkuq@Jp^Dyfv3}-yG zVHKX-&U_1z%aihO1v{kL!aV}(!pDsxKn9dGsy8KcEJpefxu_Z%@;&uW-JAoqbN|BtYKWraaRryjdY*6>{PU5fF^UL-=&#)5*9VKF@lHtC{%=zY%S`@-o_VG#TOAwDx0hG5oF79*q$^KJK>t zsML>8CAc<*x7Ie=jLfZ*nxHZrhbT4J-F*Ubwrvtyav`7na1!jf?d2lNK-wMt3l!5@ z1H(APizqj7rAwLrhmc~w!%sQ$-?$D0^j;0gX3=Li~L0w?Hxr^Wtl*m4Bm z!%CcdP|6>^Z&tUC{ntyrh;hNbUj$3W#%pkY{Ct@_Vj5mxEhnda z`;qKFrc#y!n0(tYZ&n#!_9KE*>OVuoeFMxkd%DoI(jTOcd_|lg{!1NKg#wR~%gS;8 zQO`~!@HO%P{nh~ear+UY`v08;5#f~D6qt!E`=j)Ko&b9jk`9b^JFxbuwedf^6h*L5 z^37rxT!!28uE#YH`8N~l!+~#HW6c`=V0skdjM-nK%1QvUSb)c7P5%DD6=6^0ah*tleWk+hkr{IvtP3ciMaFwpN5Bs%()a`W3?V11<0 zPZp6>8G&}B7uailGS4EHr@Gf?mU@GS?`kCRaMmE*4O0r2hp`9y`$U(Egk z3r7S7X!u)T0KLzKI(_fBcpNhQk|W^+Sw{guA2r zLr?$7Rsep4x}jjTErLgaXgT2j(vQHHBaiaUK_U1T|A9pjA?%_2@ftb+21!1G(`*S; z-Y*YAepB4Y&m0jn(9me9;iJfG$T@y9p(*p|zVO4QE?YdVER_5uPi#7%vvA_qD{>o{>U)FZ<|E_f(0z>&)!LQw0%U=l`-YxW7Ht{+Pj z@%U43|4l1x%-SRDZV@Xzj_?d@|H~zF1jQ@{`j#Ir>01oM*)?U_4ZQw9DhP24`R{!b zMaQ$3Qg_iaEU$*=BD1qOCcRb4{M1_V7Z64L@iGMd1oShp->m})AxxG(2I5I+2)?vF zy{+aKQ2#K2yTS1HXpazhgKv;83+hFynoCm%#5%MAp-K1-4*;8)QJVh; z^FMEZz_4cO8Ew6qs-KBRs5w@pW8tlF2qM0p=x9)K|D~9Ot~{d8))QdxHG-A8*vrij z$~aMVyqW$Cs+#-Zd!*_KJ-3?E9 z_alOJ6e7Mfj415yTtEa|7eW*{n#=nwY&2Rpzk!q>`Ge~n{DM^e^2H$c5hhfYJ&RB= zVDZNb>|F^j#+q~wyH6rL@VA%|H1OBaU!_hSd(ISt!y*{HwNmnJV?)yi90%nCUIw7H z2PSCSj=W_F^#C+DC-rX!8bu|}204U%+PCVeKmD!5QD!8k2?o*}YWk&Oaar)B)WyLv z^s*o^R4|F+xJ(ot>Z>{=Kcr06W5`E}dTxibFlMd}nr9m?wlMm{d{^gfg(BP}=Gn|l z0R!*%nc8>Q)&cTf-1B~wvwM&~>3kHXh@LoReny?I-28b66g<43?r zj-7(akuL045&Z(~X(jkENi=#6n8X<83{GUFzIUSNml@_^BI|!QS%U|*RviDg*;k0P zc-m8bDFh2KxPHRX(T){>#-*zbhEykr zEFh8h{U*twR(K4!1raEC@DEZ(Xz*oa5K{KvG*&8&eg$tzZoR+2KV<&y-;&#M1Cu?C936^&Ln`K!=4Yj5X8Zm z#`<>>OGY;c+AkF`pO6THBgM8>mv9E+TS-_bNx*t_1Q$i$0quv3a5><(5CTtF31Hmm z75spU8`iOFgCPE5ZT{Ad@VPY~JQ}!C7MC~4(0^;&l4q2A?(`hB9n@SF?8XD7QK0dg zppTgmTCaxagl687FFtfa{U=qb-gu zq!akB@PmPVJ7f#|s8qz?5=JO`K%yVO^eDxkF@F%k#}{eqE&y7qEvc_pm#6DrGqrJGyBE+t2)*JD14KLOZ} zC}{7TENB+Zc!LmK@}1RGg7!2U1Q z4(#pJ8r#sKqc^S9i#`j`>(7Ok47EP>DIRXR4Z`eQ*3irn9-z`V9X^G?AC>C8Xkrk5C9!c*M(M%iI8qqe+6`iIH7tV8^{ zYqKWlvKgOH4F-H27VNK%ACp0!;SfhzNJv9F=V(#{9lB>rVhBd0f(doSGT0zzGY@HB z@4C}K+0sVH|2*&s-A(ulD+Fbq@Po zmmglrq&qfyzH54W>*s3_WKlaB-0v5h&X~)wWYu>)f8f&-T)ElzpxF2U6qQh_F&eMN zed0B1H*mzhk;Jhcbwl~3f$Fw2nfsxV#dxpW?9FI1Wc?V8=Z($2Bag5SRbFB37%JQhzR#5*RA2j0xmDX%PD>t3v+GVw+n=)}wM$7|J zYG~i^;t&xXOY?P}Lj8l9+pV8Al3@tLx*zq!k(mx-KBdoR>#vLYZ~B*sP$M_f>-3q0 zFFyRTZhPn*miLxYbAv|KntT{#-EM(ye6LM!)MX!!texs}=2@nGxN0ecMkT+fe}33s z+Uzi1@^;mA$fCP9Tv_;~w~uJx@SUw^o$#JoUEyAnM`nuG+))Jt`m z(BX{n^aC5p!DgPXf+ebXP3@0mYux*y6th;+yPYb3@te)0UVdlRv zCevj75GCRmaeD|Et}yOp!7qbXmn~_U`hL8&j@OV5x54DY|MvR)HW23+Qs!vpzI(J~gV7`jb&u30ysm6wEx#Pul-mna zals5)2Zmn~!G*V6DY0{Km@Es)^i9Gqfcd!aGeti!dV=R^svz?KV7sYa4R$pLjRI(9B?+R@{YY*(F2yt zV>}+o6HittY#%rsUaIs7vgvvJ&G=&ZeV~TVEI1o$J>%}&9ztDY|1gOoa$a?D zGIc}MU;#$kf3haN-$3O(p;`K7dSmcG$DU7qTwzq)qfa_@LW`%9McqpfQP)fAz5JD3anR;6^MN~z3+#Z%g#*K8-# z8d7gyhaK#u|nh#9blYi*qav@v6d{(&lu&umJ~!)t{$^rb*Zl8LX_ zq+ir@su^k!?JO21daRuAWr+`AQB^&py{>SDtt=JvD ztbZ^B1L-t){z`GZJ}QYhOxX6ixQi}HDl9BiKT>0niQs!%v-Dd25I3x}~|eigrDzLcimL@SM*Xv%40XxEj6iKL3j;}n{tS6xYQ14SZPVUj@q6&@i2Z&q?YQYIEFO{&P-*!#O?MCAQ8u;z@K`_Tc-N~Op1pB~ z?*LC!=w?#)7;-Q^x(~aaBjor%7G*GS{BvEyGsd|!FObYaWH}JA<&fxh&5ZpFt3b0U ze}P_%=+Z+@D{^-muJHY~h{E&tk}d3AU;{eLsI5s07l#gd@MiQ-ccBW4M?)Wb!A_a) zu7d{Y!pHC%C}HAkXHo)Nz^RJjr?UZ?VbBnwVZ!dnlk?c!(&oC}`x=Ro8-p;Ln!W^A zk)jxr{(Hw=F9|6!1PSELu;l|+v$?larfK%kBx6~cqWzL@s5tF?Z?AU`Y}rlInx`~{ zDLSq>JAFlb_Z&2b(}Z#irc;gknXc<~q|Hg&N?i|-SIN~)YzFppImL{{5Rue5yc;s{ zNnOPxkDyeu76{HB4{&b#_YAK)@-iJ1Yq` zwuankU~yG>-p#MKA0 z3+kWZog}8U{w#GdR3fGn{o}nOLmh6f>(|OOcFu00fZ+!$FzrAc9%QD~A^X!3O3~NH z@MeEPl|!fLLW?MBvR3QfR=wy4KjWhxklpo+Q9}TdmOkh0CxgE^6 zSp_lanlM|)wdd{f(pMG)Ckwg{53)mBE_#|CekplIL74LyyiM3ELX^!a^xUouTfG#M zgz7h^te`{5^nLPx#@|eJ@YkLm;7R{#gR)+C)s)DFvd^F_;*xlKre3_hiS2YEQre8w zOGP`Ysa-#dP1%%4?vpwc)5H!n?A@siB&RCCLD?CYpV*|ATgtFmL~+w`T(YqBK0p#~ zzMYULS>dx^7FutQON3?$JkaszefpMx)8o2*m0$3ZA5m<22@GS~&aeDn-f&t?ue%y7 z*iFTyR@Q+Ro#V=}gF%T{(cOdh6#pWV`eNB7fILV<<;5b?baabkUejS6*lnRh$sLVI z0apxHyEO|f>t)KN_$=VOB8g+d+)QGrxSU=PGvz+=Nz7K>GzIhCB?4%cN_C77h ze&rbOt_pXDwZMCo$9*;4pNg}BKwTIVx;d7eus(EJ!5}&_R$4J z9;cjwH36Z?<08)L!8v%rY5vYXpXmOgVACWzYgJK-dgm|$E1lqolN%}pceZ-}>a#Y- zdS{bWG4y1NgM(ucB@Ens1UnMNuOic7ehkGg>uB2Q?9qu7T1gZ$ZA2B2!dc?anCoA7 zv&(0F#VRhWjHa#%Q^i%9#*on1+S4(s9ln+W;XQh?2aLds4wvp16 z{$%TiEul@>FKyLEf3!6WlZCcxf7Uh5BwX?i+Rq8}s6zQ5{BYF}x9-ro+FKewX@qBI z_v>|dqg%#NhlOMNS7N>l?=^wnfQO>3tWxM z?mAj3`2pq^EDzlJ`Sazgf)YlKE?ydf!4(q9q|sGE{5o+#>1Q}wGiK3QrpodW{ZQJ$ zh>NW1ypf$ED|O7E%PtZU>R5ZEa7IR^u=rAeH6b>%ma-4*U>9t0Os`$+7XCs-!6Slx zJAp#)spLi=*y!+vjoB=Oq$*59J5)T3ESRP|%O^Sonb4fh>PE)`df1l6#P-?;!IRuW zE+)n#a&@OYTs%1w++j5}33fjMOUrTJjzZ%(jjN}QUp6Ak^!fGce}C@sfSq6MHjYd!LpxGB^nc> z>|M2TYLo>I)+8_K{>}xwA*(t`d#~NlsG0BtTR<-C{Mj_&#gGS8Z%9*j&o>f1I$43; ztioK=QFYsEXP(9FsiWGqQFicUbH#EJ1iHPc=P>TCf2e}l39a3L{Wj(0g}v0t z6K{9#{P}^z=>w-(iOcY$E*4}-P@};!x@J%rsHQO-CxkGGc3c0)L0!_Dgp!r+U(3v? zv+J_#1_iXp|aS?la{j|lH1zaDi1<0)N4CPU-eBY=T*z zIZ?+sfN)PpO^NTq@!Q#bx1!ts4cHWq^x)j0`OXmdE<;oho!$CPbbyh&+5bSy-v<%dPykRqmA>oBj*Y4oo6$Ieu&hV7ag z<9|hapH2+&_7ViB;vZwLT@fukPn+-arK=uyx+M=6y0&R<>C7<=l$WInEM788;KC2E zx(mFk6$qcLR$ZdL&yDU-J-AVz9lT&c(x)EY;+$R-_Dx!O*^Fl@mH znd0Ee>%luE#pj9(wh4Ei;;}9H>msw;BwZr=F@Nfp-1XK;mGi2ED_M? zb79ZH3A@n^(qZ(Cg4D!1b{=MhZU3G6Zlb7HGM)|)s{&;_>lI_Mszh{jhv^CK zi2i2$#Y4ZPcQWrE$Z-0BP!ldZo`sW=Lz}e&l1W+|%A^xwP=I~ho@exGEh7`Prnd~S zj0Vfgmu8FLmQhWNPuEM50#hEVJlfc5Scg?O$xS^-DBb0091AYl65zk;dhF|M(aAdx zCbHWn!z*^9UQ%=e5+x`^(5>MekQ-q4bs)yduD-qLvUk}V6sObvMVjSbpE7kds1?}KL@f)|Kwz<3v1QV`!gZ}$Dp?&<+nK1iDn zDvWEtR)0^Y3rTzRD>-e8P6}g(Pn&C+H4__+%W}xPbz&6 z=_i7tJeM67G#HB`*#I9Kn$}`W!NY&tgfFkl`~#KcGU9eOJ+a?vSn#!w$7u|SUh2k% z&h1(4aj%(v562f+!oBwS*TRxJAYpOf)2BGCNA0>xLS_5H>*~|?dpF3H-1XI*YIm^! zm-M=ia?8v9#L}2e$`)nxp#6y|OmJ5bPL7OeUgPnpgySxjI$E0j9x<$YpN~J)e(Gk7aE|Q~S{J#G2!~EUqGO6H zZscUO=B%=d>k#`aZPOrS^x%C) zyIo97Qxys$R{d(s=TODPyV(wfS1LJrC39Z!x!G8j8*CJ6c8Z}S z+f>9-070`D8&>Od9uY>GF}lN5d$dR|@@^?T)sSsc@tm&N{oy#FTR=^0bFwWXjmu*1 z-QDlO8{5})=9WLG&!-FQzM^}Z9ZnXE>z1EE)t6CWVt)ES!fQhfG1IkYs|kq&Ebo1# z>V3Iw=4WS4z~SFBQD~;jw>L5hQuy&8BD69GZKh1s?rIIk^@ z-8d)wa9lDQ6dn(zD63uBR7b{I(CV($MNHrJo42N0zB@GfGA~2r60%aubVR0g2~|J7 zq4Gg*7UaJ!3ozNQo+7+;dC|-&4)2+!%x{_^cr!+Rypoe&m6neCY%*$$4ZDSj|EGNt z* zpqWOwh^)xEo+G1WhXwUUPU~zuW#*jHF>;oasj_Q%5S!9=P0c%XZ_zh1De}Boj2>LO zyLXSo;e<-81cfH{z3P4jTOz2QZtX@j-QF0u`A{<*&R`VbJ$_q^SR3sz<@uViJmn}Y zn2Ll#VkWZ$ybo9gLLJG;$;n+Ql_m0njU&c2Gxo*UbWnw zH;C3{F)?N~9378E*BovUbw*9H8JELa z`broKiz9*!_Uege-@D@WpfQvP_#C!->2HQncI{WT`XJrdW%ZDE=VRy zNOsBXg61v{9JI&gh*)9%tX(HIBp+|BAOa;qK+f_(iy3U+@t|1u;joz7^z*_1o?Zge zjlAX4^dZs|lhSvAV}N%|TJ|fZyCc)m4$|k{Hyx5zSKzGWseoX1u ztZJVF$$#R3CBqHN^^iWMl; zqRb1N3QtbgC?fIMa%Q5UgN=~SdwFdUopbvGG=xJmFBzBxRmv!yD$7JphaS5=ZB}T* zHyKbWEzl?4*W+Axru=rgjfDgQa}`iqePr~{_J>9wr24i7RHcTdltxQAy~BO2{6QI> zRSr0nn@KEW_@~CNSM}czjFfxb{>1*yJ!oD0n#u#!A04@z2quo` zP$w%!xo1?WiP%p(|M93QHp0y-3KEvQ#LwUl{nY9;^YKIxI_IPFGC$QP_l6fN_yZnc z@0LReb7;yEyuz*BTZCiU59FdjM%Z)e_M=CDx0xt}E%+vi!Xsk^!II=|<6=1X(G8AD z5W?YqBPDIuDukm4)o@JWG0*S_5VV!;_9E7h{(O_*vgn4wLw?SnUJ~ErOpJM!Zhxxb z>SDE!d4mB9{~bW*J*i16n2Wa@*AOVKCtQ+3pKs+Ok!5l!oSpOYTE2Dg1{{ClGLMbR z2*ntjo;zAbwqtQ!5|J|VmAj+ba_-+A;Le?V4&b13Hc{YyYJuhb!H#AE*GD44P?`t~LY zWg+-TkPV8|%DyQ>=`*Joet~omul7gsufTA?(q=*84h+Ui5E0Jiuzkjbow+1g4tvVe zcGd#ai&_Rq#w6yeLGYqz2O6CQf~uu>t7WQMS+tB1U<&FI6|XF2T=%V%yx+YOoEmEo zeb>+1m4oi5s*j$;OrT9Ep!~tDJNEntotaUtqoT8hlrj#!oVYj9II`tJWV(0BgVQtq zp`dN&>S6J~1?l3$*T(aB@-B?yfu!q*1-BEfP{%bkh|VheW-8A@W0L0g>sy~i`$G&^ z`D0q|YrCW$j$R9k&;uutRB)Z)nPW<;RO!S-fgXC!Oxe@ESwIQ0^n1<66I-KukuD9 z@dt0mXm?$9YC_vk6P>oJZClrejs!1H-!bSMw2pCtzcUx|NTNC zq<{b6JLsVK&VR*QrUQ~T5vQIWul)cNbb~{N+1CzdTa`^$3@Lc-PSD+8M;=HBSSErP z_Yo*JaOq3m&WjQihxqovt>B%lnIPE+&DAa;RQbzLLE+SQKG5ypMQ)l%iJruWsZ>Z% zH^%cs9%O z!+g3H)CBIpxi!0Ht3GtUutr6^cL-PtwN6OJ@z#oRKc*WFt-`Ejd?C{E;B8^sD%!Df zwq)DJcjGr+#az?4MhZGuE0qyPv~{>$FHxD&$31UkJAfohW4R&GqV(#35{&% zpPg_$(7Lk0H?}s^$#EwdBl62zyIojyhGm<#51QO&WR{yof5*89Sv90Mngh-azV9$m z^3}W=fJ3WFgiByH3^n+H)w|r>r0Oc~7DZkg!qaTOu$52?B3BegNaeEa2zVYBf?k}> zEgR!d!w|2uM(LWm^+pHzlra)D{k#_H3A3-C| zW^~nB=)}scS+C!-SnjooRl;a+5PkNn>YMnS?G1~OOD4F zNEzdb@Yfai*;~m&fe2ny&;&X({1&0OMECOkDIS|m|A5V_r3}lNDco{=Diy)h2P6$* zFsBY#vE~ndROb9vpeKlhW6lDpfwWer5HUW#cNlpunqw$Ob^Kh}ikM#@==S9SdQDeu zWQbL?*oYg{0WYL}mFn6beEP)WDNuOQO>ih|MN0lwK-jKV#5SbC^XXkCP?G5+)Y-%E zufmbXbsUmiSZ>s<#;&9x&WVAr!fTup`XyBLb=Q!ERyQaSn-DSbGq#fa_la!SxZ-$cZQ_L>aIUSd61(2nRK)P?wq9`Cev60S8u%smDKSCefIvM@fPR%*xg(NJ}0 zTB#Lr*pAL&DF#sK>F{Q5<~lr<(Xt^Pl5N(FvN396pye>YD>J&Sz0|{YR?W4=vpWDL zo>1iqVV6Y05Txk-n5MtYJx_#@3_hvL!U$wFiq}zlkI({4PKF3d(JCaVl*5(Q&|8NF z&b0BYq`q<&Tm~(HstgW0Jurn4vo~=r0Ep?z{Jyc4w!%wSjUpdKWs5`{3l=Ry{fVXZ znv?-P{z6i_ioH6rAJM3nTnk51+p5wc(*#VBCi-OyJ$*m}A63TJq0am}CFFhETdg|Q z_F7165!Wc9*e29(B>?Hh|5{>xo$ax#h~VYa?Cv%NPyK`B$P;{7T!b{#W65{gi{e*v zFSD3S0 zL)clEp%;6g$9m`}ayk3Bt>nuD&$hEQjQo9<{V^Ww|WBg9_!Indyfb$t3J6++?I>g{A<&q4;vQaC6lGGeNfL4opWP=Mi&}U!zBujF$Xz@60 zA?rf(leqw1%l@JXD%Z$(kpo+cv?MDSZ?yPhK`Z_$8=4aV{6RC09U4uVQpim=?y@3t zxh7Dng-Gvv)cTCRhJ44p&!N5ZHwnJ127(Q6#PrOv&>SPJ_;?4kgeQS@$J_!dx;Q%5 zlYcXFYbURA*i;S_?@M2U8x576X~&Je5y$`r4ICkXm$Ei!Iz!BR?UDnJo1l)9%QJ{GoJb!)J*4IBR z=Ai~u`Hn;a$1_eep4^HEx(XRh{%Eio^(w=JK5gu+$v}6DRAr$4_k^r6ke1e$j=zpH zY|=R-Z#%N3g}_d;3U50&@@ND*CtGRHf;OfeF|NhzGR1L0_Wq=YGDg(wu?WB+%F6)9G;gLuufDd93~uWU)C9Lz2rNKow;bThG{;a}h2ttBJ%}5uIlR#ulH} zzRj{4Ak}|ZFw{1~c|D`E$Yr@83o=1n>HS>i>kk&s2jwj!PoQuu!D9CrzXPz4ZWE8zl82=_rqG;~uU#*P%-QKn5B>LU z+fZ*ev0-v6`sDwib(!MIN@v{JDJF4Mek=!2iH2Wbd%qBoTg#P zf-&S$ooH|iH6qr1ox_aB0I?1dDg9Z&+2a0NnjA5ri3K!N$R}{nXlg(1$GHk4d`oLS0v4M8xl0c`t^=GYu~kY+W9KQhP@iz1C?>{wFn)*hurw zwbk@ik7loEn1_a#z#qn&-+9nk^}wu5pd@>R+w=v`Qor!5c`kmU@w27Fg*`{d76bkI zh=F2=DP9O(*SN3<8V^XIPPnb&Re%M(1bE4WDvVlgZo?VB2GmK5X5k$Q_mk55d$l`M z4Rdx!8famD5j`gGH8?gcn42RnZKRKz(^59)8*&N6-(G+uNvco97>C=B<`=I0wdArI%84x#CU zFGsOjy3g~5$~CF+n@sh6Qd@^cqzS#S5ahv^MFs3z8`77}h+52%1{92=Rpz9*3QV>+ zDYCiDSW#Yt2>E~NX}7Qua22Z2IQyY&KcLo$!CB)MY@y7W+uHI%3*&mqk2%*`l`XVH&)X~$pS7Bdybfn@ z#C|;CtH9bwaSc`4Bq1Kfk3 zHBkt>jd$~hUH%Na5q^SQ*sl53iu1eaJvxOGf7Y#u1aioruF zLmni|>bTYBaT8`aDod{lhgEt^BM{5-V?`?-<;1kf09TXMPC8tiL38-^9Z(2B2UO{P zx~g!&mnB4~ejRVAIx?Z2DC@B|+#us(naDaniRYaaow{Sru3pmIOWz;o10iyRc$GjU0HcpigqH?|nHnH+wawKCEd}`6?bjU4VH5hUK?CLVp^4FsZn_#v83exlA&S6{2i`lr<1 zRjKQO%H*7-W}7iKo9>>B{d_HNS5Wd&h-~$HUq#c;&3KVed*E#hEmMduL}bh^g{koM zn*(Xj$&_RApA7v&(xhra`KRaK`^K`#+f2q2OVy^QHK#V!C>|A}Y=#wT8PPwj7C?KC zLnt%n!@dmFohnDIHB9}J*=-m^1Ji_fDDB;^A6e_Q8pkxl)0L)^YN}1m2#?C_kN%$= zqHq4?q<`lE;9dUe)yYNeV5)!r^I{M4-)$9=2nvs#FQh>A_Otfb>i;kRaIdlba!S8u zF>EVS3;eG)I5S|6lA zfBbXSS_hxalKfQ_RrxHjKQus6-*M zPW#H^`8$SMf_?r2JVq?wX2|Zjr)FhkMRd576Eu=@bWMx6&P>hn+nVo77t7b&2T0pY z_#E15yT}?&A(_)vLQ!!q}RvzR9OhVfGE8cK5Z zrK=@np>1>y#umbNVdb!~U@k;Dmp!FL6?T}) zRXomZ^GV%iQ>9z|A`@+b#Hyubc!;4>#P`0Ga%tH9HrO?9*s4jNCaRd~Gq)o$ z33hdtuwf@z{z6Sj3FCnoMP)jkhcdl>PSKA7M(sroqg1@9@9i8@6=uInYm%Jz;Fw&? zzc*Y(R-R?;b6@rftze*irp`#OZQ+R`vm%916d&9ixf_b`DolM2|9P-4AbFQw7!^4* zxL6qt6EMNJ)7eH^r2A-+GI+SDs+zE&jx%E(;9Guy)Zs=%!jrlx$!$7b)JTc@tD?Tb z`qML>iWE@;bvzGuKs&xFuC`DV?XLPQRtz=bN2A5_3n@X~Nin*SJxGjgCFL8w@rj1O zY9vqc8{C}KZ-A~2DPgJk;+zyz^k7x9iaBp|S=L7QDVr)Er^=BLIS@=EtExuKQw2$Eve#`7JeNkS5P%{@IXf z11m%6Vat;s!s2L7AQS9<^W=e4+F=YWS z(QK2*JMSRB;Mzv+t!Ha7pIe{|4dwEJEx&P(<@LQMFMX+joXdU4PYT8HHCVz|{W4WR z4IESCK#7#P%9C+V<5ES|>WjLDZQnXcjP#AdTRr5Wq%%1uQ2n^cV`3XIQAKr#?Q4QYq6@Tg@^ih<$vgw-Hy=Ay53gA5x*yH{7 zWj-Z(BXTP`uXxVWF*n3bsdMVn*yTm%0vXqH)1s&opEnSFCB0RoSvRCl)ypT9C(;Wc zTa0%_yOjFP&x$wKJh8$zEZCGxHls=Njug+VF0r0zM@!wOwEjQB-a4wv?d#tb1Zmhv zhcwdNT@s3bh;&MKcQ;5Q-O?!C-Q6YK-QE4(+c@Vu-`^ka7z}l=-FL1v*Nio<59tg2 z4J>IPP8!0k!ewQtCxtmDyZkO3e!eEf@i0Mxc!Kc$F)M6fE$k3^cJ#^(*lMF1{Ir*7 zZw$OFqua%#;=6X-t_;-R^9K!nB8%$55os~8<+1Fh7?E<@O^srT7kf`)S5JInB+4}> z$Wv^yi+2yFii6P`>lpJ9F^ZPn+i5wK*Z4-Z;}y&RG{wu(I(i6cXpZnPo9mbwIamlTDy0E9 z-J>%g7tPDd_5dQOdoK#R54Y4>1565JiX8x?fpgL{ORorB2On-){Jfo8y1*vmF^Vv| zoRVixRBXlKabm$^m4^BG6LMeyjwSOk-OaedfZTNp@J=R|GZrd~ctm#@R7-nO(Wc}pfv->b<;k;pAsxRDf zD{iv+yzEpwIo}19l&h=HPFnD_Qlw2teAMWYfc-WF%~X4kEIFdpn!! zqLgk{`nw($Ow1yEZb*dax}LHYA@RA$I8tj~!lBTnP-b$ls0CVFFHh1EHAp9G-(O`Q z`s+&!6NDj|ns#WIzv+ARps%(>aXXk+tMPvM)8JV_s>{_XEsyk*pT}F7mZ1GFOr(;2 zUrvgX3%ZYh6+y`LZSlYWMdx(PtrOw*qZEVZ?g^<;yLC3psEH0sJe%hPhbhN9hsQ?p*_S57csxSoicXlWe)1t!*wT z7p_cz0}#o-H92LI8n_>Rk{5YIsROWL?T+y@xr6;?TUz#BFY*>U^LtgTb_mDFD_9f=*6!u;`diAVtHHLwllVp3+Kr!$K7GmLq9No9pD5MF%bO~O&DfZU zPqBQt7tJPaLz9ma&Ow7Cu?0`{76P~140f(`CpYd62)<6EUUpcaiy6*)q2?3(PYP_g z`^1HN4jwW~5}{M(eboF48Mi+7LX$QL%1!9{yNZuFw7JH5EENpdLQW(vUahHB7@GGQ zOvG$V8RZZxJ!n*?Gv6q*(=eLk-03Gx3LrHp%E!t~_)^xEQOw#@5^;+R{QoerN()^SKMNLui~;dmh>S?o1I3v=|5v?=>cw8AF$M>p&p&V+>7mY%MVgLO&2Ky z)O2bOgn_de6TualKZ}?6U7ileo!#5Q8Ma33^m+b`mQ+zNUA2;ADbOW+Z)~x9)0>I z(eiNm`J~w1m+jOpMx(m|n2S{wXNk-thZo`nkB%D+jY|wR_jW@GY~1w(j5tf%Th@9g zQKgk}MVf(Xr5}n9SRbj(_O5mz@H92EHB*ghI#4yy&>COhqA0N0^u6A#d1$=or~Ja4 zT0L1b<<5MVqHCMTPk{+xG7t=O{|9g=-M~H&g-tyjD_*?fuBdjdOYncnf6DUk-6?g) zGX|Qi(7#NF6Wg;&AE!?~I2a%5hun-HZ3nx5yTfBTG8tJYSo>t6^*m*)`)dt>UGo?B z%q*rl`C+R1o3rqD$Kjg!aWt&E49#on+fAKS8Qsoz#tSABcurz&cRjdN=rn7rnO65S zLAK2jg9|?d=)aG#Ug1r7x?9m{OOG>~r{U`yVs)EShI)i?F0sC-45YP#ghbYeuCG=w ze&4d>_<2RFmVwI$AwVCgf|_)(cva8o>&+xhoWE%u(*lW%Gy-=ut3j3X6hc z>AyXH$yEO((ygqT!L*cC?%06rInQvxiHqB~{sx1G`Y@*3*{OE%(xpdeqltn-AhzQD z?emWeBnigq$}iyl<^nP>c5c25pgDb=-gd@@yme}VSo3pdVydbuqn$#%>EO2ZZlXDF zB*NjxYdIyjI?L{~IX}xN&&hzKW0VVD7z{FE-$S$Pc*TO-4Q;Rx(yU*PbE4h(uy2^) z?i_g1wyVLYpfw?I8beu29AmD3q+Lopkw z5HupsyJqCipT1x|Lc5JU6kSY8Pc}vM!&B^4z*^Yc| z@vT_yj*T{2NmznK$GSV8$)xp#t=}4F{hr^1e@V#3<6zcQ&{^S`fh)=6(i9Im-n0q>qrO?4^fFB7oz!G(X!eN=OhBoU_Z9)JOHS#f=|h0KoLq9! z$gy?-S6@)AI!uU-7B{xmrD^D6tzY}|N#T9d(FnOL&E^aZZCx{?>$?S99)bt2o#py3 z!&~hG(Mh-?0vw=&&Uh#7Q1`iU-Y9of_WmZ`ryr(t{5&*d$N{nsLjGDb zR8-#sgpPOpEJ|~vur#$I4i}_`{ED9mIC(F7vJWOJk z_~77_Ua^F%?or0ZA*50|{_X_T@KfW%Fa=kQN;hj) zb!_a$?*MK`SfsLai#xUUiC$=uuN5&xvt@|OHwO2$_Bgt570m}T=V{aaAv#>$PIIHq z(HZ(VPW||~0;Bj>93<{*(>Y8c4>ogO*pZ!)DlTi=3L-bv*KJJ8nKF-B()WIVTz@59 zXSYh$vSLlAX(^}?BamvF(=Z3w;O#^%sf9N#*2;#?M1&+!S!O<1tp ziyNxQ;fW2POA*pd;(yain`AX712K379?5({FF9m`@P4I#+Z4{ZxPm}zmZzq0-iDO{ z0~KXO1$L8YLr1~vm zB=R=_qCoo3%i)3r&x+TgQ2p(y0L2%#+&wtqmcYSz+n$ICEk_AocM^*aQyPln-=ePz zpA38LUS)!hNubd8N$>4()ROfne5+Uz1_g~rhZBkYLt@bB6wp)meFPye73^wfcWvbw zUAPUP3+x2eI5jFJUIe?G58xNCsM}a0s1dU6n|DEly`*8#PHu68VBxo^evN6yr3(c$ z`Cj}%#O3h0LjnGGqiNf`$dWapm!!(uTYZ6zeJWgR^(U#*?zf^Jh6e^*3gWRAHe67< z-WuH)uu-#nFuOjD4b=7_KuRz!?$mNgZ~;gY1=0+)lIVu(y_<6P9Rb7WREkzpv@TXD zv}UGBJUkNjA(sLrZ-ywwwc5!z!WfTS5xwt7J<8loGH4Hb@=JJYA6k^_ZdIKPQ|b`s z+c&DxyI=O?d5Ar9^!d55yzgQO%p_;@>ae9-y(^bGR9UmQ^&m|5He7oZq{Jk%V{q`U zbaPG=gM6_sspTpU=wPFJh zK*B;(mpcp@_cAImsH`_pf8wv7A^seHl{>;^Tx%+u(Anu_W+|`E%mIo1Oy59DT=T@$_fdof*Qa+j zF%_p*ij)35Rx!FVqz3W_6d?; zXH?%*&plzXF_VnKZM)ovhG1PZx;6*Lcm%P9E1ET)kRFDF>Bx+&y`6FLJHuadS;Dp< zk0sm-nJT|sobQ>uERZE;A7-atLYSKJWUAgVctkAJd1NlXXBj+84CB$<)(D16#lKal z3VW+CKpZBprVwf|OQcot00%+(w3FK=hWmKFMk%f&o;aAXY*WyC=4Pi~aO)6?aaUM% zW#s0z?Dp`!f%NdJ%BU6jDra72yG43h{*S)XZaaKuph6UT`&ejE*kUo;g*%z%btgqW z%ZW29Omtc^PfK%~t9@-~ww06I;7!iG=w?z_+sTQXO219NW`Dn_L24a#%i4c3(fDJ9 z;^)wIoaWEr7g_HMaB_9p^+x%(&bEd%skCV>+)kRdHP!v;rr4*@_5^j`N^OJ;7rKw? zD{9_;sbZPp_TukbHFTuUlf%pKXt;)E9FqI-{Y1*$lg8EO zYG8!JLxnhLbVr+YVym$bznk4PVD&gJFAsJ1Dkqz+x?RmZ2#rrFgyStvtGy!EqMvk? zJHOVSik#N8wI9IGvZEa-6@<=w2*9l5))2!V=E+kx%eg4bx8YP@Aexh;gOrcaqX1K! ziRM1E2dt>doZGG>Cm{TQF|7!-+BeimIru@{e{x{+%qa25{bAXRjNd>LDf~wf?Lv<6 z;`#26zA@DwH1A}@2(Wj5q{>xAu`v(a-e>01^usqL@~kOl0lDeH$+bZ<|K^gq4BlL0 z=>(S*O?74dG9Xf$6LNA^kS}tM7XxxXFu-^0g`G=`5)9bf+l!kI575$%B#}^*d32O7 zgC=%mCS3dNa&~fm#iyZ5^^2S$_YejsNGPhT&Fd~wQ^7uEjUQmz7^Qf!jHPEpV4v7+ zX(BafZWR?{>re^-sAWpo4fjxvGekvj*^I_o_4fqzQ{vOM4@2iFrA?LAZNMpRcSP6F z%w1vA3?GS_4djsuoSnKCHQadXRh(9?gKPEl8h70_6ipvMdKy_Lq(3&Jn?fpf`^r)e zp{bC6o;#{1GbAJMb*t*qU)3+|o4Cpx=OxOy`}hw}790X)KU47w2$?S(%-N!elX!5Y zx8V&H>lEL%_*4h+Snz4zj$d^NG~7Y_py|`h8&s;YYB?d?;TVdQ$H&vsTw!?)+YD143(jbUf!l<1DR`gwMe?18s%t=LA) z&{4c0V8QWKccN>&oO69_uS!LpF4x%4g4+has?qCcnNXv7R`x|PTH@byL$qCo&)uQjQbcKQ|b z_x5P_Y)nkj&UX7w-GE0xw7D(`jJ(M+Ss*(V!mh=Jk*)K-Me%3%%o0zOAj~A6 zUXW&I-~A6RawHD*7xJMf1Wi&%K|$yh;6%qV-Vq}XBP|5dtrhMrGj8i_OW{Lr41Q3gqjjx&1+9-FqjrULo zzTPVXW4V-DobQS11orr*eQ3BN%;{+~C9Uzt@@rT!#G)CYQG?~i3ru&JPE68n-?j6c z$dSag8nvWx2rgH|SQo=r!)&w)*LL7Jgf!2SyZFqFnH<_Vxx^WqtvuN)imY=FU*bHFSex_g!VLp{vt4H4`Krgi&uf6)( zuA+MHE1;5aSNqfLJziJB@}AeS<$DZPefY6iRiusw(ijbJktsC_!-!LOZ~R65DNP{PfUMv0RZ1J!WSr0e?86`{foc- z&7$1afHdVJ05$}ijA(28yC32Ahk;Q*(L1?iwRh-$nXCZ4v-1kLRDOudi2p!8zu^Jf z4DEhx(gUjFJEESAe*l?Y=V0Dv>Q+kQ>)*souX6x_48|G@Vxa-JPwiy@clb~B9{7i# zXDmN(!DCx$<+=YY&;$Qo5F~09RpbDDj{2Vu$VR+?uW*B2=fC?5x&L>E#P8yNMI>-5 zc&(+snAx^45LIt~)%{2KpiE!h%gaZ@)559Qsqd$<%k0@b?q&=_4edK6b~QIxNOPYyRw*Paj>Aj&AsT*RupKF8NsEwlOWo5COo? z&?;Rm@xYzVg)gvrt9T9<2MF$#%4PQgDN3DJ;y$YJ)1;D6CB=SSV7xe@Dx^T`3235qzG8d+nLxO%6dM%xT6cB4N%>Jq&n4tmZz zJf~f%+1A~S;Wg#*<%>^BVWSE8;5MIMP=TqbdcAH~ir@I1s1t17-=6-EX_pXuhgrYViB*dQ zUKAVF&%aCrH@$?d)l#k6XTYgAc;=+M_a?O%0G4g;AkMQt zFWVrvuoJWwNVp_%C3tP1c8#KI1&yv>aaebQ>$H6NWOG*7xS41@Iz!Rlkr8o1TG@Z7i@J3pXjPh>l%O^JN*gzV7fLgoaJBs^a+~dAFx?#;|GEJ>nPKb zN9WU(eJ<;fN0&ZejMriaJK{4tTX>bIoc0%=w#x|A-*6n&8l;&xUnM^+9{{8ZsRf7O$f7(Ol=F>;aGDLEDf={RC?u52QJ{7w zP0`SFe@?}uh!|wu`l8jbrKw)hYKF&qrV)UH84_N-)CqY%F;inVsUfM6!mX~LT3GFR zGr@5RfCFog!X}ybipr}5lg-LT{ZAKA1nN!}5pHdd+GTp4hx1Xj2OMt0GcnM;x&&CE zma#M4uOSc5-qx>>Gaf>E*@=uxNwEuQfw9I+up|vT&-`&C$Jf9Rs?@ak#Ye)NY28be|d>Y(#q_T5G*s@-OF1 zI5-6KJuSl(P3WNS-7#40toyj)-maAV+t^yY@<>gKcW<*(Hu)pc&Xyg%A2QNPf22{F zQw(pqS|8@1Id1-GTX$zBGk~XlvfKcLYTAxs#yYskJgwx@aV1A^Hozc*!1>m{uP=Yb_Is_7|i|x@pofA`Wv4Bi%}M_r|xsS1wv`o)TfcTz-f^AAMwZ zbg4aie9L?^K1^;gA~>@(vNjK+H4uN{g`Yu!0?p8HssbE>z+3iU zenbNmvgB#RQC&VmtYQu?t@1;a<#?*gDnCxd_!5%Ni~SU%;kuRQS95YX`~q*Gr>YL0 zbAGrC3Lg5JOIR<-K5Oa5F%0l1PzO?1o}px614|ifRmta4@Q>CfIu%Yobn zafHNiq`C~Gyb?*1)N~ocNOp|q#v7LUDmla>mBVXajgB}6r##*TI7vI7#;1_UJu7=1 z@%KE=@sbwA2=Ioc4z3O}RZi$armzVToHy7wY6GtJSY1~5buQ*o%GUw1hW(ni zCZnGRFw*&U%-ki*1IIbD&0U;v`Mlu7KvB(_g`McMQs zgpr?biWC+k1ReT3>vmfcF8L~AK}MJ`KWG5^xN1({vmsaR zkh2Rd63ocBe(sJhW{MDQ>ZbAFv>f&nMMKXmS3>&?`gr{tfZd6hcbL1dKJqAvnp&d8 zynjYA#@((&@vl1@A-E4XcG?D=$y5^G6o*rRiF<;Cm{V&tPSxfZGz;Lr7zd?4C@YRMNP+J9#j!R%F=X)ylgL?W zdJjdg#d5f5t9oudFJ6?Zz0V(4^{Y4F zRDMi;%{`+G8=%S{QTcVFSb>NRk=RwANKkNZ;9m*E#_K0TKnwvEk zRsDT7XcT%duu`dNWPvS&MHAgWG6wRz)w69kT~1{?c@^J4FAZaSFHCdS7#j$fz>7oD zQYaD+C!l_bN1y|C80rPe0`32ZY^6wfUi|W;Z2>oa^oEg53T{~(*5mdDD}nV!nXBgH zi%=%uh^&?=d*79S$q(e*d!JZcvEoLCL{6c@Z95Ui`B3=il>!!Kb>xZHs|af<@6R-= z5%dS=gK#xTDl1(^EzK0&j_=ez2atzSdQ%{-@H6w`z_B_ik{b!_lwJg_-0oW!`HjdDktZo6FkTQA#VszJo%Zc5wKva-F?Dd#(FT-L`A*zb*03 zSBD3Sdl>Uu`3!uD1a7N1-zTkeJC2p)X?4_&MMI$&=-#;1zcDnKN^!9C_+*J<_T?5s3b3m!0f8EKwDBPC5!j}zLG!XXt++J#REET&&k zBo+C*EYtXc)?@LCjH#Ug4sL(Q_u(1=>%g9n5Jacd*17dj7xP%Er^aqKEx%zasB#&O z_ciFsP**l?YL7pb)A1t-@|^+SOSf%ftM=w<$tBwLa%FCnjxPCTv)ueuhU=-2)A0~q62XS^hNwZ`y1029jCr#VKngDd{koTJ(~SSQ0VIdjfenn191 zQcym8Kr-S`E~VRkF%?{k%^)>VM*yfc?Yao%b~>0@jDwXnGxsI_t@juVMAe>1%Ia zc(v3W_idW3Np4q8smOKVSEQ|52z&@(Vs>6y*!YsG0lHpSNYrNl6k`BvojUkV4aGcR ze#Z<}sChp$e(oa-?O8NQ7S*0YCfxVDI`zvLyjd>~6nH4xFK%832)(ApM*si^eE^df zQika9F(7SyHlN^#Puu>?ThkMl@%$nH;($Y>Bi|fY|qKR+k#SLus%T>NG6p;vGeJPzZHawH@~$>trOK+_}MU5V4wC&g9@y> zCI;CntR*BLYFc`uFLelpAZfAh{PyMZz|^@E**2L5txC!5;Q>|*L^#ecQmizaAk&JK z+f)1A_cI2=B(3jo@Yg0GyE&OJ3O_0v98|6Y@FKm+3FrPeO*hh-W<(ZA$5U&G7-sFM zlwY{ka)w4?6lv#o&`UDrvDNSNdsaLIM%<34mQd!q3(??;rD*J-$;UjdPbg-z9xl|Z z{3=rX*Lz#cs=k8J$fq_i@b3CBCy|Ivq)6HbE=mf`gB$ssOGNSZVxuwkxyi5oBq)A) zK!JkaCbr3&?_cBaYsxj@z1E=4#doL~NNEkY*^%{T)(rZ^DUIPv&`9AaJhTGD3{G6^mkdVUZn0H#G*}I_Oa7hb7>u( zJ3n0DX%eRT3TPpUN|V(K{ils#K`GjwFfc7+<5*93cCNH$rQkSn|w+SDkbnvOXE8CY+S&p*7DlHgmpT}$3D*h zBh_ius|I*XOT~Fo15G01S5Z%bi_l@(4*!qRjvyJNqvwl`=VPU|jDU}PwWTuDa7^9G z_bDJx>C5>zgU<`(2mq9H$$L!6b#PBZPLWUe!|3z%m-rYO#lA&un@ccm1ucwVZ^&Ep zmMbxwW~+BcMbl99FI{9ktDHWDBBz09&OC)QocFO}**)JuAo+lwc1^Q-VQVoiCLw|N z4?VbVT4y*q@{W*x&WuNp9f)5IE2X-@p7JuWh9e~m$#9cnL&IIGDU_-b5hz07NsSuM zqCzOR!Zr}-MF}WR&T!MM{>Oil_?8b>P=2Z6o#xldEL`CR=B9%wx{7MHvAhq{>W6&F zN*P#(2Kvqy`#YO%yQB5Gry|w;PXu*R$CP$yYpi!)u%bQA7d=dF4^GGAP~CvolMxp5viGJj~}_90G*Ut@|@bDllOu zax7g;hfxWm)h_0Z3Wp6&8Q)}nA$`>W(ljf;R0)auqL_U2sEZW)y6I$IhxxJ029=H> zzqlR?$)P=+A)93>O5_uie^+illc-`{ve6}=koBz`w&ZsX(TkPTa>psR7a;fmT^9z! z1cyha?4qnMj#=ne-1T3uya`p#IAmAyP(R_KW0*Se3|nM+#782ifnf6UC)#9Wsk}}) zT3;_`h=L(-`FP2DZ3xBzN6hQunU^%(wS?pZQbNEoRG^XvC;0|ACEK^5f)_BA)K1&1 z7XCk4X@X;DS}?#1s0(@)JD*}TCaXT77EtXoQ<+rM_>p~Hi?3GsDlRW@|5(hM`W(oG ztteB}N}~E?j8jXD#wg?_rTi=H<0bt%B4c7NM7o@gB44w7?G0y7F9W&~ZPPliBL)bD z30cTI?v%=^X=MC|4a$J2ml1~UmhXFjDXOSM8}ffNz<>sDeaJ8CJfeyuDM+{fBRdpf z4S2a952L%ri8uc}`!7SZZ3_5yDN^xZN3TNKlk4A+MdSyhT(ggR^`?IRZk4oU{!eNP zX#EG$p+3}m|6YPu6`B?}XN*n?q+_)yq8I-08o==$u#mT?a0dZSt$3s^-@gg6-#t`)l_TOwMBvvs=8ApoWbo3t(ZW5oLGiyT>irrN6LZ>5pnkEImU1HfX2dKm(p8zIvD zTuHn#fTG%0rCq&LN~qb$^@>c|V4(fqmsqRNBZG~3k@v=8PS}+#h)yS28&wrOhcolv zk?~C)vMc_}Z!k3;brWx?o|)$qUg6jML_~CT^>!|GmCph~Eh)9kT|hEU!Bx}&yBxybSpwm2k}gr_o>Ecfk5c6E8a?wxj#(kte|BnpPYh;EVxUe^zbc3#d zfScQ6(P6wypte;_v)IVAF>wx+(Gs- z@VzJ1?{~}u0#tYq4QjQ&O9}m{Q6U}Q9&b4|);I(a%s8Rj>+=}SN5~c*n z?dHu-?uDNND_Q#0)A_cqwL(L3d#@d_v(E1P-2P;Eyx37o>>EZ_b%=KE;({`0+=gHO zKk8tX;MsT>J(B*DxFz-fv{HJ)iefE6gYPP07i#(+rDdR}9jHhZUeEk})>$YJm(k!+ zF$QkCtV?yeO!_lMz`u)n<^nJs!1`_dk>x^v?+F+d;E`cV0@AR}MuWJFftr2$?GGTR zsIr_I^WPkm4J4ocMcf3#qSU0@${UAp%S6(*Uk>GLvpbC5FG!O)4+n{d#r)E2IQV4y z9lA?>Ifx;! zBmhSV5l9VN(1vuk8!539u37KW0EJMZjiJ{Q?5Oe)?c3QO+cxqYil~aHVx*_f0TbSI zM{3+^8@B^%fNxoH3}9ZotCJH8sE>LU`?5~>ok1n9fIU$BAq`H$-1@GnyGl{6b~Z-T zN7P0%GH>nU@BDWDS8R_5TLRSg1gT#hccA-DGQbFsHRJ!(0;w-qT%)uSJc#iR1QNfO zqzBAauU*r&83*cd$K-jW4wNvwxuBMiVa$CEc0iF-a&Cqs(uqVsE!?fQHAw4Z$Ti)J zZs@N$eHrkKZ-)^Q^pWustW~A;rZ7}e0$y>sX5jfL$$Lp~k+9dMXxoSb)l&is3vp*9TVr@l-U>r zT!j+Bou_{WqM$XJCudMLOe>34VT>1JJD>ImF2VGu%0lUNAAoHK)Gl?uYBTLiHvk9{ z3|eQ#a=*K()x!M38{(o5n8GMmA?a?|s|~Eb7#hFd-i|ntlBb>R5%Ev%I_XJ@-}DtI z)E3&0CDY#9R&g*xNf^=jHa^F#CJ^8Q05{s?cmnd*;553l{WZ$LCiojPgp6P%dpr7Y zqnc7sAdF>)Nvh3B#YW5uKQ}O%wq4^B^`RP+$zbsZ7=_Ml*&r)I*&F%V7y}vhKLhUN zB`~+WuHj0cep$Yk5WJU5LFV8^biBKPTDY%cSMf!zFk;>X?B{N zr6&@J1cXcIqh1i#D_>eZ2ymAqR1^UAfKG;oC#469=#p2~FFOpR+DY0S#DNe8>&+N> zGs^MT;<7`gYr~*ac#c?K4X07YLyM$EpxlL3ZdN*(eUpOaXJF&p@3bNW%iK;YGf}|3 zA@jtc3M5tPcyC3frJPC8Kvc!J=eHWz{-FL#KkfGt28hGEgJ#fpw=v$JT()iw3PX3> z*CG8*!BL}Oh@TL~o>aez zPdEFg5-hld=DFS0&fv~?jBsxEU(G@pP;n>WJlt675mX0GSCNc<%D^-kh2 z{Ze;~5M+V;Bl@`g4zjfkK{%J?sMvaMsj_*FFOV*fz9JlU-wLt4G|53pL<4`E1gM1v z4fs=$jd(wEFGuhXP_K~C{WS(61gBnVnUF<@K@r*3X(yypY|mfn(<4ewOS$9{Bb8fx zW??GB0nV(?>G<1Q`}^k!>h_O$5RV~(a5tJb=b?m1EV_}}-(0{#-Y}XYYyiuEGjUV- zz2L%k-TzlA(<_?%S^C-+V%mef?XVXJlob=B7r8cW>ST3eI81lakb`i`Zv(Cu3Y8FL z-k546ivO{Jf{?w^-}t<#9IFt*ZNj@%OgNMbp%F?98+`iT^v4|<0X4QqWQpoW@offo z0|b&D$y9#pkdE6V)t;>{nP1Z|UF6>^h&Xl{%hG6*{4?1Ct#B02t^%?WD0--2 z-6Q5E+SHQ?tUsX%4NC#z8Wf6VlT8UBWBN;y#sN*3n(@#(aJ8Q%*p)J%yS^P{K6rem zdV=kqnP17Bkg5i?P50~Qg3f?t4dT$jV1O>>?LgfPoQNPabJ{>fHpoLc0og>JYTF|@ zX=%9o8bty|1rSJ$Mt{8wm!kz>)1a(;a?ylAP748f@EDr$3-I*8_t#8oN~y+y_OFMM z$WN4LFPtB(e=!5BKw1jG%%&#;(Q40BfD^&l zXA%kE{(wb570y>^2Uy-h7VR)}QV?}KkvCD5w&qD$T! z`|z`FM&w}0_t)4038IZ8Z$2>jIg=z%Y)6E#f8OKw$j}Ps#C8=-fgM)? z6k#X0NjD{kAwg2<-c(Aae)hm6DY>Q>R3kE_&J>O~^Q>VDBY0Z^3Yd@xDJgp)?8^V7 z=mkxo8fJ(GNQ6o{6pZDIKD3rolV5^5(5>6!mTy5=#>b;9*CDnR;f!y{E~yMsei8>2 zNkjG!gV%YWmPcSHJ*ED(wSTD)e61(8+tm2A-r;$zZcV2h3voZ4~NK zpFtrb(pBr(!$Nk`pb+~y&l+nu{S0uJ3CvZ>*c(AFCd}2=6 zeL}}tOB!eQp7AFArt+3f!_}9!(YqT|bsrxP%ROY_fK}YoDFccG8nf~PB!&MiGHPtg z;TYRX2_(pUXz&nzL%jUurA*$NUR-vn8dudl*h1{pLcVf&%A=dr8?4zE;YomTF&^>} zHxho$Lr4)L*!V6 zAb;WBd5z;mg>hpkovjus#R+*=pa}u#OMv#y_}8P6__hbD^#ZO?Xu3kwR;VJKw}5U# zwur5L9JZ0S^}xxh@W96}X=-V*FUy1*pg|-%Rm~hg5q(feTJVlfEGidmg?u3c?VHZa z?Zq0%u-<9qkVw(6jEC5drMjo%aNA$?XIv7+zzLeBdw@vIFjy&3&h#5VhY|=ySdi3< zb+QBxB3#AIzy$mAsK}OIwzz4+I+lxq7&-lgmIZ$b&M$MyBDy>IZv+?KblU!PfW3x@ zpP{b7MOM)-o$1%8S+Nv&1aQL>2Rh4spF(asQL=P8=SVHvi3K(dEevtq;juu#Qe$^0 za6~QACHx;6wSvmf^A30hjTHYQ~a7-^6PJ0$(PbuTffy}JALGptgoa2>A zEeunGmi_lkf>7-BZ*q5~A6UT=!l^)@0Gs*|_GAN$1noo5 zmk_?;465u2+6V*mNuQ4MzaI*TKzGx@GXd(Zzs=IOIXI|cg_U$%aLAx_gcHU`j5ahg za=LQbfxA^XuFwjV9$58(z@PD7OHSf>SA1-VSS$$=2GM{RG?J^u)k>7w5M$!1j0yRC zPsim;gvdmPaa5`Q4+(Ul?Hwy zXD}zDh#O3*0lckEYw?&VK-gu=|6WQ65~QIti$$$L)YNcDvJZ#bL2Qwgi%r7!^8*2k zQR=j}mXF`2Nk7v)3Vnd{3gIecgCMa~=)&)mxP&2Os*+a}h!-4lctx}kbO#0qO=}Sj zHMsVmy-oI`3E*58=kjfI28DHd_v>8J0i!b&JUW*kjOIN8u>vsyF||CkygpDPtH1}c z((Z_8tQ!pq8+1nyZm73T)>NQc9WcRi1IsVzuD{1N!s;jrfa?S9b>tP)^lKpak%R=% zFlGrJw}37YyPNUvBV1suHaF}@y)`0+lRgWlIwHxZBV)InBq|Qgb*u8HoB<)=*^I%` z!qSwrXg@`HqM_qo`e#lD5Xx2V2uZ@p5@b-SIR%za(!P^3e+iY2XXxs6Y>WuMd)f9Y z@&c!=a9>LF(2Eqbv~dL_kTM{Ze_S~wDxw-LdLSnlg=PvkrSx%AMKp6+Euq^OuSEp* zcc!^rHhy^qg#h&x3!>GR^N4enfJG*r_^boB1EgO~5+lwJoUlO2U_PS~FY$#yLye{I zu;kirX;*OYU;_6BO|~`Fv0N!t5MKJMEtOMb(Qz<8ldlcoz~#b{5W&lz7hN3*|N0%| zAgmxKTQIDR=L{Ka=JJLsNcChO%mym2^1B&P? zcZRG(*UqZ@ES zF_~!CMjIOke$oY(k_BwS-v+W~Qdw?Tj1c%0=U6R?Z)TOpJZ@E6ep ztl%5yVMeD_>YB8@Cd&-LXyPF>F<6HRzuU3Vi}I#pz|(KD`AiE}LG&L^Cmr)Y6B`hJ zyBkf1;w6UuTzPm#0sjUQAO|;}gio=<6%{#hYn3n@r;G6P^|b+}9X{AEk_?pkOh1aT z9i2a>^q;5l>zDCFc?~!Wfef$&`;9L_B0XEL93~0OzR~?Wx@9bA*1AOU*Xf}6n(YnL z8^2p(&ICjgEiW)^(tYk2hqHRZUX{!6+5a5{{33q_zPftr17rX|d?2k<2qYjpQva-R zXV{*%@*B*B1RUaY2Boe*KE#unG{k@CVG&?z;587{ zLQe}JK_WJ`6(}mgGgRQ1f;<U zAAso$0k}u5LXMrP2G#2Zng86h6`CjP85Bx~^gKpI`mPA|*i+~b^Q;A% zTcR0Q!Rrf%hx2r)F+t=@d;LutKyE(0}ct05*YcbHa7Je+I-!0;xnn zwU)^^#QPthGWtx=y3;qaoo+a{nvFg7F{Bhd(By`MFwd9YAkIzjZstoOHerUE{@ zOx}OI9U?B+<X^_tO53Q570XSxAiQzyF`tVK)CQr@v`kThRnji92kW53&@KJ)%Jx! z*IO{?HIVBH5@+Wfv~(U}?f_!9Oh=_8PK#KlK~a zJ2llIs8`0&2>Byr65`2mgr8OBfFWBMiYJ*(3<1m)y5hY>X2Bx@+KvD;6*FajiFD}1 zibL|2$=_br#~4qV%|+#Hdz5fr6bAr zypgs641>aq4833Rq%TE_5;*fr4B~ZxwyHctpa`nIYBR*XAsPeaRDu`GxtUU3Tr^k= zO@Nn3baL1ZkExPe3yJeLn?T0k_2~>>82&0u=u^6d(mU8uKR!gOnzRAQZl=P2^8SKE zO^kccK0V$}EGp&qqjJq`2M>`-n7)w7c^Kx4WvgkOG zLz1XW>y;{x$v?w~2%t4Svw|+&zzO52?mW*2avu*vPP*`Fvq3ZrPu96l-6hf+(w^WksHL`~&RG6`pv6R7JDyFf{AZf~8St^4@sjS(O zbtY*@_MJi~+h~y8n7RKmO7HvrpJ$$V?sMnfGw0lybI$L4f4|@72(OzDU3>VPz|8DzJ-m$*R%k@#qCo3CAxYX1RbbeIGmhTQbIm7({9B0#HF{<&;ifpx0 zvNn6^Mjb9GSqFRapNar?*FLa#9rMf{8gscgq6|OSGUyoHCY{*uyJRigj^j>bpg%WB zj2zbPc{XYky;EBi7OM4iidVX+hnC{lpk>fmx%_9vwAti4@&cP{_r5)=RfTRU#bO4JIw9zcXTax*P0%-DqJ%-8hH6iHw;kg z1I^6MA2#=3~4+>DA`oEkzY+B8N%Socczu zaChA@myUj*Hh5!2lh<+IO|O0qjxhU2_{@0wwM$UHGIS5WAxvey){hy80UDgd=ATy-P419VK$L30RB??OC8m#!S!_RxtKGgJ zL4|{_bFYd-2&kNJfoU16Er_mfIh6$=jft$R)ZW5Ki4?4DW=Fw?73lKW#orV>!&`>A zx9OORCz7~KfH#L~n8N#GzuouIz(S}+Ov7gUsOI?vMq0jpb;e~9$!5wKZmVq!#}CX| zEp221zC=Uo+a;@FDg9#6xdW@j;-=@v;ke!Jl2}QTaVi{Xp_$Yu?<_L4}1KF>1#dm2gDYN zncAwUTb3)h3wce(U)$dVKalVeKP=)FOHw7D+7!@qvjAh`(q8{&Vnv|yDj?F~^3Ux3 z-Q~3ecaA38oX~+5%0<6y1O%p2r%Z>or;4bWWjmE9FS3rQf4(C4b&ThDtI z`sK~&Uy5z_me~2jcOBL@PG|tC=vcTwSs@obpXRTAi{E|XwKH~mf7%;McMoW#QoV8x zm+|8c%4OM0IkV444t*^~#x4KnR>ulHv|EYGivV>booW3DpN^jl<5t%=phlSz46*n1 zKl1ahFBm4-eRk2XFu5&l;gj6B^p?f=8nO&XOnZJ>^7Y(T`zPs|y6Xvxw#pqEQgFUj zN1Ugdog?XF$t8;W5nNSL_SneDj!SX}uchzyfp0QIT$wZjWyw)CgdFE7kCuV zb>AJnP3V6*A2d@wikw{503b7~%X>;Ah$m^*esp;8dz>(A|k!M9gT^f~~WaaaIp@^2~ z;rNuIVqf1f^S7l%Cc88o;q9LQXjX2RmXd+(nIxRP`i4OJu5E*t;kdrQ& zQeY*Qz7X{n` zfN+WgBuhkIK}pwlo;VM&5Sy>ucQF_v z(`EmoM>$dGQIJ%7QDKB5?zO(hwakDky-5_7U<;DNV9PMg84K?fE}`^uua_I>;FqS+ zUk`hf7?fu!m!gpi{FXi!>HaX(6mffItZ`LOeea%Shj2HC4{mAN(dE{Y^og~Hx=gTVXW6J0tU1NbWdXo%IYaGzes4tt{#Bu(8CW+ zRA?2?T#TRm8!}b%8lT=I?y9Gfoqp(v^y)&U*Lauv<9s)0!+KO_5^bZZvW6-5>vT%HRacKu?_r1U4qUU?Br>yb^=a^f!+UP1zh zU%37}pJb5$* zAQkq#>hiuAc6IjnLFa1)89Z7tz}aM~6t%eFVh;11vXw1mFemUz;!CW*xMg3y&QMJP=xr7Eu(A>BdM5pMjo+=A8=08W_uYATTk>{0(RWef;@swuh2(tPD)p!pP>*~% zw<}d}?^)XM&Kd9e2>*CQQ90vaGQs?3KHs3K)HX$05@t@kaG5gEb0Y1)j&&u!LU?w_ z1PQYGOKe`hI&_WlQc2Qu3^GS}S;=n_r=Me}c^zUSFlopTwg`l??`g_C;-S(~URub6 zN^Qc3j*7idd&C_E*ee8@j6fb736RIpBvn%@z(a*tbAr>p+WE@vJ=J*M9n(-nTCvSk zYcz}YMUu8M5WlQf>kuc^V7J*!)p00HXiN^Su{E;uZ5mp#x&SZHVQDeNSS1Vj2*4A& zY1cdEBFoF$!&K#t22qz_o+orrBXM0C1a;AxsiKMSgXTYfv{a(ZuqX`*RWZh!z^Ue*UxFPdzb z{XV$X=Uw{^&xJO_j8I*vn}PR^Ag)-;nCNC!h?U+JLMwe+WQq2hHy-{h|GrkmSKacv zrH>XM6WDzBuPoNTb7sH7I>+?}cVk7+N6tEdy!^%x(d2h5Gd&wV(E&dFn6o)X&AxA% zQwMWK8ke6#DHmX`Yxeqv^XHB|dUW!mm>K-?g_802YFF>!m!vS<;p*BmCt=5Db2}op z5=m**)9!FzcMd>yhdMB;46Jva(4)Na$*SI}h+qnH817t0CX&_M=p{l}i=aaGd1&I3 zL%qFs=D3Plgth5==6+M&ut8LN;tkSs#Z@}Z(?rS(PdK$lC&WJNO^@C6sH8_j%4KL1 zk0AQ3P)=_1>J&*9APz>2HRHZEVQ*z2@y$JC0wYgDO3qM=aX~&PRzXY#jx<{7AQUi2 zvTycPL{j1>v#9ry7~u)D+r4|LM>_8tOTMe$H2{@95gyRnc)AOk+spmLPZ!WP*0&buK@<#S7k$@Xp497+)V)sUXtGF_*a@Ca1Di}oHV~%TJI})T z8IvMH2ZS7q8xvQ^k`F(Mo^nWG_<>m^Ifto&IcqH25HyNyI-Z*+u%a5d>~dxE-YI#z zGtJAAJe}xZO=KF>J0WrJhH%|so86li_Z0|Q+6+k)hV%$FJPjf1q6A|Dn`5Ro(`#Da z@xcj~Ef06 zi13M=F2{ry3oanX0FI?+A;QhQQB25TCew1^b8?F7li&+0Z4SYALdxPuh^gB`p2VTX zw}r1#cviuKJ?xOc7-*J`Xgj;OmiuX_N8S4jnRIN6O7ix)xe~kFs`GA1i$AW&9kXMN zh{L|8o(%ktOuTFd z@BS4E9ub}|*)ykeMjY-I0x|U&c^!iv2}q*6jcc)H+iB*tB7vo-H%kM9$PN2sauDfr zFqr~C)!{~Oz>JF&@9PRY+;JnG?5{R~kQ(0ajthc;!Q_Lwo67nAA*@th-@Q7j4j~}L zl8#G0l;y|j>-utpO_d(ky=$wej{5M3=v{d1i#4X!MV;3(TB}mvHS7mP4;TaJr%5g= z^8ilhk?{-dG2bHp3kCwa+UP(~)ZNgoes^ zicd(zUU?M-O?Bwe>w0;?HUuhsC)iRfT6bPLc~`!7aEbE&kwF-$I$|1HEPN&UKzcI3 zC}vq^c}NgQiaMWrn`ZtxY_4!k3Em(Hu(rMi2;uNLGs+_Z@OZrLuVg;K_V4u`BK zO=rY)OL`VVH8lV3$DQb+@6bYRp0Z|6BBPfn`?zR?ymX-7fC+hQz@wI2Wva7DxtRou ztD;G96T~bW>H)C#-rLGi*vIik>$e?Xw*^0gq2=_Z zlU6wfH=EHJiWiX1gp8@Fa9f#MgE(QsojMbNAzDa!kGNNBG%e8FDz;j1Jw1^!lsy`q=aw<3Ud=&bBkJR)N8a7Fmy zWxM7t_Zz=p-s%YI@TV><&1G5@?jF_s^e3>jC64;=FdJ|nW-^8!X8P_%ppO@93o$}> zJDlNXLh5#jslM#kr5cP3qF-GtXG)BO9dE^v>NeU2T*wwFJIc^#fdYsU2~Q4>ZoGcv zr&B6B#ra)){yUbQ1`*NLe-x?4Ma_ox4*R!eN8MIB|NVDfTmZDZ=Ln{3<31&&{ynW6m}jAoqQxG+Kq9N$ps}v^tdY_9gY=zV@Ixhw2Y*QvAW%V? z%EMXH?^juG*&Teq6Bx)ZGFakg1sr8QUb=f4@DO4ecmnC1&$8-&1;s^kn<_wJ_1&(oTvEP*SimY;Q5t|8XdSu5j29;iw3W2U<8myU+hF z@;WEvgI7fHmz?zX)^*EkA-*4VK;^6&cCr2lQfE%DpS{abimW-(%n?ba-;MmG%?6#8 z)`A2dfoe-8ODz8z!NAc}X1QC8ybiFs9XKK7|9;(%1b4KM6kv(#xw|(0qc-giQsQWE z`qED>bE1lahPl00-LIU=HwgiqsU%q8^PdW=1&2ltf=@24aOeB|yOFs9cE((vjz)eA>e3${puGuxvpZJ(JVfI(EVT1M$)lU;qFB literal 0 HcmV?d00001 diff --git a/eventbridge-apidestination-agentcore-cdk/architecture.png b/eventbridge-apidestination-agentcore-cdk/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..1dc63d8ee7150fa5a96f7d13c1d01972fd79d07c GIT binary patch literal 48598 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

  • 94O|XyEo`U(^v4|`TuuA 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 literal 0 HcmV?d00001 From 6e75f4443e6c5c67f19530b9b509f1e9c902d1ac Mon Sep 17 00:00:00 2001 From: Antoine Boucherie Date: Fri, 14 Aug 2026 21:32:33 +0800 Subject: [PATCH 6/6] Rewrite Why Gateway section: higher-level, 7 crisp benefits, no specific latency number --- agentcore-gateway-eventbridge-cdk/README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/agentcore-gateway-eventbridge-cdk/README.md b/agentcore-gateway-eventbridge-cdk/README.md index fb0e3ab305..a875c6a1ed 100644 --- a/agentcore-gateway-eventbridge-cdk/README.md +++ b/agentcore-gateway-eventbridge-cdk/README.md @@ -25,20 +25,17 @@ Important: this application uses various AWS services and there are costs associ ## Why route through AgentCore Gateway instead of calling EventBridge directly? -In a mesh of tens or hundreds of agents, Gateway acts as a governed chokepoint between agent reasoning and infrastructure side effects. Three properties make it worth the extra hop: +In a mesh of many agents, Gateway is a governed chokepoint between agent reasoning and infrastructure side effects: -- **Policy-based access control** — Cedar policies attached to the Gateway evaluate every tool call against its input before it reaches the Lambda target. You can restrict which agents emit which event types, or block specific `detail_type` values during a production freeze, without redeploying any agent. -- **Per-caller rate limiting** — set different throughput budgets per agent identity so a misbehaving agent can't flood the bus while high-priority agents keep running. -- **Single IAM blast radius** — only the Gateway's Lambda backend holds `events:PutEvents`. Agents authenticate with scoped, revocable credentials (JWT or IAM/SigV4), and the Gateway centrally logs every tool invocation for audit. +- **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 roughly 200ms of added latency per emission — negligible for asynchronous event-driven workflows, and often more than offset by removing per-agent IAM churn as the mesh grows. - -| Concern | Direct SDK | Via AgentCore Gateway | -|---------|-----------|----------------------| -| Governance | Agent can emit any event schema | Gateway tool definition constrains allowed schemas | -| Observability | Must instrument yourself | Gateway logs every tool invocation automatically | -| Schema evolution | Redeploy agent container to change | Update Gateway tool definition only | -| Multi-agent consistency | Each agent implements PutEvents differently | All agents use the same governed tool | +The tradeoff is added network latency per emission, which is generally negligible for asynchronous event-driven workflows. ## How it works