Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Production-Grade AI Agent on AWS Bedrock AgentCore

A reference implementation of a LangGraph multi-agent application deployed the way it would actually need to ship: with per-tool authorization, gateway-mediated tool access, short-lived identity, full tracing, cost attribution, and network isolation.

The application itself is deliberately simple. It's a research assistant that takes a question, runs a few web searches through a tool, and summarizes the result. The point isn't the app. The point is everything around it: the architecture that turns an agent from a script into something you could reasonably run in front of real users.

A note before you read on: for a single tool like this, the architecture here is overkill, and a plain Lambda would do the job with far less effort. It starts to earn its place once you have many tools, multiple tenants, and a real blast radius to worry about. See When not to use this.


The authorization layer, in two traces

The clearest way to see what the Cedar policy actually does is to run the same query twice and change only one thing: whether the policy permits the tool.

Policy in place — the tool runs

With the Cedar policy attached, the agent's tool call is authorized at the gateway, the search tool fires three times, and the summarizer gets real results to work with. Note tool_call_count: 3 and the populated research findings.

Allowed run: the tool is authorized and fires, tool_call_count is 3

Policy removed — the call never executes

With the policy gone, the same query runs, but the tool call is never authorized. There is no research_tools step in the graph, tool_call_count stays at 0, and the summarizer is left honestly reporting that it has the search queries but none of the results.

Denied run: no tool call, tool_call_count is 0, summarizer has no results

That difference is the whole argument: authorization here isn't decorative. Remove it and the agent simply can't act.


Architecture

Architecture diagram

