diff --git a/.gitignore b/.gitignore index f1e3d7422..d335f4e71 100644 --- a/.gitignore +++ b/.gitignore @@ -237,4 +237,6 @@ terraform.rc *.idea # This file gets generated as part of Maven shade plugin which can be ignored -dependency-reduced-pom.xml \ No newline at end of file +dependency-reduced-pom.xml + +.history diff --git a/cloudfront-agentcore-runtime-cdk/.gitignore b/cloudfront-agentcore-runtime-cdk/.gitignore new file mode 100644 index 000000000..3a13cdab5 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info +*.pyc +cdk.out +.DS_Store +.bedrock_agentcore diff --git a/cloudfront-agentcore-runtime-cdk/README.md b/cloudfront-agentcore-runtime-cdk/README.md new file mode 100644 index 000000000..ad21b2c31 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/README.md @@ -0,0 +1,157 @@ +# CloudFront to Amazon Bedrock AgentCore Runtime + +This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three [AgentCore Runtime service contracts](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html): A2A, HTTP, and MCP protocols. + +Benefits of using CloudFront in front of AgentCore Runtime: +- Global edge caching and low-latency access +- DDoS protection via AWS Shield Standard (included) +- Optional AWS WAF integration for IP rate limiting, geo-blocking, and bot protection +- Custom domain support with SSL/TLS certificates +- Request/response transformation via Amazon CloudFront Functions +- Centralized access logging and monitoring + +Learn more about this pattern at [Serverless Land Patterns](https://serverlessland.com/patterns/cloudfront-agentcore-runtime-cdk) + +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 for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Pre-requisites + +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed and configured +- [Docker](https://docs.docker.com/get-docker/) installed and running +- [Python 3.12](https://www.python.org/downloads/) installed + +## Deployment Instructions + +1. Clone the GitHub repository: + + ```shell + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/cloudfront-agentcore-runtime-cdk + ``` + +2. Create and activate a Python virtual environment: + + ```shell + python3 -m venv .venv + source .venv/bin/activate + ``` + +3. Install the required dependencies: + + ```shell + pip3 install -r requirements.txt + ``` + +4. Deploy the stacks: + + ```shell + cdk deploy --all + ``` + +5. After `cdk deploy --all` completes, verify the deployment: + 1. Check the CDK outputs for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl`. + 2. Verify the CloudFront distribution is deployed: + ```shell + aws cloudfront list-distributions --query "DistributionList.Items[*].[Id,Status,DomainName]" --output table + ``` + 3. Confirm the distribution status shows "Deployed". + +## How it works + +This pattern creates: + +1. **Amazon Bedrock AgentCore Runtimes** - Three agents supporting A2A, HTTP, and MCP protocols +2. **Amazon Cognito User Pool** for JWT authentication +3. **CloudFront Distribution** that proxies requests to AgentCore Runtimes: + - `/a2a/*` - A2A protocol agent + - `/rest/*` - HTTP protocol agent + - `/mcp/*` - MCP protocol agent + +**Architecture Flow:** +1. Client sends request to CloudFront with Bearer token +2. CloudFront Function strips path prefix (/a2a, /rest, /mcp) +3. Request is forwarded to the appropriate AgentCore Runtime +4. AgentCore validates JWT token +5. Response is returned through the same path + +## Testing + +1. Get Pool ID, Client ID, and CloudFront URL from the CDK outputs after running the `cdk deploy` command. Look for `UserPoolId`, `UserPoolClientId`, and `DistributionUrl` in the outputs. + +2. Get a bearer token: + + ```shell + cd test + ./get_token.sh + ``` + + Example output: + ``` + Cognito Token Generator + Get Pool ID and Client ID from CDK stack outputs + + Pool ID []: us-west-2_xxxxxx + Client ID []: xxxxxxxxxxxxxxxxxx + Username []: testuser + Password: + Region [us-west-2]: + export BEARER_TOKEN="eyJraWQiOi..." + ``` + +3. Set environment variables (get DistributionUrl from AgentcoreCloudFrontStack outputs): + + ```shell + export CF_URL="https://.cloudfront.net" + export BEARER_TOKEN="" + ``` + +4. Test A2A protocol: + + ```shell + python test_a2a.py + ``` + + You can also use tools like [A2A Inspector](https://a2a-inspector.vercel.app/) with the agent card URL: + ``` + https://.cloudfront.net/a2a/ + ``` + +5. Test HTTP protocol: + + ```shell + python test_http.py + ``` + +6. Test MCP protocol (requires `mcp` package): + + ```shell + python test_mcp.py + ``` + +## Optional: Update A2A Agent Card URL + +If you want A2A clients to discover your agent via CloudFront instead of the direct AgentCore Runtime URL, run this script after deployment: + +```shell +cd .. +./scripts/update_a2a_cloudfront_url.sh +``` + +This configures the A2A agent to advertise the CloudFront URL in its agent card (`/.well-known/agent-card.json`). This is required when using A2A-compatible clients that rely on the agent card for endpoint discovery. + +## Cleanup + +Delete the stacks: + +```bash +cdk destroy --all +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/Dockerfile new file mode 100644 index 000000000..ee192fa32 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/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 9000 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:9000/ping || exit 1 + +CMD ["python", "agent.py"] diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py new file mode 100644 index 000000000..bc46952d9 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/agent.py @@ -0,0 +1,44 @@ +import logging +import os +from strands import Agent +from strands.multiagent.a2a import A2AServer +from a2a.types import AgentSkill +import uvicorn +from fastapi import FastAPI + +logging.basicConfig(level=logging.INFO) + +runtime_url = os.environ.get('CLOUDFRONT_URL', os.environ.get('AGENTCORE_RUNTIME_URL', 'http://127.0.0.1:9000/')) + +strands_agent = Agent( + name="Test Agent", + description="A helpful assistant that answers questions clearly and concisely.", + callback_handler=None +) + +a2a_server = A2AServer( + agent=strands_agent, + http_url=runtime_url, + serve_at_root=True, + enable_a2a_compliant_streaming=True, + skills=[ + AgentSkill( + id="general-assistant", + name="General Assistant", + description="Answers questions and provides helpful information", + tags=["assistant", "qa"], + examples=["What is the capital of France?"] + ) + ] +) + +a2a_app = a2a_server.to_fastapi_app() + +@a2a_app.get("/ping") +def ping(): + return {"status": "healthy"} + +app = a2a_app + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=9000) diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt new file mode 100644 index 000000000..1e4729f37 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/a2a/requirements.txt @@ -0,0 +1,4 @@ +strands-agents[a2a] +bedrock-agentcore +uvicorn +fastapi diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/http/Dockerfile new file mode 100644 index 000000000..acc74f75f --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/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/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py new file mode 100644 index 000000000..54406e73f --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/agent.py @@ -0,0 +1,17 @@ +from strands import Agent +from bedrock_agentcore import BedrockAgentCoreApp + +app = BedrockAgentCoreApp() +agent = Agent() + +@app.entrypoint +async def agent_invocation(payload): + user_message = payload.get( + "prompt", "No prompt found in input, please provide a json payload with prompt key" + ) + stream = agent.stream_async(user_message) + async for event in stream: + yield event + +if __name__ == "__main__": + app.run() diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt new file mode 100644 index 000000000..2bdadd46e --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/http/requirements.txt @@ -0,0 +1,2 @@ +strands-agents +bedrock-agentcore diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/Dockerfile new file mode 100644 index 000000000..e1dcaba31 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/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 8000 + +COPY . . + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/mcp || exit 1 + +CMD ["python", "agent.py"] diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py new file mode 100644 index 000000000..ef495b3f8 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/agent.py @@ -0,0 +1,18 @@ +from mcp.server.fastmcp import FastMCP + +mcp = FastMCP(host="0.0.0.0", stateless_http=True) + +@mcp.tool() +def add_numbers(a: int, b: int) -> int: + return a + b + +@mcp.tool() +def multiply_numbers(a: int, b: int) -> int: + return a * b + +@mcp.tool() +def greet_user(name: str) -> str: + return f"Hello, {name}!" + +if __name__ == "__main__": + mcp.run(transport="streamable-http") diff --git a/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt new file mode 100644 index 000000000..6664c6da9 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/agent-code/mcp/requirements.txt @@ -0,0 +1 @@ +mcp diff --git a/cloudfront-agentcore-runtime-cdk/app.py b/cloudfront-agentcore-runtime-cdk/app.py new file mode 100644 index 000000000..c795058b1 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/app.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import aws_cdk as cdk +import os + +from infra.agentcore_stack import AgentcoreStack +from infra.cloudfront_stack import CloudFrontStack + +app = cdk.App() + +region = os.environ.get("CDK_DEFAULT_REGION") +if not region: + raise ValueError("CDK_DEFAULT_REGION environment variable must be set. Run: export CDK_DEFAULT_REGION=") + +agentcore_stack = AgentcoreStack(app, "AgentCoreAgentsStack", + env=cdk.Environment(region=region), + cross_region_references=True +) + +cloudfront_stack = CloudFrontStack(app, "CloudFrontToAgentCoreStack", + env=cdk.Environment(region=region), + cross_region_references=True, + a2a_agent_runtime_arn=agentcore_stack.a2a_agent_runtime_arn, + http_agent_runtime_arn=agentcore_stack.http_agent_runtime_arn, + mcp_agent_runtime_arn=agentcore_stack.mcp_agent_runtime_arn +) +cloudfront_stack.add_dependency(agentcore_stack) + +app.synth() diff --git a/cloudfront-agentcore-runtime-cdk/cdk.json b/cloudfront-agentcore-runtime-cdk/cdk.json new file mode 100644 index 000000000..98290be50 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/cdk.json @@ -0,0 +1,79 @@ +{ + "app": ".venv/bin/python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true + } +} diff --git a/cloudfront-agentcore-runtime-cdk/example-pattern.json b/cloudfront-agentcore-runtime-cdk/example-pattern.json new file mode 100644 index 000000000..e696c1d10 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/example-pattern.json @@ -0,0 +1,81 @@ +{ + "title": "Amazon CloudFront to Amazon Bedrock AgentCore Runtime", + "description": "This pattern demonstrates how to proxy requests to Amazon Bedrock AgentCore Runtime through CloudFront with OAuth 2.0 authentication, supporting all three AgentCore Runtime service contracts: A2A, HTTP, and MCP protocols.", + "language": "Python", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates a CloudFront distribution that proxies requests to three AgentCore Runtimes (A2A, HTTP, MCP protocols).", + "CloudFront Functions strip path prefixes (/a2a, /rest, /mcp) before forwarding to the appropriate AgentCore Runtime.", + "AgentCore validates JWT tokens for OAuth 2.0 authentication.", + "Benefits include: global edge caching, DDoS protection via AWS Shield, optional WAF integration for rate limiting and geo-blocking, custom domain support, and centralized logging." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cloudfront-agentcore-runtime-cdk", + "templateURL": "serverless-patterns/cloudfront-agentcore-runtime-cdk", + "projectFolder": "cloudfront-agentcore-runtime-cdk", + "templateFile": "app.py" + } + }, + "resources": { + "bullets": [ + { + "text": "AgentCore Runtime Service Contracts", + "link": "https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html" + }, + { + "text": "Amazon Bedrock AgentCore Runtime Documentation", + "link": "https://aws.github.io/bedrock-agentcore-starter-toolkit/" + }, + { + "text": "CloudFront Functions Documentation", + "link": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-functions.html" + }, + { + "text": "Using AWS WAF with CloudFront", + "link": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-awswaf.html" + } + ] + }, + "deploy": { + "text": [ + "python3 -m venv .venv", + "source .venv/bin/activate", + "pip3 install -r requirements.txt", + "cdk bootstrap aws:///us-west-2 aws:///us-east-1", + "cdk deploy --all" + ] + }, + "testing": { + "text": [ + "Get a bearer token: cd test && ./get_token.sh", + "Set environment variables: export CF_URL=\"https://<distribution-id>.cloudfront.net\" && export BEARER_TOKEN=\"<token>\"", + "Test A2A protocol: python test_a2a.py", + "Test HTTP protocol: python test_http.py", + "Test MCP protocol: pip install mcp && python test_mcp.py" + ] + }, + "cleanup": { + "text": [ + "Delete the stacks: cdk destroy --all" + ] + }, + "authors": [ + { + "name": "Rakshith Rao", + "image": "https://serverlessland.com/assets/images/resources/contributors/rakshith-rao.png", + "bio": "I am a Senior Solutions Architect at AWS and help our strategic customers build and operate their key workloads on AWS.", + "linkedin": "rakshithrao" + }, + { + "name": "Biswanath Mukherjee", + "image": "https://serverlessland.com/assets/images/resources/contributors/biswanath-mukherjee.jpg", + "bio": "I am a Sr. Solutions Architect working at AWS India.", + "linkedin": "biswanathmukherjee" + } + ] +} diff --git a/cloudfront-agentcore-runtime-cdk/images/architecture.png b/cloudfront-agentcore-runtime-cdk/images/architecture.png new file mode 100644 index 000000000..d5a48fc73 Binary files /dev/null and b/cloudfront-agentcore-runtime-cdk/images/architecture.png differ diff --git a/cloudfront-agentcore-runtime-cdk/infra/__init__.py b/cloudfront-agentcore-runtime-cdk/infra/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py new file mode 100644 index 000000000..56d8f1a66 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/infra/agentcore_stack.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +from aws_cdk import ( + Stack, + CfnOutput, + RemovalPolicy, + aws_iam as iam, + aws_ecr_assets as ecr_assets, + aws_bedrockagentcore as bedrockagentcore, + aws_cognito as cognito, +) +from constructs import Construct + + +class AgentcoreStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + user_pool = cognito.UserPool(self, "UserPool", + user_pool_name=f"{self.stack_name}-user-pool", + self_sign_up_enabled=False, + sign_in_aliases=cognito.SignInAliases(username=True), + password_policy=cognito.PasswordPolicy(min_length=8), + removal_policy=RemovalPolicy.DESTROY + ) + + user_pool_client = cognito.UserPoolClient(self, "UserPoolClient", + user_pool=user_pool, + user_pool_client_name=f"{self.stack_name}-client", + generate_secret=False, + auth_flows=cognito.AuthFlow(user_password=True, custom=True) + ) + + a2a_docker_image = ecr_assets.DockerImageAsset(self, "A2aAgentImage", + directory="./agent-code/a2a", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + http_docker_image = ecr_assets.DockerImageAsset(self, "HttpAgentImage", + directory="./agent-code/http", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + mcp_docker_image = ecr_assets.DockerImageAsset(self, "McpAgentImage", + directory="./agent-code/mcp", + platform=ecr_assets.Platform.LINUX_ARM64 + ) + + a2a_agent_name = f"{self.stack_name.replace('-', '_')}_A2a_Agent" + + agent_role = iam.Role(self, "AgentCoreRole", + assumed_by=iam.ServicePrincipal("bedrock-agentcore.amazonaws.com", + conditions={ + "StringEquals": {"aws:SourceAccount": self.account}, + "ArnLike": {"aws:SourceArn": f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:*"} + } + ), + inline_policies={ + "AgentCorePolicy": iam.PolicyDocument(statements=[ + iam.PolicyStatement( + sid="ECRImageAccess", + actions=["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"], + resources=[a2a_docker_image.repository.repository_arn, http_docker_image.repository.repository_arn, mcp_docker_image.repository.repository_arn] + ), + iam.PolicyStatement( + sid="ECRTokenAccess", + actions=["ecr:GetAuthorizationToken"], + resources=["*"] # ecr:GetAuthorizationToken does not support resource-level permissions, see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelasticcontainerregistry.html + ), + iam.PolicyStatement( + actions=["logs:DescribeLogStreams", "logs:CreateLogGroup"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/bedrock-agentcore/runtimes/*"] + ), + iam.PolicyStatement( + actions=["logs:DescribeLogGroups"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:*"] + ), + iam.PolicyStatement( + actions=["logs:CreateLogStream", "logs:PutLogEvents"], + resources=[f"arn:aws:logs:{self.region}:{self.account}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*"] + ), + iam.PolicyStatement( + actions=["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"], + resources=["*"] # X-Ray actions do not support resource-level permissions, see https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsx-ray.html + ), + iam.PolicyStatement( + actions=["cloudwatch:PutMetricData"], + resources=["*"], # cloudwatch:PutMetricData does not support resource-level permissions, scoped via condition key; see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazoncloudwatch.html + conditions={"StringEquals": {"cloudwatch:namespace": "bedrock-agentcore"}} + ), + iam.PolicyStatement( + sid="GetAgentAccessToken", + actions=["bedrock-agentcore:GetWorkloadAccessToken", "bedrock-agentcore:GetWorkloadAccessTokenForJWT", "bedrock-agentcore:GetWorkloadAccessTokenForUserId"], + resources=[ + f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default", + f"arn:aws:bedrock-agentcore:{self.region}:{self.account}:workload-identity-directory/default/workload-identity/{a2a_agent_name}-*" + ] + ), + iam.PolicyStatement( + sid="BedrockModelInvocation", + actions=["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], + resources=["arn:aws:bedrock:*::foundation-model/*", f"arn:aws:bedrock:{self.region}:{self.account}:*"] + ) + ]) + } + ) + + discovery_url = f"https://cognito-idp.{self.region}.amazonaws.com/{user_pool.user_pool_id}/.well-known/openid-configuration" + + a2a_agent_runtime = bedrockagentcore.CfnRuntime(self, "AgentRuntime", + agent_runtime_name=a2a_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=a2a_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="A2A", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + http_agent_name = f"{self.stack_name.replace('-', '_')}_Http_Agent" + http_agent_runtime = bedrockagentcore.CfnRuntime(self, "HttpAgentRuntime", + agent_runtime_name=http_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=http_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="HTTP", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + mcp_agent_name = f"{self.stack_name.replace('-', '_')}_Mcp_Agent" + mcp_agent_runtime = bedrockagentcore.CfnRuntime(self, "McpAgentRuntime", + agent_runtime_name=mcp_agent_name, + agent_runtime_artifact=bedrockagentcore.CfnRuntime.AgentRuntimeArtifactProperty( + container_configuration=bedrockagentcore.CfnRuntime.ContainerConfigurationProperty( + container_uri=mcp_docker_image.image_uri + ) + ), + network_configuration=bedrockagentcore.CfnRuntime.NetworkConfigurationProperty(network_mode="PUBLIC"), + protocol_configuration="MCP", + role_arn=agent_role.role_arn, + environment_variables={"AWS_DEFAULT_REGION": self.region}, + authorizer_configuration=bedrockagentcore.CfnRuntime.AuthorizerConfigurationProperty( + custom_jwt_authorizer=bedrockagentcore.CfnRuntime.CustomJWTAuthorizerConfigurationProperty( + discovery_url=discovery_url, + allowed_clients=[user_pool_client.user_pool_client_id] + ) + ) + ) + + self.a2a_agent_runtime_arn = a2a_agent_runtime.attr_agent_runtime_arn + self.http_agent_runtime_arn = http_agent_runtime.attr_agent_runtime_arn + self.mcp_agent_runtime_arn = mcp_agent_runtime.attr_agent_runtime_arn + + CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) + CfnOutput(self, "UserPoolClientId", value=user_pool_client.user_pool_client_id) diff --git a/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py new file mode 100644 index 000000000..b2b182a7b --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/infra/cloudfront_stack.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +from aws_cdk import ( + Stack, + CfnOutput, + Fn, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, +) +from constructs import Construct + + +class CloudFrontStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, a2a_agent_runtime_arn: str, http_agent_runtime_arn: str, mcp_agent_runtime_arn: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + a2a_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", a2a_agent_runtime_arn)))) + a2a_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", a2a_encoded_arn)))) + + http_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", http_agent_runtime_arn)))) + http_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", http_encoded_arn)))) + + mcp_encoded_arn = Fn.join("", Fn.split(":", Fn.join("%3A", Fn.split(":", mcp_agent_runtime_arn)))) + mcp_encoded_arn = Fn.join("", Fn.split("/", Fn.join("%2F", Fn.split("/", mcp_encoded_arn)))) + + a2a_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{self.region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", a2a_encoded_arn, "/invocations"]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + http_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{self.region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", http_encoded_arn]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + mcp_agentcore_origin = origins.HttpOrigin( + f"bedrock-agentcore.{self.region}.amazonaws.com", + origin_path=Fn.join("", ["/runtimes/", mcp_encoded_arn]), + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY + ) + + strip_a2a_prefix_fn = cloudfront.Function(self, "StripA2aPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/a2a/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + strip_http_prefix_fn = cloudfront.Function(self, "StripHttpPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/rest/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + strip_mcp_prefix_fn = cloudfront.Function(self, "StripMcpPrefixFunction", + code=cloudfront.FunctionCode.from_inline(""" +function handler(event) { + var request = event.request; + request.uri = request.uri.replace(/^\\/mcp/, ''); + if (request.uri === '') request.uri = '/'; + return request; +} +"""), + runtime=cloudfront.FunctionRuntime.JS_2_0 + ) + + dummy_origin = origins.HttpOrigin("aws.amazon.com") + + distribution = cloudfront.Distribution(self, "Distribution", + default_behavior=cloudfront.BehaviorOptions( + origin=dummy_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED + ), + additional_behaviors={ + "/a2a/*": cloudfront.BehaviorOptions( + origin=a2a_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_a2a_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ), + "/rest/*": cloudfront.BehaviorOptions( + origin=http_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_http_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ), + "/mcp/*": cloudfront.BehaviorOptions( + origin=mcp_agentcore_origin, + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + function_associations=[ + cloudfront.FunctionAssociation( + function=strip_mcp_prefix_fn, + event_type=cloudfront.FunctionEventType.VIEWER_REQUEST + ) + ] + ) + }, + price_class=cloudfront.PriceClass.PRICE_CLASS_100 + ) + + CfnOutput(self, "DistributionUrl", value=f"https://{distribution.distribution_domain_name}") diff --git a/cloudfront-agentcore-runtime-cdk/requirements.txt b/cloudfront-agentcore-runtime-cdk/requirements.txt new file mode 100644 index 000000000..1db8dd37f --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/requirements.txt @@ -0,0 +1,4 @@ +aws-cdk-lib>=2.220.0 +constructs>=10.0.0,<11.0.0 +requests>=2.25.0 +mcp>=1.0.0 diff --git a/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh b/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh new file mode 100755 index 000000000..d6cd5cbdd --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/scripts/update_a2a_cloudfront_url.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -e + +REGION=${AWS_DEFAULT_REGION:-us-west-2} + +echo "Fetching CloudFront distribution..." +DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[*].[Id, Origins.Items[0].DomainName] | [?contains([1], 'bedrock-agentcore')] | [0][0]" --output text --no-cli-pager | grep -v "^None$" | head -1) +if [ -z "$DISTRIBUTION_ID" ]; then + echo "Error: Could not find CloudFront distribution for AgentCore" + exit 1 +fi +CF_DOMAIN=$(aws cloudfront get-distribution --id $DISTRIBUTION_ID --query "Distribution.DomainName" --output text --no-cli-pager) +CF_URL="https://${CF_DOMAIN}" + +echo "Fetching A2A agent runtime..." +RUNTIME_ID=$(aws bedrock-agentcore-control list-agent-runtimes --region $REGION --query "agentRuntimes[?contains(agentRuntimeName, 'A2a_Agent')].agentRuntimeId" --output text --no-cli-pager | grep -v "^None$" | head -1) +if [ -z "$RUNTIME_ID" ]; then + echo "Error: Could not find A2A agent runtime" + exit 1 +fi + +echo "Found runtime: $RUNTIME_ID" + +RUNTIME_INFO=$(aws bedrock-agentcore-control get-agent-runtime --agent-runtime-id $RUNTIME_ID --region $REGION --no-cli-pager) +CONTAINER_URI=$(echo $RUNTIME_INFO | jq -r '.agentRuntimeArtifact.containerConfiguration.containerUri') +ROLE_ARN=$(echo $RUNTIME_INFO | jq -r '.roleArn') +DISCOVERY_URL=$(echo $RUNTIME_INFO | jq -r '.authorizerConfiguration.customJWTAuthorizer.discoveryUrl') +CLIENT_ID=$(echo $RUNTIME_INFO | jq -r '.authorizerConfiguration.customJWTAuthorizer.allowedClients[0]') + +echo "Updating A2A agent with CloudFront URL: ${CF_URL}/a2a/" + +aws bedrock-agentcore-control update-agent-runtime \ + --agent-runtime-id $RUNTIME_ID \ + --agent-runtime-artifact containerConfiguration={containerUri=$CONTAINER_URI} \ + --role-arn $ROLE_ARN \ + --network-configuration networkMode=PUBLIC \ + --protocol-configuration serverProtocol=A2A \ + --authorizer-configuration "customJWTAuthorizer={discoveryUrl=$DISCOVERY_URL,allowedClients=$CLIENT_ID}" \ + --environment-variables "AWS_DEFAULT_REGION=$REGION,CLOUDFRONT_URL=${CF_URL}/a2a/" \ + --region $REGION \ + --no-cli-pager + +echo "Waiting for runtime to be ready..." +while true; do + STATUS=$(aws bedrock-agentcore-control get-agent-runtime --agent-runtime-id $RUNTIME_ID --region $REGION --query "status" --output text --no-cli-pager) + if [ "$STATUS" == "READY" ]; then + echo "Runtime is ready." + break + fi + echo "Status: $STATUS. Waiting..." + sleep 5 +done + +echo "Done." diff --git a/cloudfront-agentcore-runtime-cdk/test/.cognito_config b/cloudfront-agentcore-runtime-cdk/test/.cognito_config new file mode 100644 index 000000000..c1cdf26a2 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/.cognito_config @@ -0,0 +1,4 @@ +POOL_ID= +CLIENT_ID= +USERNAME=test +REGION=us-west-2 diff --git a/cloudfront-agentcore-runtime-cdk/test/get_token.sh b/cloudfront-agentcore-runtime-cdk/test/get_token.sh new file mode 100755 index 000000000..dba90ca18 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/get_token.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +echo "Cognito Token Generator" +echo "Get Pool ID and Client ID from CDK stack outputs" +echo "" + +CONFIG_FILE=".cognito_config" + +if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" +fi + +read -p "Pool ID [$POOL_ID]: " input && POOL_ID=${input:-$POOL_ID} +read -p "Client ID [$CLIENT_ID]: " input && CLIENT_ID=${input:-$CLIENT_ID} +read -p "Username [$USERNAME]: " input && USERNAME=${input:-$USERNAME} +read -sp "Password: " PASSWORD +echo +read -p "Region [${REGION:-us-west-2}]: " input && REGION=${input:-${REGION:-us-west-2}} + +cat > "$CONFIG_FILE" << EOF +POOL_ID=$POOL_ID +CLIENT_ID=$CLIENT_ID +USERNAME=$USERNAME +REGION=$REGION +EOF + +aws cognito-idp admin-create-user \ + --user-pool-id $POOL_ID \ + --username $USERNAME \ + --region $REGION \ + --message-action SUPPRESS > /dev/null 2>&1 || true + +aws cognito-idp admin-set-user-password \ + --user-pool-id $POOL_ID \ + --username $USERNAME \ + --password $PASSWORD \ + --region $REGION \ + --permanent > /dev/null 2>&1 || true + +BEARER_TOKEN=$(aws cognito-idp initiate-auth \ + --client-id "$CLIENT_ID" \ + --auth-flow USER_PASSWORD_AUTH \ + --auth-parameters USERNAME=$USERNAME,PASSWORD=$PASSWORD \ + --region $REGION | jq -r '.AuthenticationResult.AccessToken') + +echo "export BEARER_TOKEN=\"$BEARER_TOKEN\"" diff --git a/cloudfront-agentcore-runtime-cdk/test/test_a2a.py b/cloudfront-agentcore-runtime-cdk/test/test_a2a.py new file mode 100755 index 000000000..91a8197e9 --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_a2a.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import requests +import json +import uuid +import sys + +def debug_response(resp): + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + try: + print(f" [DEBUG] Body: {json.dumps(resp.json(), indent=2)}") + except: + print(f" [DEBUG] Body: {resp.text[:500]}") + +def test_agent_card(base_url, headers): + print("\n[TEST] /.well-known/agent-card.json (GET)") + print(f" [DEBUG] URL: {base_url}/a2a/.well-known/agent-card.json") + resp = requests.get(f"{base_url}/a2a/.well-known/agent-card.json", headers=headers) + debug_response(resp) + assert resp.status_code == 200, f"Agent card failed: {resp.status_code}" + data = resp.json() + required_fields = ["name", "description", "skills"] + for field in required_fields: + assert field in data, f"Missing {field} in agent card" + assert len(data["skills"]) > 0, "Agent must have at least one skill" + print(f" [PASS] Agent '{data['name']}' with {len(data['skills'])} skill(s)") + +def test_message_send(base_url, headers): + print("\n[TEST] / (POST message/send)") + print(f" [DEBUG] URL: {base_url}/a2a/") + payload = { + "jsonrpc": "2.0", + "id": str(uuid.uuid4()), + "method": "message/send", + "params": { + "message": { + "role": "user", + "parts": [{"kind": "text", "text": "Hello, what is 1+1?"}], + "messageId": str(uuid.uuid4()) + } + } + } + print(f" [DEBUG] Request: {json.dumps(payload, indent=2)}") + resp = requests.post(f"{base_url}/a2a/", headers=headers, json=payload) + debug_response(resp) + assert resp.status_code == 200, f"Message send failed: {resp.status_code}" + data = resp.json() + assert "jsonrpc" in data, "Missing jsonrpc in response" + assert data["jsonrpc"] == "2.0", "Invalid jsonrpc version" + assert "id" in data, "Missing id in response" + if "error" in data: + print(f" [WARN] Error response: {data['error']}") + else: + assert "result" in data, "Missing result in response" + print(f" [PASS] Got valid JSON-RPC response") + +def main(): + import os + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_a2a.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + print("=" * 60) + print("A2A Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + try: + test_agent_card(base_url, headers) + test_message_send(base_url, headers) + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except AssertionError as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cloudfront-agentcore-runtime-cdk/test/test_http.py b/cloudfront-agentcore-runtime-cdk/test/test_http.py new file mode 100755 index 000000000..97641d39a --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_http.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import requests +import json +import sys + +def debug_response(resp): + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + try: + print(f" [DEBUG] Body: {resp.text[:500]}") + except: + print(f" [DEBUG] Body: {resp.text}") + +def test_invocations(base_url, headers): + print("\n[TEST] /rest/invocations (POST)") + print(f" [DEBUG] URL: {base_url}/rest/invocations") + payload = {"prompt": "What is 2+2?"} + print(f" [DEBUG] Request: {json.dumps(payload, indent=2)}") + resp = requests.post(f"{base_url}/rest/invocations", headers=headers, json=payload, stream=True) + print(f" [DEBUG] Status: {resp.status_code}") + print(f" [DEBUG] Headers: {dict(resp.headers)}") + assert resp.status_code == 200, f"Invocations failed: {resp.status_code}" + + content_type = resp.headers.get("content-type", "") + if "text/event-stream" in content_type: + print(" [DEBUG] Streaming response (SSE):") + for line in resp.iter_lines(): + if line: + print(f" {line.decode('utf-8')}") + print(f" [PASS] Got streaming response") + else: + print(f" [DEBUG] Body: {resp.text[:500]}") + print(f" [PASS] Got response") + +def main(): + import os + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_http.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + print("=" * 60) + print("HTTP Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + try: + test_invocations(base_url, headers) + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except AssertionError as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + except Exception as e: + print(f"\n[ERROR] {e}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/cloudfront-agentcore-runtime-cdk/test/test_mcp.py b/cloudfront-agentcore-runtime-cdk/test/test_mcp.py new file mode 100755 index 000000000..042c9fe3a --- /dev/null +++ b/cloudfront-agentcore-runtime-cdk/test/test_mcp.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import asyncio +import os +import sys + +from mcp import ClientSession +from mcp.client.streamable_http import streamablehttp_client + +async def main(): + base_url = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else os.environ.get("CF_URL", "").rstrip("/") + token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("BEARER_TOKEN", "") + + if not base_url or not token: + print("Usage: python test_mcp.py ") + print("Or set CF_URL and BEARER_TOKEN environment variables") + sys.exit(1) + + print("=" * 60) + print("MCP Protocol Validation") + print("=" * 60) + print(f"Base URL: {base_url}") + print(f"Token: {token[:20]}...") + + mcp_url = f"{base_url}/mcp/invocations" + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + print(f"\n[TEST] MCP Connection") + print(f" [DEBUG] URL: {mcp_url}") + + try: + async with streamablehttp_client(mcp_url, headers, timeout=120, terminate_on_close=False) as ( + read_stream, + write_stream, + _, + ): + async with ClientSession(read_stream, write_stream) as session: + await session.initialize() + print(" [PASS] MCP session initialized") + + print("\n[TEST] List Tools") + tools = await session.list_tools() + print(f" [PASS] Found {len(tools.tools)} tools:") + for tool in tools.tools: + print(f" - {tool.name}") + + print("\n[TEST] Call add_numbers tool") + result = await session.call_tool("add_numbers", {"a": 5, "b": 3}) + print(f" [PASS] Result: {result.content}") + + print("\n" + "=" * 60) + print("All tests passed!") + print("=" * 60) + except Exception as e: + print(f"\n[FAIL] {e}") + sys.exit(1) + +if __name__ == "__main__": + asyncio.run(main())