A single request travels through the system like this:

  1. User → Streamlit UI → API Gateway (REST). The front end calls a REST endpoint. The agent is never exposed directly to the browser.
  2. API Gateway → invoke Lambda (in-VPC). A thin Lambda inside the VPC receives the request and invokes the agent runtime. (AgentCore Runtime isn't triggered directly by API Gateway, so this Lambda is the entry point into the VPC.)
  3. invoke Lambda → AgentCore Runtime. The LangGraph multi-agent graph runs as an ARM container on Bedrock AgentCore Runtime, in a private subnet.
  4. Runtime → Cognito. Before calling any tool, the runtime exchanges a Cognito client id and secret for a short-lived JWT.
  5. Runtime → AgentCore Gateway. The agent calls the gateway (never the tool directly), presenting the JWT. The gateway validates the token, then evaluates the Cedar policy for the requested tool.
  6. Gateway → Tool Lambda. If the policy permits it, the gateway invokes the tool Lambda through an IAM role.
  7. Tool Lambda → Serper.dev → Google. The tool performs the actual web search and returns results back up the chain.

Outbound access from the private subnet goes through VPC interface endpoints (PrivateLink) for AWS services, with a NAT gateway only for controlled egress. Throughout the run, traces are sent to Langfuse, where each LLM call carries its tokens and dollar cost.


What this demonstrates

Six concerns a notebook never forces you to answer, and the approach taken for each:

# Concern Approach
1 Stopping the agent from calling a tool it shouldn't Per-tool authorization with Cedar policies, evaluated at the AgentCore Gateway. The agent is the principal, the tool is the action, the gateway is the resource. No matching policy means no call.
2 Reaching a tool safely All tools sit behind the AgentCore Gateway. The agent calls the gateway, which authenticates and authorizes before invoking the tool Lambda via IAM.
3 Authenticating without long-lived secrets A short-lived Cognito JWT, fetched per request and validated at the gateway before any tool logic runs.
4 Knowing what the agent did Full tracing with Langfuse: every hop, from supervisor to researcher to tool call to summarizer, as one tree.
5 Knowing what a query costs Cost attributed per LLM call in the trace. One research query is roughly $0.0364 end to end, broken down by step.
6 Where it runs, isolated AgentCore Runtime as an ARM container in private subnets, reaching AWS over VPC endpoints, with NAT for controlled egress and least-privilege IAM.

The Cedar policy

This is the core of the authorization model. The policy permits one specific agent identity to invoke one specific tool, on one specific gateway. Everything not explicitly permitted is denied by default.

permit (
  principal == AgentCore::OAuthUser::"<agent-client-id>",
  action   == AgentCore::Action::"search-tool___serper_search",
  resource == AgentCore::Gateway::"arn:aws:bedrock-agentcore:
              <region>:<account-id>:gateway/<gateway-name>"
);

Removing this policy is exactly what the denied-run trace above shows: the agent still tries to call the tool, but with nothing permitting it, the call never executes.


Repository layout

.
├── backend/                  # LangGraph multi-agent app (supervisor, researcher, summarizer)
│   ├── ...                    #   runs as the AgentCore Runtime container
│   └── Dockerfile             #   ARM image, pushed to ECR
├── frontend/                 # Streamlit UI
├── lambdas/
│   ├── invoke_runtime/        # In-VPC Lambda behind API Gateway; invokes AgentCore Runtime
│   └── serper_search/         # Tool Lambda the gateway calls (Serper.dev search)
├── policies/
│   └── search_app.cedar       # The Cedar authorization policy
├── docs/
│   ├── architecture.png       # Architecture diagram
│   ├── trace-allowed.png      # Langfuse trace: policy allowed, tool runs
│   └── trace-denied.png       # Langfuse trace: policy removed, no tool call
├── requirements.txt
└── README.md

Stack

  • Orchestration: LangGraph (multi-agent: supervisor, researcher, summarizer)
  • Model: Amazon Bedrock (ChatBedrockConverse)
  • Agent hosting: Amazon Bedrock AgentCore Runtime (ARM container, image on ECR)
  • Tool gateway + authorization: Amazon Bedrock AgentCore Gateway with Cedar policies
  • Identity: Amazon Cognito (OAuth client credentials, JWT)
  • Tool: AWS Lambda calling Serper.dev
  • Entry point: Amazon API Gateway (REST) → in-VPC invoke Lambda
  • Frontend: Streamlit
  • Observability: Langfuse
  • Networking: VPC, private subnets, VPC interface endpoints (PrivateLink), NAT gateway, least-privilege IAM

Prerequisites

  • An AWS account with access to Amazon Bedrock and Bedrock AgentCore in your region
  • Bedrock model access enabled for the model you intend to use
  • A Cognito user pool and an app client (client credentials flow)
  • A Serper.dev API key for the search tool
  • A Langfuse project (public and secret keys)
  • Docker with buildx (for building the ARM container image)
  • AWS CLI configured, plus whatever IaC you prefer (CDK / Terraform / SAM)
  • Python 3.11+

Setup

These steps are a skeleton. Adjust paths, names, and commands to match your own infrastructure-as-code setup.

  1. Deploy the tool Lambda (lambdas/serper_search) — the serper_search function the gateway will invoke.

  2. Create the AgentCore Gateway and register the tool Lambda as a target.

  3. Configure inbound auth on the gateway to use your Cognito user pool as the identity provider, and add your app client to the allowed clients.

  4. Attach the Cedar policy (policies/search_app.cedar) scoping the agent identity to the search tool. Substitute your own account id, region, and gateway name (see The Cedar policy).

  5. Build and push the ARM image for the agent runtime (backend/) to ECR.

    cd backend
    docker buildx build --platform linux/arm64 -t <ecr-repo>:latest --push .
  6. Deploy the AgentCore Runtime from that image, into your private subnets, with the VPC endpoints and IAM role attached.

  7. Deploy the invoke Lambda (lambdas/invoke_runtime) and the API Gateway REST entry point in front of it.

  8. Run the Streamlit frontend.

    pip install -r requirements.txt
    streamlit run frontend/app.py
  9. Send a query and watch the trace appear in Langfuse.


Reproducing the allowed-vs-denied demo

This is what the two trace screenshots above show, and it's the most convincing thing to try yourself:

  1. Send a query with the Cedar policy attached. The tool fires; tool_call_count is greater than 0; you get a real answer.
  2. Remove (or detach) the Cedar policy for the tool.
  3. Send the same query again. The tool call is no longer authorized; tool_call_count is 0; the summarizer reports it has the queries but no results.
  4. Re-attach the policy to restore the working behavior.

Observability: why Langfuse

I started with ADOT (the AWS Distro for OpenTelemetry). For general infrastructure telemetry it's a reasonable choice, but for agent workflows specifically I found Langfuse gave me far more useful signal for far less wiring. Because it's built around LLM calls, the whole multi-agent run shows up as one readable tree, and each model call carries its tokens and its dollar cost without any extra instrumentation. That's where the per-query cost figure comes from.

If your needs are different (for example, you're standardizing on OpenTelemetry across a wider platform), ADOT may still be the right call. This is a preference for this use case, not a universal recommendation.


When not to use this

This is the honest part, and it's worth stating plainly.

For a single tool, this architecture is more than you need. The gateway, the Cedar policies, the JWT exchange, the separate runtime — for one search tool, a plain Lambda would do the same job with a fraction of the moving parts, and it would be easier to reason about.

The reason to reach for something like this is when the cost of getting authorization wrong goes up:

  • Many tools, where "the agent can call anything" stops being acceptable.
  • Multiple tenants or callers, where different identities should have different tool access.
  • A real blast radius, where an agent calling the wrong thing has consequences worth preventing.

At that point, per-tool authorization, gateway-mediated access, and proper isolation stop being overhead and start being the thing that lets you sleep. The goal of this repo is to show what that looks like end to end, so the tradeoff is a deliberate choice rather than a surprise.



License

MIT. See LICENSE.

About

Production-grade AI agent security with AWS Bedrock AgentCore, Cedar authorization, Cognito authentication, LangGraph, and Langfuse observability.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages