references/browser.md
# browser
Add the AgentCore Browser tool so your agent can navigate web pages, fill forms, and extract information.
## When to use
- Your agent needs to interact with a website that has no API
- Your agent needs to fill forms, scrape data, or drive a web app
- You want an isolated, session-scoped browser for the agent (not a shared one)
- You want live-view / recording / replay of what the browser did, for debugging or auditing
Do NOT use this reference for:
- Calling an API — use Gateway (`agents-connect`)
- Running code in a sandbox — see [`code-interpreter.md`](code-interpreter.md)
- Serving browser-based UIs to users — that's a different problem (the AGUI protocol, not the Browser tool)
## Mental model
The Browser tool is a **managed Chrome instance**, one per session, running in an isolated microVM. Your agent connects to it over WebSocket (via CDP — Chrome DevTools Protocol) and drives it with an automation framework. You pick the framework:
| Framework | When to use |
|---|---|
| **Strands `AgentCoreBrowser`** | Agent-driven browsing inside a Strands agent. Highest-level, tool-use-native. |
| **Nova Act** | You want an LLM to decide the next action at each step ("click the search box, type X, press enter"). Best for open-ended tasks. |
| **Playwright** | Deterministic scripted automation. Best when you know the exact steps — login flows, scraping a known page structure. |
If you're adding browsing to a Strands agent, use `AgentCoreBrowser` and skip the framework decision — it wraps Nova Act under the hood and fits the agent-tool mental model.
If you're not using Strands, pick between Nova Act (reasoning-driven) and Playwright (script-driven) based on whether the task is open-ended or well-defined.
Sessions are **ephemeral by default** (reset after each use). Default timeout is 15 minutes, max 8 hours. You can run multiple concurrent sessions.
## Prerequisites
- Python 3.10+
- `bedrock-agentcore` SDK installed
- IAM permissions for `bedrock-agentcore:*Browser*` actions (scope to your browser resource ARN in production)
- AWS region that supports Browser — check the docs for the current list
- For Strands path: model access for your chosen model (Claude Sonnet 4.x is the common default)
- For Nova Act path: a Nova Act API key from [nova.amazon.com/act](https://nova.amazon.com/act) (US-based amazon.com accounts only at time of writing)
IAM policy skeleton (attach to the caller identity — your user, role, or AgentCore Runtime execution role):
```json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "BrowserAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateBrowser",
"bedrock-agentcore:GetBrowser",
"bedrock-agentcore:ListBrowsers",
"bedrock-agentcore:StartBrowserSession",
"bedrock-agentcore:StopBrowserSession",
"bedrock-agentcore:GetBrowserSession",
"bedrock-agentcore:ListBrowserSessions",
"bedrock-agentcore:ConnectBrowserAutomationStream",
"bedrock-agentcore:ConnectBrowserLiveViewStream"
],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:browser/*"
}]
}
```
Check current IAM action names against the docs — the list evolves.
## Path A — Strands agent with the Browser tool (recommended for most)
```python
from strands import Agent
from strands_tools.browser import AgentCoreBrowser
browser_tool = AgentCoreBrowser(region="<REGION>")
agent = Agent(tools=[browser_tool.browser])
result = agent("Find the release date of the latest AgentCore SDK on GitHub.")
print(result.message["content"][0]["text"])
```
Install: `pip install bedrock-agentcore strands-agents strands-agents-tools`
The agent decides when to use the browser, opens sessions on demand, and cleans them up. Under the hood, `AgentCoreBrowser` uses the AWS-managed `aws.browser.v1` resource — no resource creation needed.
**Dropping into an AgentCore Runtime entrypoint:**
```python
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands_tools.browser import AgentCoreBrowser
from model.load import load_model # scaffolded by `agentcore create`
import os
app = BedrockAgentCoreApp()
REGION = os.getenv("AWS_REGION", "us-west-2")
@app.entrypoint
def invoke(payload, context):
browser_tool = AgentCoreBrowser(region=REGION)
agent = Agent(model=load_model(), tools=[browser_tool.browser])
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()
```
## Path B — Nova Act for reasoning-driven tasks
Use when the task needs an LLM to decide each click/type step.
```python
from bedrock_agentcore.tools.browser_client import browser_session
from nova_act import NovaAct
def run_browser_task(prompt: str, starting_page: str, nova_act_key: str, region: str = "us-west-2"):
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
with NovaAct(
cdp_endpoint_url=ws_url,
cdp_headers=headers,
nova_act_api_key=nova_act_key,
starting_page=starting_page,
) as nova:
return nova.act(prompt)
```
Install: `pip install bedrock-agentcore nova-act boto3`
The `browser_session` context manager handles start/stop. Do not leak sessions — always use the context manager or wrap raw `BrowserClient` calls in try/finally.
**Credential handling:** the Nova Act API key is a secret. If this is running inside an AgentCore Runtime agent, register it as a credential (`agentcore add credential --name NovaAct --api-key ...`) and retrieve it with `@requires_api_key(provider_name="NovaAct")`. Do not put it in runtime env vars. See `agents-connect` Path D.
## Path C — Playwright for scripted automation
Use when the steps are fixed and you want deterministic behavior (logins, scrapes, automated tests).
```python
import asyncio
from bedrock_agentcore.tools.browser_client import browser_session
from playwright.async_api import async_playwright
async def scrape_title(url: str, region: str = "us-west-2") -> str:
async with async_playwright() as pw:
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
browser = await pw.chromium.connect_over_cdp(ws_url, headers=headers)
context = browser.contexts[0]
page = context.pages[0]
try:
await page.goto(url)
return await page.title()
finally:
await page.close()
await browser.close()
print(asyncio.run(scrape_title("https://example.com")))
```
Install: `pip install bedrock-agentcore playwright`
Sync variant (`sync_playwright`) is also supported — pick based on whether your agent code is async.
## Observability
Browser is observable by default:
- **Live view** — watch a running session in real time from the AWS console (Built-in tools → Browser → your session → "View live session"). You can take over control from the automation interactively.
- **CloudWatch logs** — `/aws/bedrock-agentcore/browser/*`
- **Metrics** — in `AWS/BedrockAgentCore` namespace
**Session recording** (DOM, clicks, console logs, network) is opt-in per browser. To enable:
1. Create a **custom browser** (not `aws.browser.v1`) with recording configured
2. Give its execution role `s3:PutObject` on your recording bucket
3. Recordings land in your S3 bucket and replay in the AWS console
The managed `aws.browser.v1` resource does **not** record. Use custom browsers when you need audit trails.
## Session lifecycle — always close
```python
# Right — context manager
with browser_session(region) as client:
ws_url, headers = client.generate_ws_headers()
...
# Also right — explicit try/finally
client = BrowserClient(region=region)
client.start()
try:
...
finally:
client.stop()
# Wrong — leaked session
client = BrowserClient(region=region)
client.start()
... # if this raises, the session sits idle until its 15-minute timeout
```
Sessions hold a microVM. Leaked sessions cost money until they time out. The context manager is non-negotiable for production.
## VPC mode
If your agent runs in VPC mode, the Browser tool can also run in VPC. See [`vpc.md`](vpc.md) for the subnet + security group pattern (the same service-linked role covers Browser ENIs). Browser in VPC requires a NAT gateway for public-internet sites — public subnets don't give Browser internet access.
## Common failures
**"Access denied" starting a session:** IAM is missing `StartBrowserSession` on the browser resource ARN. Check `aws sts get-caller-identity` matches the identity you attached the policy to.
**"Model access denied" from a Strands agent:** The browser tool itself is fine, but the agent's model isn't enabled. Go to Bedrock console → Model access → enable your model in the region.
**Nova Act errors about API key:** The key is US-amazon.com-accounts only at launch. If you're outside the US or using a work account, you can't use Nova Act yet — fall back to Playwright or Strands.
**Browser session times out mid-task:** Default is 15 minutes of idle time. Pass `sessionTimeoutSeconds` to `StartBrowserSession` (max 28800 = 8 hours). Don't use this to cover up agents that are slow — fix the agent or chunk the work.
**Live view doesn't show your session:** Live view requires `ConnectBrowserLiveViewStream` IAM permission. The session also has to be `Ready`, not `Starting` or `Stopping`.
## Output
- Which framework fits (Strands vs Nova Act vs Playwright)
- Working code with session lifecycle handled
- IAM policy scoped to the browser resource
- Observability setup if needed (live view, recording)
## Quality criteria
- Browser sessions are always wrapped in a context manager or try/finally — never leaked
- IAM is scoped to `browser/*` in the account, not `Resource: "*"`
- Nova Act API keys and other secrets use `agentcore add credential` + `@requires_api_key`, not env vars
- The code handles the case where the agent runs outside AgentCore Runtime (no `.env.local`, no credential provider) — typically by reading a local secret for development and the credential provider for production
references/code-interpreter.md
# code-interpreter
Add the AgentCore Code Interpreter tool so your agent can execute code in a sandboxed environment — Python, JavaScript, or TypeScript.
## When to use
- Your agent needs to run math, data analysis, or transform data where a calculation is more reliable than an LLM answer
- Your agent generates code as an answer and you want it executed (and its output verified) before returning
- Your agent needs to read/write files (CSV, JSON, plots) that should persist to S3
- You need an isolated, session-scoped code sandbox
Do NOT use this reference for:
- Interacting with web pages — see [`browser.md`](browser.md)
- Running arbitrary long-lived services — Code Interpreter is for short-lived code execution, not hosting servers
- Shell commands *inside your live agent session's own microVM* — that's `InvokeAgentRuntimeCommand`, covered in [`integrate.md`](integrate.md)
## Mental model
Code Interpreter is a **managed sandbox**, one per session, running in an isolated microVM. Your code can:
- Execute Python, JavaScript, or TypeScript
- Read/write files on a local filesystem (up to 100 MB inline upload, up to 5 GB via S3)
- Make network calls (if internet access is enabled on the resource)
- Use pre-installed libraries (pandas, numpy, scikit-learn, torch, etc. — see docs for the current list)
Sessions are **stateful within a session** (variables and files persist across `execute_code` calls in the same session) and **ephemeral across sessions** (start a new session and the filesystem is clean).
## Prerequisites
- Python 3.10+ in your agent environment
- `bedrock-agentcore` SDK
- IAM permissions for `bedrock-agentcore:*CodeInterpreter*` actions, scoped to the resource ARN
- Model access if calling via an agent framework (the framework calls a model to decide when to execute code)
IAM policy skeleton:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "CodeInterpreterAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreateCodeInterpreter",
"bedrock-agentcore:GetCodeInterpreter",
"bedrock-agentcore:ListCodeInterpreters",
"bedrock-agentcore:StartCodeInterpreterSession",
"bedrock-agentcore:StopCodeInterpreterSession",
"bedrock-agentcore:InvokeCodeInterpreter",
"bedrock-agentcore:GetCodeInterpreterSession",
"bedrock-agentcore:ListCodeInterpreterSessions"
],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:code-interpreter/*"
}]
}
```
Check current action names against the docs — the list evolves.
## Path A — Strands agent with Code Interpreter (recommended for most)
```python
from strands import Agent
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
tool = AgentCoreCodeInterpreter(region="<REGION>")
agent = Agent(
tools=[tool.code_interpreter],
system_prompt=(
"You are an assistant that validates claims with code. "
"When asked to compute, calculate, or analyze, write Python and run it."
),
)
result = agent("What are the first 10 Fibonacci numbers?")
print(result.message["content"][0]["text"])
```
Install: `pip install bedrock-agentcore strands-agents strands-agents-tools`
The agent decides when to execute code, starts sessions on demand, and stops them. Under the hood, the tool uses the AWS-managed `aws.codeinterpreter.v1` resource — no resource creation needed.
**Dropping into an AgentCore Runtime entrypoint:**
```python
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
from model.load import load_model
import os
app = BedrockAgentCoreApp()
REGION = os.getenv("AWS_REGION", "us-east-1")
@app.entrypoint
def invoke(payload, context):
tool = AgentCoreCodeInterpreter(region=REGION)
agent = Agent(
model=load_model(),
tools=[tool.code_interpreter],
system_prompt="Validate computations with code.",
)
return {"response": str(agent(payload.get("prompt", "")))}
if __name__ == "__main__":
app.run()
```
## Path B — Direct SDK for programmatic execution
Use when your code — not an agent — decides what to run. Good for ETL, data transformation, and agent-internal validation.
```python
from bedrock_agentcore.tools.code_interpreter_client import code_interpreter_session
REGION = "us-east-1"
with code_interpreter_session(REGION) as session:
# Stateful: variables persist across calls within the session
session.execute_code("import pandas as pd")
session.execute_code("df = pd.DataFrame({'x': [1, 2, 3]})")
result = session.execute_code("df.describe().to_string()")
print(result.stdout)
```
The context manager handles start/stop. Do not leak sessions.
**Language selection** — default is Python. For JavaScript/TypeScript, pass `language="javascript"` or `language="typescript"` to `execute_code` (or the runtime setting at session start). See the runtime selection doc for the current supported runtimes.
## Path C — Custom Code Interpreter with S3 access
The managed `aws.codeinterpreter.v1` resource has no S3 write permissions. For agents that produce artifacts (plots, reports, processed datasets) you want to persist, create a **custom Code Interpreter** with an execution role that has S3 access.
This is a CreateCodeInterpreter call (SDK/API, not exposed via `agentcore` CLI at time of writing). The execution role's trust policy grants `bedrock-agentcore.amazonaws.com` the ability to assume it, and its permissions policy grants `s3:PutObject` and related actions on your artifact bucket. Check the docs for the current `CreateCodeInterpreter` shape and the exact trust policy format.
**Same-account S3 rule.** The S3 bucket must be in the **same AWS account** as the Code Interpreter resource. Cross-account buckets are not supported as targets even with the right bucket policy — `CreateCodeInterpreter` fails with a validation error. If you need the artifacts in another account, replicate from the same-account bucket afterward.
## Observability
- **CloudWatch logs** — stdout/stderr from executed code, plus session lifecycle events
- **CloudTrail** — every `StartCodeInterpreterSession`, `InvokeCodeInterpreter`, `StopCodeInterpreterSession` call
- **Metrics** — in `AWS/BedrockAgentCore` namespace
## Pre-installed libraries
The managed Python runtime includes: `pandas`, `numpy`, `scipy`, `matplotlib`, `plotly`, `scikit-learn`, `torch`, `torchvision`, `statsmodels`, and dozens more for data analysis / ML. Check the current list in the docs before telling a user "library X is preinstalled" — the list changes with platform updates.
For libraries not preinstalled, call `install_packages(["your-lib==1.2"])` in your session (or `!pip install ...` via `execute_command`). Installed packages last only for the session.
## Session lifecycle — always close
```python
# Right — context manager
with code_interpreter_session(region) as session:
session.execute_code("...")
# Right — try/finally with explicit client
client = CodeInterpreterClient(region=region)
client.start()
try:
client.execute_code("...")
finally:
client.stop()
# Wrong — leaked session sits until timeout
```
Default session timeout is 900 seconds (15 min), max 28800 seconds (8 hours). Leaked sessions cost money.
## VPC mode
Code Interpreter supports VPC — same pattern as Runtime and Browser (service-linked role, your subnets, your security group). See [`vpc.md`](vpc.md).
**Public internet from the sandbox** requires a NAT gateway on a private subnet, same as Runtime. Public subnets don't give Code Interpreter ENIs internet access. If the code needs `pip install` to reach PyPI, plan for NAT.
## Common failures
**"Access denied" on StartCodeInterpreterSession:** IAM missing the action on the resource ARN. Use `aws sts get-caller-identity` to confirm which identity you attached the policy to.
**"ValidationException: Role does not have access to required S3 buckets":** S3 bucket is in a different account. Move the bucket or replicate from an in-account staging bucket.
**Code times out:** Default execute timeout is short. Split long jobs into chunks, or use a custom Code Interpreter with extended timeouts. Don't try to run 30-minute training jobs in Code Interpreter — that's a SageMaker / Batch job.
**"Module not found" despite being listed as preinstalled:** The preinstalled list may differ between `python` and `nodejs` runtimes. Verify runtime selection and list matches.
## Output
- Which path fits (Strands tool vs direct SDK vs custom with S3)
- Working code with session lifecycle handled
- IAM policy scoped to the code-interpreter resource
## Quality criteria
- Sessions are always wrapped in a context manager or try/finally — never leaked
- IAM is scoped to `code-interpreter/*` in the account, not `Resource: "*"`
- S3 destination buckets are in the same account as the Code Interpreter resource
- Language / runtime selection is explicit when the code isn't Python
references/integrate.md
# integrate
Help a developer call their deployed agent from an application.
## When to use
- Developer has a deployed agent and wants to call it from their app
- Developer needs the agent URL and auth credentials
- Developer wants to handle streaming responses from the agent
- Developer needs to manage conversation sessions across multiple calls
- Developer is building a frontend, backend service, or CLI that consumes the agent
- Caller and agent are in different AWS accounts (cross-account invocation)
Do NOT use for:
- Giving the agent tools to call external APIs → use `agents-connect`
- Deploying the agent → use `agents-deploy`
- Debugging agent responses → use `agents-debug`
- Securing the agent endpoint for production → use `agents-harden` (but this skill covers the client-side auth code)
## Input
`$ARGUMENTS` can be:
- A language or framework: "from React", "in Python", "Node.js backend"
- An auth preference: "using IAM", "with JWT"
- Empty — the skill will detect the project context and guide accordingly
## Process
### Step 1: Check deployment status
Read `agentcore/agentcore.json` to get the agent name. Then check if it's deployed:
```bash
agentcore status --type agent
```
**If not deployed:** "Your agent needs to be deployed before you can call it from an app. Run `agentcore deploy` first, or use the `agents-deploy` skill for guidance."
Do not proceed until the agent is deployed.
### Step 2: Get the agent endpoint
```bash
agentcore fetch access --name <AgentName> --type agent
```
This returns:
- **Agent Runtime ARN** — needed for SDK invocation
- **Endpoint URL** — for direct HTTPS calls
- **Auth configuration** — what auth method is configured
Note the auth type from the output. It determines how the client app authenticates.
### Step 3: Determine auth method
Read the agent's `authorizerType` field from `agentcore/agentcore.json` (it's a top-level field on the runtime entry; JWT details live in the separate `authorizerConfiguration` object on the same runtime).
| Auth type | How the client authenticates | Best for |
|---|---|---|
| **None** (default) | IAM SigV4 signing on the request | Backend services with AWS credentials |
| **AWS_IAM** | IAM SigV4 signing on the request | Backend services, Lambda-to-agent calls |
| **CUSTOM_JWT** | Bearer token in Authorization header | Web/mobile apps with an identity provider |
**If no authorizer is configured:** The agent uses IAM auth by default. The calling identity needs `bedrock-agentcore:InvokeAgentRuntime` permission.
**If CUSTOM_JWT:** The client sends a JWT from the configured identity provider. The agent validates it against the discovery URL, allowed audience, and allowed clients configured during setup.
### Step 4: Generate client code
Based on the developer's language preference (from `$ARGUMENTS` or ask), generate the appropriate client code.
#### Python (boto3) — IAM auth
```python
import boto3
import json
from botocore.exceptions import ClientError
client = boto3.client("bedrock-agentcore", region_name="<REGION>")
try:
response = client.invoke_agent_runtime(
agentRuntimeArn="<AGENT_RUNTIME_ARN>",
qualifier="DEFAULT", # or a specific version number
payload=json.dumps({
"prompt": "Hello, what can you do?"
}).encode(),
runtimeSessionId="session-123", # reuse for multi-turn conversations
)
# Handle streaming response — response["response"] is a StreamingBody
stream = response["response"]
if hasattr(stream, "iter_lines"):
for line in stream.iter_lines():
if line:
print(line.decode(), end="", flush=True)
else:
# Some SDK versions return raw bytes — read all at once
content = stream.read()
print(content.decode() if isinstance(content, bytes) else content)
except ClientError as e:
code = e.response["Error"]["Code"]
if code == "AccessDeniedException":
# Missing bedrock-agentcore:InvokeAgentRuntime permission
raise RuntimeError("Caller lacks InvokeAgentRuntime permission") from e
elif code == "ValidationException":
# Wrong ARN, bad payload format, invalid session ID
raise RuntimeError(f"Invalid request: {e}") from e
elif code == "ThrottlingException":
# Retry with exponential backoff
raise
else:
raise
```
#### Python (HTTPS) — JWT auth
```python
import requests
AGENT_URL = "<ENDPOINT_URL>"
JWT_TOKEN = "<TOKEN_FROM_YOUR_IDP>"
response = requests.post(
AGENT_URL,
headers={
"Authorization": f"Bearer {JWT_TOKEN}",
"Content-Type": "application/json",
},
json={"prompt": "Hello, what can you do?"},
stream=True,
)
for chunk in response.iter_content(chunk_size=None):
print(chunk.decode(), end="", flush=True)
```
#### JavaScript/TypeScript (AWS SDK) — IAM auth
```typescript
import {
BedrockAgentCoreClient,
InvokeAgentRuntimeCommand,
} from "@aws-sdk/client-bedrock-agentcore";
const client = new BedrockAgentCoreClient({ region: "<REGION>" });
const response = await client.send(
new InvokeAgentRuntimeCommand({
agentRuntimeArn: "<AGENT_RUNTIME_ARN>",
qualifier: "DEFAULT",
payload: new TextEncoder().encode(
JSON.stringify({ prompt: "Hello, what can you do?" })
),
runtimeSessionId: "session-123",
})
);
// response.response is the streaming body
const decoder = new TextDecoder();
for await (const chunk of response.response) {
process.stdout.write(decoder.decode(chunk));
}
```
#### JavaScript/TypeScript (fetch) — JWT auth
```typescript
const AGENT_URL = "<ENDPOINT_URL>";
const JWT_TOKEN = "<TOKEN_FROM_YOUR_IDP>";
const response = await fetch(AGENT_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${JWT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt: "Hello, what can you do?" }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}
```
### Step 5: Session management
Explain how sessions work:
- **`runtimeSessionId`** — pass the same value across multiple calls to maintain conversation context
- Generate a unique session ID per user conversation (e.g., UUID)
- Sessions are server-side — the agent remembers the conversation history for that session ID
- If you omit the session ID, each call is stateless (no conversation memory)
```python
import uuid
# New conversation
session_id = str(uuid.uuid4())
# First turn
invoke(session_id, "What's the weather in Seattle?")
# Follow-up in same conversation
invoke(session_id, "What about tomorrow?")
# New conversation — new session
new_session_id = str(uuid.uuid4())
invoke(new_session_id, "Different topic entirely")
```
### Step 6: Protocol-specific guidance
Read the agent's `protocol` from `agentcore/agentcore.json`.
**If HTTP (default):** The patterns above apply directly.
**If MCP:** The agent exposes an MCP endpoint. Clients connect using the MCP protocol (Streamable HTTP). Point the developer to MCP client libraries for their language.
**If A2A:** The agent exposes an Agent-to-Agent endpoint with a card at `/.well-known/agent-card.json`. The calling agent discovers capabilities via the card and communicates over JSON-RPC 2.0. See [`references/multi-agent.md`](multi-agent.md) in this skill for A2A integration patterns.
### Step 7: Integration patterns that look right but fail
Two patterns come up often enough in support cases to call out directly.
**API Gateway `/{proxy+}` with a URL-encoded Runtime ARN.** Fronting AgentCore Runtime with an API Gateway REST API whose resource is `/{proxy+}` and whose integration URI is the encoded runtime ARN appears to work — the deploy succeeds and short requests return. Longer requests fail at around 2 minutes with `Integration closed connection prematurely` in the logs, regardless of `integrationTimeoutInMillis`. `HTTP_PROXY` is a generic forwarding integration; it doesn't handle SigV4, streaming, or session semantics the way the SDK client does.
Use one of these instead:
- Call Runtime directly from the client with the `bedrock-agentcore` SDK (Step 4 above). This is the intended path.
- Put a Lambda between API Gateway and Runtime if you need API Gateway for rate limiting, a public HTTPS endpoint, or other reasons. The Lambda receives the request, calls `invoke_agent_runtime`, and streams the response back. The Lambda's execution role needs `bedrock-agentcore:InvokeAgentRuntime`. Be aware that API Gateway has a 29-second hard ceiling on synchronous responses — this works only for fast agents. For anything multi-step, use the direct SDK path instead.
**Lambda-in-front for synchronous agent responses hits a short timeout ceiling.** A `Client → API Gateway → Lambda → Runtime` chain caps at ~29 seconds because of the API Gateway synchronous response limit. Any agent that reasons, calls multiple tools, or uses a non-trivial model will exceed it. If you're hitting timeouts on a Lambda wrapping Runtime, the fix is usually to drop the Lambda and let the client call Runtime directly — Runtime supports streaming responses natively, which is typically the reason teams add a Lambda in the first place.
### Step 8: Cross-account invocation
Calling an agent in a different AWS account than your caller uses standard AWS cross-account IAM patterns — no AgentCore-specific plumbing. The caller account assumes a role in the agent's account, gets temporary credentials, and uses them to sign the invoke request.
**Setup in the agent's account (Account B):**
Create an IAM role that trusts the caller account and has permission to invoke the runtime.
```json
// Trust policy — who can assume this role
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::<CALLER_ACCOUNT_ID>:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "<unique-external-id>"}
}
}]
}
```
```json
// Permissions policy — what this role can do
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<AGENT_ACCOUNT_ID>:runtime/<RUNTIME_NAME>-*"
}]
}
```
Scope the `Principal` in the trust policy as narrowly as possible (a specific role ARN in the caller account rather than `:root` for anything beyond proof-of-concept). Use an `ExternalId` to prevent the confused deputy problem.
**In the caller's app (Account A):**
```python
import boto3
import json
# Assume the role in Account B
sts = boto3.client("sts")
assumed = sts.assume_role(
RoleArn="arn:aws:iam::<AGENT_ACCOUNT_ID>:role/<ROLE_NAME>",
RoleSessionName="agent-invoker",
ExternalId="<unique-external-id>",
)
creds = assumed["Credentials"]
# Use the temporary credentials to invoke the runtime
agentcore = boto3.client(
"bedrock-agentcore",
region_name="<REGION>",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"],
)
response = agentcore.invoke_agent_runtime(
agentRuntimeArn="arn:aws:bedrock-agentcore:<REGION>:<AGENT_ACCOUNT_ID>:runtime/<RUNTIME_NAME>",
qualifier="DEFAULT",
payload=json.dumps({"prompt": "hello"}).encode(),
runtimeSessionId="session-123",
)
```
**Production notes:**
- Cache the assumed-role credentials. They're valid for the session duration (default 1 hour). Re-assume when they're close to expiring, not on every request.
- Boto3's `Session` with a profile using `role_arn` and `source_profile` can automate this if your caller environment supports AWS config profiles. `assume_role` in code is the explicit version.
- If the caller is in a Lambda, ECS task, or EC2 instance, the execution/task role is what gets the AssumeRole permission. That role's trust policy is what gets listed in Account B's trust policy.
- The runtime's own resource policy (if any) is separate from IAM. Typically you don't need a resource policy for cross-account — the IAM role in Account B is what grants access.
## Running shell commands inside a live agent session (`InvokeAgentRuntimeCommand`)
Once an agent's session is running, you can execute shell commands inside that **same session's microVM** — same filesystem, same env, same network namespace — and stream the output back. This sits alongside `InvokeAgentRuntime` (which drives the agent's reasoning loop), not in place of it.
When this is useful:
- Coding/devops agents where your app runs deterministic ops (git pull, build, test, file system inspection) instead of asking the LLM to reason about them
- Seeding the session's filesystem before the agent runs (drop a dataset into `/tmp`, then invoke the agent to analyze it)
- Debugging a stuck or misbehaving session — run `ps`, `ls`, `cat /tmp/log` from outside without going through the agent
- Any workflow where you want the reliability of a scripted command and the context of a warm session
When it's the wrong tool:
- Spawning new sessions to run arbitrary code for users — use the [`code-interpreter.md`](code-interpreter.md) built-in tool instead; it's purpose-built, sandboxed differently, and doesn't consume an agent's session
- Running anything an unrelated caller shouldn't be able to do — commands execute with the runtime's execution role and filesystem
**IAM permission required:** `bedrock-agentcore:InvokeAgentRuntimeCommand` on the runtime ARN. This is a **separate** action from `InvokeAgentRuntime` — scope it explicitly to the callers who need it.
```python
import boto3
client = boto3.client("bedrock-agentcore", region_name="<REGION>")
response = client.invoke_agent_runtime_command(
agentRuntimeArn="<AGENT_RUNTIME_ARN>",
qualifier="DEFAULT",
runtimeSessionId="session-123", # must be an existing session
command="ls -la /tmp && cat /tmp/status.json",
)
# Output streams back over HTTP/2 on response["response"]
for chunk in response["response"].iter_chunks():
print(chunk.decode(), end="", flush=True)
```
**Session must exist.** `InvokeAgentRuntimeCommand` attaches to a running session; it won't create one. If the session has expired or never existed, the call fails. Invoke the agent first (to start the session), then use the session ID for subsequent command calls.
**Same microVM, same filesystem.** A file written by the command is visible to the agent on the next invoke, and vice versa. Use this to pre-load artifacts, then reason over them in the agent. Session isolation still applies — other sessions cannot see these files.
> [!WARNING]
> InvokeAgentRuntimeCommand executes arbitrary shell commands inside a live agent
> session with the runtime's full execution role. Never grant
> bedrock-agentcore:InvokeAgentRuntimeCommand to the same principals that have
> bedrock-agentcore:InvokeAgentRuntime unless they explicitly need shell access.
> Always create a separate IAM policy for command execution. Always enable CloudTrail
> logging for InvokeAgentRuntimeCommand calls. If commands are constructed from
> user-supplied input, validate and sanitize — this is a command injection surface.
**IAM separation:** `InvokeAgentRuntimeCommand` is a distinct IAM action from `InvokeAgentRuntime`. Grant it only to the callers that need shell access — not to every identity that can invoke the agent. Minimal example:
```json
{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntimeCommand",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<YOUR_ACCOUNT_ID>:runtime/<RUNTIME_NAME>-*"
}
```
Keep this in a separate IAM policy from the one that grants `InvokeAgentRuntime`. Attach it only to roles that explicitly need to run commands inside agent sessions.
**Command injection:** The code example above uses a hardcoded command string — intentionally. If your real usage constructs commands from user-supplied input, validate before passing: reject strings containing `&&`, `;`, `$(...)`, backticks, `|`, or other shell metacharacters. Passing unsanitized user input to `InvokeAgentRuntimeCommand` is a direct code execution vulnerability.
**CloudTrail monitoring:** Enable an EventBridge rule to alert on unexpected `InvokeAgentRuntimeCommand` calls:
```bash
aws events put-rule \
--name AgentCoreCommandExecution \
--event-pattern '{"source":["aws.bedrock-agentcore"],"detail-type":["AWS API Call via CloudTrail"],"detail":{"eventName":["InvokeAgentRuntimeCommand"]}}' \
--state ENABLED
```
A compromised caller with this permission can read/write the agent's filesystem, reach any network resource the agent can reach, and use the execution role's credentials — CloudTrail logging is the minimum detection baseline.
## Reference integrations
Two common integration targets have published, reusable patterns you can start from instead of building the integration layer yourself.
**Slack.** [Integrating Amazon Bedrock AgentCore with Slack](https://aws.amazon.com/blogs/machine-learning/integrating-amazon-bedrock-agentcore-with-slack/) walks through a reusable integration layer that brings any AgentCore agent into a Slack workspace. The architecture (API Gateway → Lambda → SQS → AgentCore) handles Slack's 3-second webhook timeout via asynchronous processing: one Lambda validates the Slack signature and returns immediately, another posts a "Processing..." placeholder, and a third invokes the agent and replaces the placeholder with the real response. The pattern maps Slack thread timestamps to AgentCore Memory session IDs and Slack user IDs to actor IDs, so conversation context persists in the same thread over time. The integration layer is decoupled from the agent — you swap in any agent (FinOps, DevOps, incident response) without touching the Slack infrastructure. Deploys with one `cdk deploy`.
**Microsoft Teams.** The same async-processing architecture (API Gateway → Lambda → queue → AgentCore) applies to Teams. See [How Amazon Bedrock transforms Microsoft Teams conversations into actionable insights](https://aws.amazon.com/blogs/industries/how-amazon-bedrock-transforms-microsoft-teams-conversations-into-actionable-insights/) for Teams-specific setup (Bot Framework registration, bot channel configuration). If you've already built the Slack pattern above, the Teams version is primarily a different webhook validator and response formatter.
Both patterns handle the "webhook platform with short timeout" problem in the same way — the chat platform gets an immediate ack and a placeholder, the real agent call happens asynchronously, and the response replaces the placeholder when ready. If you're integrating a third chat platform not listed here, use either blog as a template.
## Output
- The agent's endpoint URL and ARN
- Auth method explanation with client-side code
- Working client code in the developer's preferred language
- Session management guidance
- Protocol-specific notes if applicable
## Quality criteria
- Client code uses the correct SDK client (`bedrock-agentcore`, not `bedrock-agent`)
- Auth method matches what's configured on the agent
- Streaming response handling is included (not just request/response)
- Session ID pattern is explained
- Code is complete and runnable — includes imports, error handling basics
references/local-vs-deployed.md
# Local vs. Deployed — What Works Where
AgentCore has a local dev server (`agentcore dev`) and a deployed runtime. They don't have feature parity. This reference tells you what works where so generated code and troubleshooting handle both environments correctly.
## Quick reference
| Feature | `agentcore dev` (local) | Deployed runtime |
|---|---|---|
| Agent invocation | ✅ via curl on localhost:8080 | ✅ via `invoke_agent_runtime` or HTTPS |
| Framework model calls | ✅ if Bedrock creds are available | ✅ |
| Python/JS function tools (framework-native) | ✅ | ✅ |
| Credentials (`@requires_api_key`, `@requires_access_token`) | ✅ from `agentcore/.env.local` | ✅ from Secrets Manager |
| Memory | ❌ env var not set locally | ✅ `MEMORY_<NAME>_ID` injected |
| Gateway | ❌ env var not set locally | ✅ `AGENTCORE_GATEWAY_<NAME>_URL` injected |
| Cedar policy evaluation | ❌ policies only enforced at gateway | ✅ |
| Traces (X-Ray) | ✅ `agentcore dev` emits OTEL to CloudWatch by default; disable with `--no-traces` | ✅ auto-enabled |
| CloudWatch logs | ✅ via ADOT / OTEL wiring (same path as traces) | ✅ if using `logging` module + OTEL |
| **Evaluator *definition*** (`agentcore add evaluator`, writing the instructions/code) | ✅ — writes to `agentcore.json`; custom code is unit-testable locally | ✅ |
| **`agentcore run eval`** (on-demand eval over traces) | ✅ — operates on CloudWatch spans; local-dev spans land there if OTEL is on (default) | ✅ |
| **`Evaluate` API with hand-constructed spans** (boto3) | ✅ — no runtime needed at all; submit `SessionSpans` directly | ✅ |
| **Dataset runner** (`OnDemandEvaluationDatasetRunner`) | ❌ invokes an AgentCore Runtime agent in its pipeline | ✅ |
| **Online eval monitoring** (`agentcore add online-eval`) | ❌ ingests traces continuously from deployed runtime | ✅ |
| Observability dashboards | ✅ once Transaction Search is on and local spans are flowing | ✅ in CloudWatch console |
| VPC networking | ❌ local always has internet | ✅ subject to `networkMode: VPC` |
| Inbound auth (AWS_IAM, CUSTOM_JWT) | ❌ no auth required locally | ✅ enforced on every request |
## Implications for generated code
**Always guard features that aren't available locally:**
```python
# Memory pattern
MEMORY_ID = os.getenv("MEMORY_MYMEMORY_ID")
if MEMORY_ID:
# deployed — wire up memory
session_manager = AgentCoreMemorySessionManager(...)
else:
# local — agent runs without memory
session_manager = None
```
```python
# Gateway pattern
GATEWAY_URL = os.getenv("AGENTCORE_GATEWAY_WEATHER_URL")
if GATEWAY_URL:
# deployed — use gateway tools
tools = get_gateway_tools(GATEWAY_URL)
else:
# local — agent runs without external tools or with local stubs
tools = []
```
**Credentials work in both, but read from different sources.** The `@requires_api_key` decorator handles this automatically — don't try to read env vars directly.
## Testing workflow
Because memory, gateway, and policies don't work locally, the realistic test loop is:
1. **Local:** `agentcore dev` to verify the agent's code structure, framework wiring, system prompt, and any in-code logic
2. **Deploy to a staging target:** `agentcore deploy --target staging` to test with real memory, gateway, and policies
3. **Production:** only after staging validation
Don't expect `agentcore dev` to reproduce a production failure involving memory recall, gateway tool calls, or policy denials — those require a deployed environment.
## Common "works locally, fails deployed" causes
- Missing `MEMORY_<NAME>_ID` guard — code crashes because the env var is unexpectedly present
- Hardcoded localhost URLs for gateway — replace with `AGENTCORE_GATEWAY_<NAME>_URL`
- IAM permissions that work for your dev credentials but not the execution role
- Region mismatch between `aws configure` (used locally) and `aws-targets.json` (used in deploy)
- Tool call auth that works with your personal credentials but not with gateway SigV4 from the execution role
## Common "works deployed, fails locally" causes
- Code that assumes memory/gateway env vars are always set
- Direct SDK calls that expect the deployed execution role's permissions
- Hardcoded deployed-only URLs or ARNs
references/memory.md
# memory
Add, configure, and debug AgentCore Memory — the managed service that lets your agent remember things across sessions.
## When to use
- You want your agent to remember user preferences, facts, or conversation history across separate sessions
- You added memory via `agentcore create` or `agentcore add memory` and need to wire it into your agent code
- Memory recall isn't working as expected
- You want to share memory across multiple agents
Do NOT use this skill for within-session conversation history. That's handled automatically by the runtime — no configuration needed.
## Input
`$ARGUMENTS` is optional. If provided, use it as the memory resource name:
```
/memory # uses name from agentcore.json, or prompts
/memory UserContext # targets a specific memory resource by name
```
## Process
### Step 1: Read the project
Read `agentcore/agentcore.json`. Look for:
- The `memories` array — is memory already configured?
- The `runtimes` array — what agents are in the project and what framework do they use?
- The project `name` — needed for env var construction
**If `agentcore/agentcore.json` does not exist**, check if there's any AgentCore project structure nearby (look for `agentcore/` directory). If none found, proceed with the most helpful answer possible based on what the developer asked — don't block on missing context. If the question is about strategy selection or code patterns, answer it directly. Only ask "which situation are you in?" if the answer genuinely depends on it (e.g., they need CLI commands that differ by setup type).
### Step 2: Determine the situation
**Case A — No memory configured yet**
The `memories` array is empty or missing. Proceed to Step 3 (strategy selection).
**Case B — Memory configured, needs wiring**
Memory exists in `agentcore.json` but the agent code doesn't use it yet. Skip to Step 5 (generate wiring code).
**Case C — Memory configured and wired, debugging recall**
Ask: "What's happening? What did you expect the agent to remember, and what did it actually do?"
Then diagnose using the patterns in the Debugging section below.
**Case D — Developer asking about memory without a project**
Answer the question directly. For strategy questions, explain the options. For code questions, show the pattern with a note that they'll need to substitute their actual memory ID.
### Step 3: Choose a strategy
Present the options and ask the developer which fits their use case. Don't skip this — the wrong strategy wastes money and produces worse results.
```
Which memory strategy fits your use case?
SEMANTIC
Best for: remembering facts about users across sessions
How it works: extracts facts and stores them as embeddings; retrieves
relevant context via similarity search at session start
Cost: higher (embedding model + vector search per session)
Example: "Remember that Alex prefers bullet points and works in fintech"
USER_PREFERENCE
Best for: remembering explicit settings and preferences
How it works: extracts structured preference data; optimized for
key-value retrieval
Cost: lower (structured extraction, no vector search)
Example: "Remember my preferred response format and language"
SUMMARIZATION
Best for: remembering what you talked about last time
How it works: compresses conversation history into summaries; injects
the summary at the start of each new session
Cost: medium (summarization model runs at session end)
Example: "Pick up where we left off last time"
EPISODIC
Best for: remembering sequences of events or interactions over time
How it works: stores episodic records of interactions with temporal
context
Cost: medium
Common combinations:
SEMANTIC + USER_PREFERENCE → facts + preferences (most common)
SEMANTIC + SUMMARIZATION → full episodic memory (highest capability, highest cost)
USER_PREFERENCE alone → lightweight preference store
Which strategy (or combination) do you want?
```
### Step 4: Add memory to agentcore.json
Run the CLI command to add memory to the project config:
```bash
agentcore add memory --name <MemoryName> --strategies <STRATEGY1,STRATEGY2> --expiry 30
```
This updates `agentcore/agentcore.json`. The memory resource is provisioned when you next run `agentcore deploy` — it takes 2–5 minutes to become active.
The resulting config entry looks like:
```json
{
"memories": [{
"type": "AgentCoreMemory",
"name": "MyMemory",
"eventExpiryDuration": 30,
"strategies": [
{"type": "SEMANTIC"},
{"type": "USER_PREFERENCE"}
]
}]
}
```
**Memory name rules:** alphanumeric + underscores, max 48 chars, starts with a letter.
**Env var injected at deploy time:** `MEMORY_<UPPERCASENAME>_ID`
Example: memory named `UserContext` → env var `MEMORY_USERCONTEXT_ID`
### Step 5: Generate wiring code
Read `app/<AgentName>/main.py` (or the equivalent entrypoint) to detect the framework. Each framework has its own integration pattern — pick the one that matches:
| Framework | Recommended integration | Source |
|---|---|---|
| Strands | `AgentCoreMemorySessionManager` (CLI template) | `bedrock_agentcore.memory.integrations.strands.*` |
| LangGraph | `AgentCoreMemorySaver` + `AgentCoreMemoryStore` | `langgraph-checkpoint-aws` (official AWS-maintained) |
| OpenAI Agents SDK | `MemoryClient` via `@function_tool` | `bedrock_agentcore.memory.MemoryClient` |
| Google ADK / Claude Agent SDK | BYO — use `MemoryClient` directly | Validate end-to-end before shipping |
> [!WARNING]
> Always check for the MEMORY_ID env var before initializing memory. Memory is NOT
> available during `agentcore dev` — the env var is only set after deploy. Code that
> assumes memory is always available will fail silently in local development.
#### Strands — Session Manager pattern (recommended for new projects)
```python
import os
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
from strands import Agent
from model.load import load_model # scaffolded by `agentcore create`
app = BedrockAgentCoreApp()
# AgentCore injects this env var after deploy.
# Format: MEMORY_<UPPERCASENAME>_ID
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
@app.entrypoint
def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
session_manager = None
if MEMORY_ID:
# RetrievalConfig parameters:
# top_k: max number of memory records to retrieve per namespace (SDK default: 10)
# relevance_score: similarity threshold, 0 = return anything, 1 = exact match (SDK default: 0.2)
# The CLI template deviates from SDK defaults to favor precision over recall:
# top_k=3 limits context window usage; relevance_score=0.5 filters low-quality matches.
# Tune these if retrieval misses relevant facts (lower) or surfaces irrelevant ones (raise).
memory_config = AgentCoreMemoryConfig(
memory_id=MEMORY_ID,
session_id=session_id,
actor_id=actor_id,
retrieval_config={
f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5),
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
}
)
session_manager = AgentCoreMemorySessionManager(memory_config, REGION)
agent = Agent(
model=load_model(),
session_manager=session_manager, # None is safe — agent runs without memory
system_prompt="You are a helpful assistant.",
)
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()
```
#### Strands — Hook pattern (for adding memory to an existing agent)
```python
import os
from bedrock_agentcore.memory import MemoryClient
from strands.hooks import AgentInitializedEvent, HookProvider, MessageAddedEvent
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
memory_client = MemoryClient(region_name=os.getenv("AWS_REGION", "us-east-1")) if MEMORY_ID else None
class MemoryHook(HookProvider):
def on_agent_initialized(self, event):
"""Load recent conversation turns into the agent's context."""
if not MEMORY_ID:
return
session_id = event.agent.state.get("session_id", "default")
turns = memory_client.get_last_k_turns(
memory_id=MEMORY_ID,
actor_id="user",
session_id=session_id,
k=3
)
if turns:
context = "\n".join([
f"{m['role']}: {m['content']['text']}"
for t in turns for m in t
])
event.agent.system_prompt += f"\n\nPrevious conversation:\n{context}"
def on_message_added(self, event):
"""Save each message to memory after it's processed."""
if not MEMORY_ID:
return
session_id = event.agent.state.get("session_id", "default")
msg = event.agent.messages[-1]
memory_client.create_event(
memory_id=MEMORY_ID,
actor_id="user",
session_id=session_id,
messages=[(str(msg["content"]), msg["role"])]
)
def register_hooks(self, registry):
registry.add_callback(AgentInitializedEvent, self.on_agent_initialized)
registry.add_callback(MessageAddedEvent, self.on_message_added)
# Add to your existing agent:
agent = Agent(
# ... your existing config ...
hooks=[MemoryHook()] if MEMORY_ID else [],
state={"session_id": "default"},
)
```
#### LangGraph — `langgraph-checkpoint-aws` (recommended)
LangGraph has an **official AWS-maintained integration** via the [`langgraph-checkpoint-aws`](https://pypi.org/project/langgraph-checkpoint-aws/) package. It provides two integrations that map cleanly to LangGraph's memory model:
- **`AgentCoreMemorySaver`** — persists LangGraph's checkpoint objects (conversation state, execution graph, metadata) to AgentCore Memory. This is LangGraph's short-term / session memory.
- **`AgentCoreMemoryStore`** — saves conversational messages for AgentCore's long-term extraction (facts, preferences, summaries) and lets the agent search those memories in future sessions.
Use these instead of wiring `MemoryClient` calls into your graph manually — they handle the protocol conversion, actor/session mapping, and retry logic for you.
**Install:**
```bash
pip install langgraph-checkpoint-aws
```
**Required IAM permissions** on the agent's execution role:
- `bedrock-agentcore:CreateEvent`
- `bedrock-agentcore:ListEvents`
- `bedrock-agentcore:RetrieveMemories`
**Basic pattern — short-term checkpointing only:**
```python
import os
from langgraph.prebuilt import create_react_agent
from langgraph_checkpoint_aws import AgentCoreMemorySaver
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from model.load import load_model # scaffolded by `agentcore create`
app = BedrockAgentCoreApp()
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
# Only wire checkpointing if memory is available (deployed)
checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
@app.entrypoint
async def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
graph = create_react_agent(
model=load_model(),
tools=tools,
checkpointer=checkpointer, # None is safe — graph runs without persistence
)
# LangGraph's RunnableConfig maps thread_id → AgentCore session_id,
# actor_id → AgentCore actor_id under the hood
config = {
"configurable": {
"thread_id": session_id,
"actor_id": actor_id,
}
}
result = await graph.ainvoke(
{"messages": [("human", payload["prompt"])]},
config=config,
)
return {"response": result["messages"][-1].content}
```
**Full pattern — short-term + long-term retrieval:**
For long-term memory (facts, preferences, summaries extracted by AgentCore), add `AgentCoreMemoryStore` with a pre-model hook that saves messages for extraction and (optionally) retrieves relevant memories:
```python
import os
import uuid
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
from langgraph.prebuilt import create_react_agent
from langgraph.store.base import BaseStore
from langgraph_checkpoint_aws import AgentCoreMemorySaver, AgentCoreMemoryStore
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
checkpointer = AgentCoreMemorySaver(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
store = AgentCoreMemoryStore(MEMORY_ID, region_name=REGION) if MEMORY_ID else None
def pre_model_hook(state, config: RunnableConfig, *, store: BaseStore):
"""Save the latest human message for async extraction; optionally retrieve preferences."""
actor_id = config["configurable"]["actor_id"]
thread_id = config["configurable"]["thread_id"]
namespace = (actor_id, thread_id)
messages = state.get("messages", [])
for msg in reversed(messages):
if isinstance(msg, HumanMessage):
store.put(namespace, str(uuid.uuid4()), {"message": msg})
break
# Optional: retrieve user preferences to inject into context
# preferences_ns = ("preferences", actor_id)
# preferences = store.search(preferences_ns, query=msg.content, limit=5)
return {"llm_input_messages": messages}
graph = create_react_agent(
model=load_model(),
tools=tools,
checkpointer=checkpointer,
store=store,
pre_model_hook=pre_model_hook if store else None,
)
```
**Invoke with config:**
```python
config = {"configurable": {"thread_id": "session-1", "actor_id": "user-alice"}}
response = graph.invoke({"messages": [("human", "I prefer short answers.")]}, config=config)
# New session for the same actor — long-term memories are retrieved
new_config = {"configurable": {"thread_id": "session-2", "actor_id": "user-alice"}}
response = graph.invoke({"messages": [("human", "Summarize my latest report.")]}, config=new_config)
```
The agent remembers "I prefer short answers" across sessions because AgentCore Memory extracts it as a user preference. See the [AgentCore docs on LangGraph integration](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-integrate-lang.html) for the full reference.
**If you need low-level control** (custom retrieval queries, direct event management), fall back to `MemoryClient`:
```python
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name=REGION)
# client.create_event(...), client.retrieve_memories(...), client.get_last_k_turns(...)
```
Use `MemoryClient` directly only when the checkpoint/store abstractions don't fit your use case.
#### OpenAI Agents SDK — memory as function tools
The OpenAI Agents SDK pattern is to expose memory as `@function_tool` decorated functions. The agent decides when to read and write:
```python
import os
from agents import Agent, Runner, function_tool
from bedrock_agentcore.memory import MemoryClient
MEMORY_ID = os.getenv("MEMORY_<UPPERCASENAME>_ID")
REGION = os.getenv("AWS_REGION", "us-east-1")
_client = MemoryClient(region_name=REGION) if MEMORY_ID else None
def _build_memory_tools(actor_id: str, session_id: str):
"""Factory — binds actor/session into tool closures."""
@function_tool
def recall_context(query: str, top_k: int = 3) -> str:
"""Search long-term memory for facts or preferences about the user."""
if not _client or not MEMORY_ID:
return "Memory unavailable."
try:
memories = _client.retrieve_memories(
memory_id=MEMORY_ID,
namespace=f"/users/{actor_id}/facts",
query=query,
top_k=top_k,
)
return "\n".join(m.get("content", {}).get("text", "") for m in memories) or "No relevant memories."
except Exception as e:
return f"Memory error: {e}"
@function_tool
def save_fact(content: str) -> str:
"""Save a fact to long-term memory."""
if not _client or not MEMORY_ID:
return "Memory unavailable."
try:
_client.create_event(
memory_id=MEMORY_ID,
actor_id=actor_id,
session_id=session_id,
messages=[(content, "ASSISTANT")],
)
return "Saved."
except Exception as e:
return f"Error: {e}"
return [recall_context, save_fact]
@app.entrypoint
async def invoke(payload, context):
actor_id = payload.get("userId", "default-user")
session_id = getattr(context, "session_id", "default-session")
agent = Agent(
name="Assistant",
instructions="Use recall_context at the start of each session to check what you know about the user. Use save_fact when the user tells you something worth remembering.",
tools=_build_memory_tools(actor_id, session_id),
)
result = await Runner.run(agent, payload["prompt"])
return {"response": result.final_output}
```
#### Google ADK and Claude Agent SDK — bring your own memory integration
AgentCore Memory doesn't have a framework-specific integration for ADK or the Claude Agent SDK yet, and the samples repo doesn't contain a combined pattern we can point to. Use the general `MemoryClient` API and wire it into the framework's existing extension points:
- **Google ADK:** Expose memory operations as ADK tools (functions passed to `Agent(tools=[...])`). The ADK agent decides when to call them.
- **Claude Agent SDK:** Wrap `query()` with a pre-call memory load and a post-call memory save. The SDK's `ClaudeAgentOptions.system_prompt` is the injection point for retrieved context.
For both frameworks, follow the `MemoryClient` API shown in the OpenAI Agents pattern above — the client calls (`retrieve_memories`, `create_event`, `get_last_k_turns`) are identical. The framework-specific part is just where you call them.
Before shipping a memory integration for ADK or Claude SDK, validate the end-to-end flow against a deployed agent:
1. Deploy with memory enabled
2. Invoke the agent with facts to remember
3. Start a new session
4. Invoke again and verify the agent recalls those facts
5. Check `agentcore logs --runtime <AgentName> --query "memory" --since 1h --level error` for any memory errors
If you build a working pattern, consider contributing it to [`awslabs/agentcore-samples`](https://github.com/awslabs/agentcore-samples) so the next developer doesn't have to figure it out.
### Step 6: Explain the local dev gap and next steps
Always include this note:
```
⚠️ Memory is not available during local development (agentcore dev).
The MEMORY_<NAME>_ID env var is only injected after deploy. The code above
handles this gracefully — it runs without memory when the env var isn't set.
To test memory:
agentcore deploy -y
agentcore invoke "My name is Alex and I prefer concise answers"
agentcore invoke "What do you know about me?"
If using long-term memory (SEMANTIC or USER_PREFERENCE), wait 5–30 seconds
between the first and second invoke — extraction runs asynchronously after
each session ends.
Session ID note: use UUIDs (v4) for session IDs — they satisfy the platform's
minimum length requirement (33 characters) and are what `agentcore invoke`
generates by default. Short or sequential session IDs (e.g., "session-1",
"test") can cause long-term memory extraction to fail silently.
```
**If the developer is using the SDK directly (no CLI project)**, they need to create the memory resource first:
```python
from bedrock_agentcore.memory import MemoryClient
client = MemoryClient(region_name="us-east-1")
# Create memory and wait for it to become ACTIVE (takes 2-5 minutes)
memory = client.create_memory_and_wait(
name="UserMemory",
strategies=[
{"userPreferenceMemoryStrategy": {
"name": "prefs",
"namespaces": ["/user/preferences/"]
}},
{"semanticMemoryStrategy": {
"name": "facts",
"namespaces": ["/user/facts/"]
}}
],
event_expiry_days=30
)
MEMORY_ID = memory["id"]
print(f"Memory created: {MEMORY_ID}")
# Set this as an env var or hardcode for testing:
# export MEMORY_ID=<value>
```
Then use the same wiring code from Step 5, reading `MEMORY_ID` from the environment.
## Debugging memory recall
If memory was working and stopped, or never worked:
**Agent keeps forgetting things even with memory set up:**
Most common cause: the memory resource is configured but the code isn't reading from it at session start. Check that your entrypoint calls `get_last_k_turns` (or uses the session manager) before creating the agent, not after. Also verify the `MEMORY_<NAME>_ID` env var is set — it's only injected after deploy, not during `agentcore dev`.
**Memory not persisting across sessions:**
1. Check that LTM strategies (SEMANTIC, USER_PREFERENCE) are configured — not just SUMMARIZATION
2. Wait 5–30 seconds after a session ends before starting a new one — extraction is async
3. Verify the memory resource is ACTIVE: `agentcore status --type memory`
4. Use UUIDs (v4) for session IDs — the platform requires a minimum of 33 characters. Short IDs like "session-1" or "test" cause LTM to fail silently. `agentcore invoke` generates compliant IDs by default.
**Memory not loading at session start:**
1. Verify `MEMORY_<NAME>_ID` env var is set: `agentcore status --type memory --json`
2. Check the actor_id is consistent across sessions — memory is scoped per actor
3. Confirm the namespace paths in retrieval_config match the namespaces used when writing — the retrieval namespace must exactly match the namespace the strategy extracts into
4. CLI defaults use paths without trailing slashes (e.g., `/users/{actorId}/facts`). If you customized namespace templates when creating the memory resource, use whatever pattern you chose — consistency between writer and reader is what matters.
**Memory provisioning slow:**
Memory takes 2–5 minutes to become ACTIVE after `agentcore deploy`. Check status:
```bash
agentcore status --type memory
```
## S3 delivery / export buckets must be in the same account
If you're configuring S3 delivery for memory exports, session transcripts, or Browser recording output, the destination bucket must be in the **same AWS account** as the AgentCore resource. Cross-account S3 buckets are not supported as delivery destinations, even with correct bucket policies granting the service principal access.
Symptom of attempting a cross-account bucket: `CreateMemory` (or the relevant resource creation call) fails with `ValidationException: Role does not have access to required S3 buckets` — even when IAM and bucket policies are correctly configured for cross-account access.
**Workaround:** create a same-account bucket for the AgentCore resource to write to. If you need the data in a different account, replicate from the same-account bucket via S3 replication or a scheduled copy job.
## Sharing memory across agents
Memory is a top-level resource — not nested under a single agent. To share:
1. Create one memory resource: `agentcore add memory --name SharedMemory --strategies SEMANTIC`
2. In each agent's code, read the same env var: `MEMORY_SHAREDMEMORY_ID`
3. Use a consistent `actor_id` scheme across agents (e.g., the end user's ID)
## Cross-region inference (data residency)
Memory consolidation (extraction + summarization for long-term strategies) uses cross-region inference by default. Your memory **data stays in your primary region**, but the **inference call** that extracts facts or summarizes a session may execute in another AWS region within the same geography (e.g., `us-east-1` → `us-east-2` or `us-west-2`; EU stays in EU; etc.).
This matters when:
- You have a data-residency requirement that goes beyond storage — some regulations constrain where inference may run, not just where results land
- You're building for a customer whose contract pins processing to a single region
- Your audit trail needs to show which region handled each prompt
**There's no extra cost for cross-region inference, and CloudWatch/CloudTrail logs don't include the inference region.** Across the `Memory`, `Policy`, and `Evaluations` services, this is the default behavior.
**To opt out for Memory:** use a **built-in-with-overrides** strategy (see [`memory-custom-strategy`](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-custom-strategy.html)) and pin the model to a specific region. The overrides strategy lets you specify the exact model ID used for extraction and consolidation, which gives you region control.
The supported geographies and inference-region mappings change as AgentCore expands — check [the cross-region inference docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/cross-region-inference.html) for the current list rather than baking it in here.
## Beyond the CLI: memory features that require the API
The CLI's `agentcore add memory` and `agentcore.json` cover strategy selection, expiry, and basic configuration. Some memory capabilities are API/SDK-only — the CLI doesn't expose them. When the developer needs one of these, the graduation path is: create the memory via CLI as usual, deploy, then apply the additional config via boto3 or AWS CLI.
**Resource-based policies** (cross-account access, principal-level restrictions):
```python
import boto3, json
client = boto3.client("bedrock-agentcore-control")
memory_id = "<MEMORY_ID>" # from: agentcore status --type memory --json
client.put_memory_resource_policy(
memoryId=memory_id,
policy=json.dumps({
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111122223333:root"},
"Action": [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:RetrieveMemories",
"bedrock-agentcore:ListEvents"
],
"Resource": "*"
}]
})
)
```
**Custom extraction models** (pin the model used for LTM extraction — e.g., for data residency):
Use the "built-in with overrides" strategy type via `UpdateMemory`. See the [custom memory strategy docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory-custom-strategy.html) for the full configuration shape.
**Self-managed strategies** (bring your own extraction logic):
Also API-only. See the AgentCore memory docs for the `selfManagedMemoryStrategy` configuration.
**When you hit a memory capability not covered here**, use the `awsknowledge` MCP server if available — search for the specific API operation (e.g., "AgentCore PutMemoryResourcePolicy") to get the current parameter shapes. The API surface evolves between releases.
**General rule:** if `agentcore.json` has a field for it, use the CLI. If it doesn't, create the resource via CLI, deploy, then apply the additional config via boto3. Don't fight the CLI to do something it wasn't designed for.
## Output
- Updated `agentcore/agentcore.json` with memory resource (via CLI command)
- Wiring code for `app/<AgentName>/main.py` appropriate for the detected framework
- Explanation of the local dev gap and how to test after deploy
## Quality criteria
- Generated code handles `MEMORY_ID` being None (local dev) without crashing
- Env var name matches the memory resource name in `agentcore.json` (uppercase, underscores)
- Framework-specific pattern is used — never generate Strands hooks for a LangGraph project
- LTM extraction delay is communicated
- Session ID guidance recommends UUIDs (v4) when LTM strategies are used (minimum 33 characters)
references/migrate.md
# migrate
Move an existing Amazon Bedrock Agent to AgentCore Runtime.
## When to use
- You have an existing Bedrock Agent (created via the Bedrock console or API) and want to run it on AgentCore Runtime
- You want to add AgentCore capabilities (Memory, Gateway, Observability) to an existing agent
- You want to move from the declarative Bedrock Agents model to a code-first framework
## Input
`$ARGUMENTS` is optional:
```
/migrate # interactive — walks through the migration
/migrate strands # migrate targeting Strands framework
/migrate langgraph # migrate targeting LangGraph framework
```
## What migration does
The `agentcore create --type import` command reads your existing Bedrock Agent's configuration and generates an AgentCore project that reproduces its behavior in a code-first framework. Specifically:
- **System prompt** → copied into the generated `main.py`
- **Action groups (Lambda-backed)** → converted to Gateway targets with `--type lambda-function-arn`
- **Knowledge bases** → referenced in the system prompt with a note to wire retrieval manually (AgentCore doesn't auto-import KB bindings)
- **Guardrails** → noted in comments but not auto-converted (AgentCore uses Cedar policies, not Bedrock Guardrails)
- **Agent alias / version** → the import targets a specific alias, not the draft
What migration does **not** do:
- It does not delete or modify the original Bedrock Agent — the source agent keeps running
- It does not migrate conversation history or session state
- It does not convert Bedrock Guardrails to Cedar policies (different authorization model)
- It does not auto-wire Knowledge Base retrieval — you'll need to add that as a tool or direct SDK call
## Prerequisites
1. The Bedrock Agent must exist and have at least one alias
2. Your AWS credentials must have `bedrock:GetAgent`, `bedrock:GetAgentAlias`, and `bedrock:ListAgentActionGroups` permissions
3. You need the agent ID, alias ID, and region
## Process
### Step 1: Run the import
```bash
agentcore create \
--type import \
--agent-id <AGENT_ID> \
--agent-alias-id <ALIAS_ID> \
--region <REGION> \
--name <ProjectName> \
--framework Strands
```
The `--framework` flag determines which code-first framework the generated project uses. Strands is recommended for the closest mapping to Bedrock Agent behavior.
**Project name rules apply:** max 23 characters, alphanumeric only, starts with a letter.
### Step 2: Review the generated project
```bash
cd <ProjectName>
cat app/<AgentName>/main.py
cat agentcore/agentcore.json
```
Check:
- The system prompt matches your original agent's instructions
- Action groups appear as Gateway targets in `agentcore.json` (under `agentCoreGateways`)
- The model ID is correct for your target region
### Step 3: Fill in what migration doesn't cover
**Knowledge Bases:** If your Bedrock Agent used Knowledge Bases, you have two options:
1. **Keep using the KB via boto3** — call `bedrock-agent-runtime:RetrieveAndGenerate` or `Retrieve` directly from your agent code as a tool
2. **Replace with AgentCore Memory** — if the KB was used for user-specific context, AgentCore Memory with SEMANTIC strategy may be a better fit. See [memory.md](memory.md).
**Guardrails → Cedar policies:** Bedrock Guardrails (content filters, denied topics, word filters) don't have a 1:1 mapping to Cedar policies. Cedar policies control *which tools the agent can call and with what parameters* — they're authorization rules, not content filters. If you need content filtering, keep the guardrail logic in your agent code (pre/post-processing) or use Bedrock Guardrails as a standalone API call.
**Custom orchestration:** If your Bedrock Agent used custom orchestration (return-of-control, custom Lambda orchestrators), you'll need to rebuild that logic in the framework's native patterns — Strands tool chains, LangGraph graph nodes, etc.
### Step 4: Test locally and deploy
```bash
# Test locally (memory and gateway won't be available yet)
agentcore dev
# Deploy when ready
agentcore deploy -y
# Verify
agentcore invoke "Hello, what can you do?"
agentcore status
```
### Step 5: Cut over traffic
Once the AgentCore agent is working correctly:
1. Update your application to invoke the AgentCore Runtime instead of the Bedrock Agent
2. See [integrate.md](integrate.md) for the invocation patterns (SigV4, JWT, SDK)
3. Keep the original Bedrock Agent running as a fallback until you're confident
4. Delete the Bedrock Agent only after the AgentCore agent has been stable in production
## Common migration issues
**"Model not available in target region"**
The imported agent may reference a model ID that isn't available in your AgentCore deployment region. Edit `model/load.py` to use a cross-region inference profile or a model available in your region.
**"Action group Lambda in a different region"**
Gateway targets can invoke Lambda functions cross-region, but latency increases. Consider deploying the Lambda in the same region as your AgentCore agent, or accept the latency trade-off.
**"Agent behavior differs after migration"**
The most common cause is prompt format differences between Bedrock Agent's orchestration and the code-first framework. Bedrock Agent injects structured XML around tool results; Strands/LangGraph use different formats. Tune the system prompt to compensate.
## Output
- A working AgentCore project that reproduces the Bedrock Agent's behavior
- A list of what was auto-converted and what needs manual work
- Guidance on cutting over traffic from the old agent to the new one
references/multi-agent.md
# multi-agent
Build AgentCore systems where agents delegate work to other agents.
## When to use
- You want an orchestrator agent to delegate complex tasks to a specialist
- You're building a system where agents have different roles and capabilities
- You want agents to discover and communicate with each other via the A2A standard
- You want multiple agents to share the same memory
## Input
`$ARGUMENTS` is optional:
```
/multi-agent # interactive — asks which pattern you need
/multi-agent a2a # A2A protocol setup
/multi-agent direct # direct invocation pattern
/multi-agent memory # shared memory across agents
```
## Choosing a pattern
### Step 1: Deploy the specialist agent
The specialist is a standard AgentCore agent. Deploy it normally:
```bash
agentcore create --name SpecialistAgent --defaults
# ... add your specialist logic to app/SpecialistAgent/main.py ...
agentcore deploy -y
```
Get the specialist's runtime ARN after deploy:
```bash
agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn'
```
### Step 2: Add the specialist as a tool in the orchestrator
The orchestrator calls the specialist via `bedrock-agentcore:InvokeAgentRuntime`. Add this tool to your orchestrator's agent code:
```python
import os
import json
import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
app = BedrockAgentCoreApp()
# Set this env var in your orchestrator's deployment config
SPECIALIST_ARN = os.getenv("SPECIALIST_AGENT_ARN")
REGION = os.getenv("AWS_REGION", "us-east-1")
def call_specialist(prompt: str, session_id: str = None) -> str:
"""
Call the specialist agent and return its response.
The specialist runs in its own isolated session.
"""
client = boto3.client("bedrock-agentcore", region_name=REGION)
kwargs = {
"agentRuntimeArn": SPECIALIST_ARN,
"qualifier": "DEFAULT", # or a specific version number to pin
"payload": json.dumps({"prompt": prompt}).encode(),
}
if session_id:
kwargs["runtimeSessionId"] = session_id
response = client.invoke_agent_runtime(**kwargs)
# response["response"] is a StreamingBody — read, then parse JSON
body = response["response"].read()
result = json.loads(body.decode() if isinstance(body, bytes) else body)
return result.get("response", result.get("result", str(result)))
```
Passing `"DEFAULT"` as the qualifier calls the live version. To pin to a specific version (staging pin, canary, or rollback), pass a numeric version string instead — see [`agents-deploy/references/versioning.md`](../../agents-deploy/references/versioning.md) for the full workflow.
**For Strands**, register it as a `@tool`:
```python
from strands import Agent, tool
@tool
def delegate_to_specialist(task: str) -> str:
"""
Delegate a complex analysis task to the specialist agent.
Use this when the task requires deep domain expertise.
Args:
task: The specific task or question for the specialist.
Returns:
The specialist's detailed response.
"""
return call_specialist(task)
@app.entrypoint
def invoke(payload, context):
agent = Agent(
model=load_model(), # scaffolded by `agentcore create`
system_prompt="""You are an orchestrator. For complex analysis tasks,
delegate to the specialist using the delegate_to_specialist tool.
Synthesize the specialist's response for the user.""",
tools=[delegate_to_specialist],
)
result = agent(payload.get("prompt", ""))
return {"response": str(result)}
if __name__ == "__main__":
app.run()
```
**For LangGraph**, add it as a tool node:
```python
from langchain_core.tools import tool as lc_tool
@lc_tool
def delegate_to_specialist(task: str) -> str:
"""Delegate complex tasks to the specialist agent."""
return call_specialist(task)
# Add to your LangGraph tool node
tools = [delegate_to_specialist]
tool_node = ToolNode(tools)
llm_with_tools = llm.bind_tools(tools)
```
**For OpenAI Agents SDK**, register as a `@function_tool`:
```python
from agents import Agent, Runner, function_tool
@function_tool
def delegate_to_specialist(task: str) -> str:
"""Delegate a complex analysis task to the specialist agent.
Use when the task requires deep domain expertise."""
return call_specialist(task)
@app.entrypoint
async def invoke(payload, context):
agent = Agent(
name="Orchestrator",
instructions="For complex analysis, delegate to the specialist using delegate_to_specialist. Synthesize the response for the user.",
tools=[delegate_to_specialist],
)
result = await Runner.run(agent, payload["prompt"])
return {"response": result.final_output}
```
**For Google ADK**, pass as a plain function in the agent's `tools=[]` list. Note: the official samples use A2A for ADK multi-agent patterns (see `awslabs/agentcore-samples/02-use-cases/A2A-multi-agent-incident-response/host_adk_agent/`). The direct-invocation pattern below is extrapolated from the ADK base template — validate against your ADK version before relying on it in production:
```python
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
def delegate_to_specialist(task: str) -> str:
"""Delegate complex analysis to the specialist agent."""
return call_specialist(task)
agent = Agent(
model="gemini-2.5-flash",
name="orchestrator",
description="Orchestrator that delegates complex tasks to specialists.",
instruction="For complex analysis, call delegate_to_specialist and synthesize the response.",
tools=[delegate_to_specialist],
)
@app.entrypoint
async def invoke(payload, context):
user_id = payload.get("user_id", "default_user")
session_id = getattr(context, "session_id", "default_session")
session_service = InMemorySessionService()
session = await session_service.create_session(
app_name="orchestrator", user_id=user_id, session_id=session_id
)
runner = Runner(agent=agent, app_name="orchestrator", session_service=session_service)
content = types.Content(role="user", parts=[types.Part(text=payload["prompt"])])
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
if event.is_final_response():
return {"response": event.content.parts[0].text}
```
For a validated ADK multi-agent pattern, use A2A instead of direct invocation — see the A2A section below and the sample linked above.
**For Claude Agent SDK:** See [`awslabs/agentcore-samples/03-integrations/agentic-frameworks/claude-agent/claude-sub-agents/`](https://github.com/awslabs/agentcore-samples/tree/main/03-integrations/agentic-frameworks/claude-agent/claude-sub-agents) for the official sub-agent pattern. This plugin doesn't ship a Claude SDK delegation pattern because the sample is more current than anything we could extrapolate.
### Step 3: Grant IAM permission
The orchestrator's execution role needs permission to invoke the specialist:
```json
{
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<YOUR_ACCOUNT_ID>:runtime/SpecialistAgent-*"
}
```
Add this to `agentcore/agentcore.json` under the orchestrator agent's IAM config, or add it manually to the auto-created execution role after deploy.
### Step 4: Pass the specialist ARN at deploy time
Add the specialist ARN as an environment variable in the orchestrator's deployment:
```bash
# After deploying the specialist, get its ARN:
SPECIALIST_ARN=$(agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn')
# For local dev, write to .env.local:
echo "SPECIALIST_AGENT_ARN=$SPECIALIST_ARN" >> agentcore/.env.local
```
**For the deployed orchestrator**, the specialist ARN needs to be available as an environment variable. The recommended pattern is:
1. **Edit `agentcore/agentcore.json`** — find the orchestrator agent's entry and add the env var to its configuration (the exact field name depends on your CLI version; run `agentcore validate` after editing). In current CLI versions, agent environment variables are typically managed through the deployment config.
2. **Or use CDK overrides** — for teams using the CDK constructs directly, set the env var in the Runtime construct's environment property.
3. **Or write the env var at deploy time** — some teams use a pre-deploy script that generates `agentcore/.env.local` and `agentcore/agentcore.json` updates together:
```bash
# pre-deploy.sh — run before every orchestrator deploy
SPECIALIST_ARN=$(agentcore status --runtime SpecialistAgent --json | jq -r '.runtimes[0].arn')
echo "SPECIALIST_AGENT_ARN=$SPECIALIST_ARN" >> agentcore/.env.local
# Then deploy
agentcore deploy -y
```
The CLI does not currently provide a dedicated `--env` flag on `agentcore add agent`. Check `agentcore add agent --help` for the current options in your CLI version.
---
## Pattern 2: A2A protocol
The specialist exposes the A2A standard — discoverable via an agent card, callable via JSON-RPC. AgentCore's A2A runtime handles the HTTP server, port binding (9000), and agent card serving for you.
### Step 1: Build the A2A specialist
Use the `serve_a2a` helper from `bedrock-agentcore` — this matches what the CLI scaffolds via `agentcore create --protocol A2A`.
```python
# app/SpecialistA2A/main.py
from strands import Agent, tool
from strands.multiagent.a2a.executor import StrandsA2AExecutor
from bedrock_agentcore.runtime import serve_a2a
from model.load import load_model
@tool
def analyze_data(dataset_name: str) -> str:
"""Run detailed analysis on the named dataset."""
# Your specialist logic here
return f"Analysis results for {dataset_name}..."
agent = Agent(
model=load_model(),
system_prompt="You are an analysis specialist. Use tools when appropriate.",
tools=[analyze_data],
)
if __name__ == "__main__":
serve_a2a(StrandsA2AExecutor(agent))
```
```
# requirements.txt
strands-agents[a2a]
bedrock-agentcore
```
`serve_a2a` handles port 9000 binding, agent card generation at `/.well-known/agent-card.json`, and JSON-RPC routing automatically. No FastAPI or uvicorn needed.
### Step 2: Deploy the A2A specialist
```bash
agentcore create --name SpecialistA2A --protocol A2A
# The CLI scaffolds app/SpecialistA2A/main.py with the serve_a2a pattern shown above — customize it with your specialist logic
agentcore deploy -y
```
After deploy, get the runtime URL:
```bash
agentcore fetch access --name SpecialistA2A --type agent
```
### Step 3: Test locally
```bash
# Start the A2A server locally (from your project dir)
agentcore dev
# Test the agent card (discovery)
curl http://localhost:9000/.well-known/agent-card.json | jq .
# Send a message
curl -X POST http://localhost:9000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-001",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "What is 42 * 17?"}],
"messageId": "msg-001"
}
}
}' | jq .
```
### Step 4: Call the A2A specialist from the orchestrator
The specialist URL is a non-secret identifier, so pass it via an env var in the orchestrator's deployment config. The bearer token **is** a secret — do **not** stash it in `os.getenv(...)` on the deployed runtime (runtime env vars are not vault-backed). Register an OAuth M2M provider once, then use `@requires_access_token` to fetch a fresh token at call time:
```bash
# One-time: register the OAuth provider that issues tokens for the specialist.
# Omit --client-secret to get an interactive prompt (value goes straight into the credential provider).
agentcore add credential \
--name SpecialistA2A \
--type oauth \
--discovery-url https://<YOUR_IDP>/.well-known/openid-configuration \
--client-id <CLIENT_ID> \
--scopes a2a.invoke
```
```python
import asyncio
import os
from uuid import uuid4
import httpx
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.types import Message, Part, Role, TextPart
from bedrock_agentcore.identity.auth import requires_access_token
# Non-secret identifier — fine to pull from the environment.
SPECIALIST_URL = os.getenv("SPECIALIST_A2A_URL")
@requires_access_token(
provider_name="SpecialistA2A",
scopes=["a2a.invoke"],
auth_flow="M2M",
)
async def call_a2a_specialist(message: str, *, access_token: str) -> str:
session_id = str(uuid4())
headers = {
"Authorization": f"Bearer {access_token}",
"X-Amzn-Bedrock-AgentCore-Runtime-Session-Id": session_id,
}
async with httpx.AsyncClient(timeout=300, headers=headers) as http_client:
resolver = A2ACardResolver(httpx_client=http_client, base_url=SPECIALIST_URL)
agent_card = await resolver.get_agent_card()
config = ClientConfig(httpx_client=http_client, streaming=False)
client = ClientFactory(config).create(agent_card)
msg = Message(
kind="message",
role=Role.user,
parts=[Part(TextPart(kind="text", text=message))],
message_id=uuid4().hex,
)
async for event in client.send_message(msg):
if hasattr(event, "parts"):
return " ".join(p.text for p in event.parts if hasattr(p, "text"))
return ""
# Use in your orchestrator's entrypoint:
@app.entrypoint
def invoke(payload, context):
result = asyncio.run(call_a2a_specialist(payload.get("prompt", "")))
return {"response": result}
```
The decorator handles caching and refresh. For local dev, put the OAuth values in `agentcore/.env.local` so `agentcore dev` can resolve the decorator — the deployed runtime reads them from the credential provider instead.
---
## Shared memory across agents
Memory is a top-level resource — not nested under a single agent. Multiple agents can share it by reading the same env var.
### Setup
1. Create one shared memory resource:
```bash
agentcore add memory --name SharedMemory --strategies SEMANTIC,USER_PREFERENCE
```
1. In each agent's code, read the same env var:
```python
MEMORY_ID = os.getenv("MEMORY_SHAREDMEMORY_ID")
```
1. Use a consistent `actor_id` scheme — typically the end user's ID — so both agents read and write the same user's memory.
### Key consideration
When multiple agents share memory, they share the same namespace. Use namespaced paths to avoid collisions:
```python
# Orchestrator writes to /orchestrator/ namespace
memory_client.create_event(
memory_id=MEMORY_ID,
actor_id=user_id,
session_id=session_id,
messages=[("User asked about X", "user")],
)
# Specialist reads from all namespaces
turns = memory_client.get_last_k_turns(
memory_id=MEMORY_ID,
actor_id=user_id,
session_id=session_id,
k=5,
)
```
---
## Troubleshooting
**A2A server not responding:**
- Verify it's running on port 9000 (not 8080)
- Check the agent card endpoint returns: `curl http://localhost:9000/.well-known/agent-card.json`
- Verify your `main.py` uses `serve_a2a(StrandsA2AExecutor(agent))` — the older `A2AServer + FastAPI` pattern is deprecated in favor of this
**Direct invocation permission denied:**
- Check the orchestrator's execution role has `bedrock-agentcore:InvokeAgentRuntime`
- Verify the resource ARN pattern matches the specialist's ARN
- IAM changes take ~30 seconds to propagate
**Specialist not found:**
- Verify `SPECIALIST_AGENT_ARN` env var is set correctly
- Check `agentcore status --runtime SpecialistAgent` shows `deployed` state
**A2A auth errors:**
- A2A supports SigV4 and OAuth 2.0 — make sure you're using the right auth method
- Get the correct bearer token: `agentcore fetch access --name SpecialistA2A --type agent`
## Output
- Decision tree to choose the right pattern
- Complete code for the chosen pattern (orchestrator + specialist)
- IAM policy for agent-to-agent invocation
- Local testing commands
## Quality criteria
- Pattern recommendation matches the developer's latency and interoperability needs
- Generated code includes correct IAM permissions for agent-to-agent invocation
- A2A server runs on port 9000 (not 8080) using `serve_a2a(StrandsA2AExecutor(agent))`
- Agent card is at `/.well-known/agent-card.json` with correct capabilities
- Shared memory uses consistent `actor_id` scheme across agents
references/payments.md
# payments
Add AgentCore Payments to your agent — the managed service that lets your agent pay for x402- and MPP-protected APIs, MCP tools, and web content via microtransactions (Coinbase CDP, Stripe Privy).
AgentCore Payments is **protocol-agnostic**: it supports both **x402** (Coinbase/Cloudflare's HTTP-native stablecoin micropayment protocol) and **MPP** (the Machine Payments Protocol from Stripe and Tempo). Both are exercised through the same `ProcessPayment` API and the same manager/connector/instrument/session resources — the service detects which protocol a merchant speaks from its `402 Payment Required` response and mints the matching payment proof. You do not pick a protocol up front; you provision payments once and the agent can pay either kind of merchant. See **How x402 Payment Works** and **MPP (Machine Payments Protocol)** below for the two wire flows.
The control-plane resources (payment manager, connector, credential provider) are provisioned with the AgentCore **CLI**. The per-user data-plane resources (instrument, session) are created with the AgentCore **SDK** (a provided script). Payments can be wired into the agent in two ways: (1) a **framework-native integration** for Strands (plugin) or LangGraph (middleware) that handles 402 detection, payment signing, and retry transparently — no custom tool code needed, or (2) a **framework-agnostic local tool** (`scripts/process_payment_tool.py`) for any other Python framework (OpenAI Agents SDK, CrewAI, etc.) or when you need full manual control.
## When to use
- You want your agent to autonomously pay for x402- or MPP-protected content (APIs, MCP tools, paywalled sites)
- A tool call returns `402 Payment Required` and you want it settled and retried automatically
- You have a payment manager and need to wire payments into your agent code
- You want budget controls on what the agent can spend
- Payment processing isn't working as expected
Do NOT use this skill for:
- Connecting to non-paid external tools/APIs via Gateway → use `agents-connect`
- Inbound auth (who can call your agent) → use `agents-harden`
- General agent scaffolding or project creation
- Non-payment related agent capabilities (memory, VPC, multi-agent)
## Input
`$ARGUMENTS` is optional: `/payments`, `/payments wire`, `/payments debug`, `/payments coinbase`, `/payments stripe`.
## Process
**Execution model — minimize human stops.** Run the steps yourself, in order, without pausing between them. There are only **two** points that require the developer; pause at these and resume automatically once the developer confirms:
- **Step 3b (connector credentials)** — for **Coinbase QuickCreate** (recommended) the developer authorizes through Coinbase in the browser — no secrets; for **Manual** (Coinbase or Stripe) the developer runs the connector command with their secrets. Present the path, then wait for them to confirm the connector is `READY`.
- **Step 7 (delegation + funding)** — the developer authorizes the wallet and funds it (browser + faucet). Surface the instructions, then wait.
Everything else — Steps 0–3a, **4 (deploy), 5 (wire), 6 (instrument/session), 8 (set env + test)** — you run automatically. After the developer confirms 3b, ask them for the **user id** and **email** for the first wallet (Step 6 needs them), then immediately continue through 4 → 5 → 6 (and present Step 7) without asking permission for each. After they confirm 7, run Step 8. Do not stop after every step.
### Step 0: Install / verify the AgentCore CLI
The CLI is the **npm** package `@aws/agentcore` (Node.js 20+). It is NOT a pip package — do not `pip install` it.
```bash
agentcore --version # need >= 0.20.0 (payment commands are preview, added in 0.20.x)
# if missing or older:
npm install -g @aws/agentcore
```
### Step 1: Have an AgentCore project (for CLI provisioning)
The CLI provisions payment resources into a project (`agentcore/agentcore.json`).
- **Project exists**: read `agentcore/agentcore.json` — check the `payments` array and the `runtimes` array (framework).
- **No project**: scaffold one (don't call `--help`; run it directly). Non-interactive:
```bash
agentcore create --project-name <ProjectName> --name <AgentName> --framework Strands --defaults
```
`--project-name` and `--name` are both required non-interactively (`--name` is the agent/resource name; without it the CLI drops to the interactive wizard). Project name: start with a letter, alphanumeric, ≤23 chars, no underscores. `--defaults` = Python + Bedrock, no memory; or run `agentcore create` for the interactive wizard. A project is only needed to provision the payment resources via the CLI — the local payment tool (Step 5) works in any agent, framework, or host.
### Step 2: Determine the situation
- **Case A — nothing configured**: proceed to Step 3.
- **Case B — manager/connector exist, needs wiring**: skip to Step 5.
- **Case C — wired, debugging**: ask what's failing, then use the Debugging section.
- **Case D — developer asking about payments without a project** (architecture, flow explanation): explain the end‑to‑end payment flow (see **How x402 Payment Works** for x402, and **MPP (Machine Payments Protocol)** for MPP), and ask whether they want to set up payments (→ proceed to Step 3) or need wiring help (→ Step 5).
**Framework check**: If the project uses **Strands** or **LangGraph** (check `agentcore/agentcore.json` → `runtimes` array), offer the native integration path (Step 5a) which is simpler — no custom tool script needed. If the project uses another framework, or the developer wants manual control, use the generic tool path (Step 5b).
**Provider & credential mode**: For **Coinbase**, prefer **QuickCreate** (Step 3b, recommended) — you authorize through Coinbase and the service provisions the credentials, so there are no secrets to gather or store. Choose **Manual** only if you already manage your own Coinbase keys. **Stripe (Privy) is manual-only.**
### Step 3: Provision the payment manager and connector (CLI — control plane)
**3a. Payment manager — no secrets, run it directly (non-interactive).** The agent can run this for the developer:
```bash
agentcore add payment-manager \
--name <ManagerName> \
--network-preferences eip155:84532
```
`eip155:84532` is Base Sepolia (testnet). Names: alphanumeric + underscores, ≤48 chars, start with a letter.
**Then tag the project as skill-onboarded — this step is mandatory, not optional.** The agent MUST edit `agentcore/agentcore.json` and add the `agentcore:onboarding-source` entry to the top-level `tags` object (keep the existing entries) before moving on to Step 3b. If the `tags` object does not exist yet, create it with the entries shown:
```json
"tags": {
"agentcore:created-by": "agentcore-cli",
"agentcore:project-name": "<existing value>",
"agentcore:onboarding-source": "agent-toolkit-skill"
}
```
Project tags are applied to the provisioned AWS resources at deploy. The `agentcore:onboarding-source` tag lets the AgentCore Payments service distinguish resources onboarded through this skill from resources provisioned with the CLI directly — set it exactly as shown. **This tag is required: never skip it, and do not proceed to `agentcore deploy` (Step 4) without it** — resources deployed without the tag are indistinguishable from direct-CLI provisioning and defeat the purpose of onboarding through this skill.
**3b. Payment connector — choose a credential mode.** There are two ways to supply the connector's credentials:
- **Coinbase — QuickCreate (recommended):** you authorize through Coinbase and AgentCore Payments provisions and stores the credentials for you — no keys to generate or paste. **Coinbase only.**
- **Manual (Coinbase CDP or Stripe Privy):** you generate the provider keys yourself and pass them to the connector. **This is the only path for Stripe (Privy).**
**Coinbase — QuickCreate (recommended). No secrets, so the agent can run this directly.** Prerequisite: an AWS Marketplace subscription to **"Coinbase Wallets for AgentCore Payments"**. Because no credentials are entered, nothing sensitive lands in the command, shell history, or `agentcore/.env.local`, and you skip the "get your provider credentials" step below.
```bash
agentcore add payment-connector \
--manager <ManagerName> \
--name <ConnectorName> \
--provider CoinbaseCDP \
--provision-mode QUICK_CREATE
```
This records a QuickCreate Coinbase connector locally with **no secrets**. When the connector is created at `agentcore deploy` (Step 4), the CLI opens the Coinbase authorization flow in your browser; the developer signs in and authorizes, and the connector moves `PENDING_AUTHENTICATION` → `READY` — there is no API Key ID, API Key Secret, or Wallet Secret to obtain or store. Present the command (the agent may run it — no secrets are involved), then have the developer complete the browser authorization at deploy and confirm the connector is `READY` before continuing. (Driving the API directly instead of the CLI: pass `provisionMode=QUICK_CREATE` with an empty `credentialProviderConfigurations` list — AWS CLI `--provision-mode QUICK_CREATE --credential-provider-configurations '[]'` — then open the returned `authorizationUrl` and poll `get-payment-connector` until `READY`.)
**Handling the `authorizationUrl` (short-lived + single-use).** If the connector is created via the API/SDK — or the agent surfaces the URL to the developer instead of the CLI opening the browser itself — treat the `authorizationUrl` returned for the `PENDING_AUTHENTICATION` connector carefully:
- **Valid for 10 minutes** after the connector is created, then it expires — opening a stale URL returns an "Invalid request"/expired error at Coinbase. Open it promptly.
- **Open it exactly once, directly in a browser.** It carries a one-time OAuth consent session. Do NOT paste it anywhere that auto-previews or "unfurls" links (Slack, Teams, other chat tools), and the agent must NOT fetch or open it — a link-preview fetch can consume the one-time session, so the developer's later click fails with "Invalid request". Share it as plain/code text and have the developer open it.
- **Poll `GetPaymentConnector` until the status is terminal — do not reopen the URL to check.** After the developer authorizes, poll the connector's `status` until it reaches one of `READY`, `AUTHENTICATION_EXPIRED`, or `AUTHENTICATION_FAILED` (space the calls out, e.g. every few seconds). While it is still `PENDING_AUTHENTICATION`, consent has not completed — keep polling.
- **`READY`** — done. The credential provider is provisioned and the connector is ready to use; no further action.
- **`AUTHENTICATION_EXPIRED` / `AUTHENTICATION_FAILED`** — the OAuth consent lapsed (the 10-minute window passed) or failed. The connector cannot be recovered in place, so stop polling it and **ask the developer to replace it**: delete the expired/failed connector and recreate it by restarting QuickCreate (which mints a fresh `authorizationUrl`).
```bash
# Poll the connector status until READY / AUTHENTICATION_EXPIRED / AUTHENTICATION_FAILED
# (also returns a still-valid authorizationUrl while PENDING_AUTHENTICATION):
aws bedrock-agentcore-control get-payment-connector \
--payment-manager-id "<PAYMENT_MANAGER_ID>" \
--payment-connector-id "<PAYMENT_CONNECTOR_ID>" \
--region <AWS_REGION>
```
If the status is `AUTHENTICATION_EXPIRED` or `AUTHENTICATION_FAILED`, replace the connector — delete it, then restart QuickCreate:
```bash
# Delete the expired/failed connector, then re-run the QuickCreate command above to mint a fresh authorizationUrl.
agentcore remove payment-connector --manager <ManagerName> --name <ConnectorName> --yes
agentcore deploy
# then re-run: agentcore add payment-connector … --provider CoinbaseCDP --provision-mode QUICK_CREATE (and agentcore deploy)
```
**Manual (Coinbase CDP or Stripe Privy) — needs provider credentials. The DEVELOPER runs this, not the agent.** The agent presents the prerequisites and the command below, but must NOT execute it or handle the credentials. This single command creates the credential provider and the connector. The CLI writes the provider secrets in **plaintext to `agentcore/.env.local`** and records the credential locally; `agentcore deploy` (Step 4) then uploads them to **AgentCore Identity** (`agentcore.json` keeps only a reference). For Stripe Privy, three of these values are reused later — the delegation frontend in Step 7b reads them back out of `agentcore/.env.local`, so the developer is never asked for them twice. Note the CLI namespaces each key as `AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_<FIELD>`, using the manager and connector names chosen below; Step 7b matches on the `_STRIPE_PRIVY_<FIELD>` suffix for that reason.
**Before running — get your provider credentials** (do this first; the connector command needs them). These match the exact locations in the [AgentCore Payments prerequisites](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-prerequisites.html).
- **Coinbase CDP** (<https://portal.cdp.coinbase.com/>):
1. Create or log in to a Coinbase Developer Platform account and project.
2. Generate an **API key** (or reuse one) at <https://portal.cdp.coinbase.com/api-keys/secret> and note two values:
- **API Key ID** — the public identifier for your CDP project.
- **API Key Secret** — the private secret used to sign API requests to the CDP control plane.
3. Under **Project > Wallets > Non-custodial Wallet > Security**, generate a **Wallet Secret** — used for cryptographic wallet operations such as deriving addresses and signing transactions.
4. In the same place (**Project > Wallets > Non-custodial Wallet > Security**), **enable Delegated signing** (required).
- **Stripe Privy** (<https://dashboard.privy.io/>):
1. Create a **dedicated** Privy app for AgentCore operations (do not reuse apps that serve other purposes).
2. In **App settings > Basics > API Keys**, copy the **App ID** and **App Secret**.
3. In **Wallet Infrastructure > Keys and quorums**, choose **New Key** to generate a P-256 key pair, and note two values:
- **Authorization ID (ID)** — the public key identifier from the generated pair.
- **Authorization Private Key (Private key)** — the private key from the generated pair, used for signing wallet operations.
Recommended — interactive wizard. Run the command with **no flags** (the secrets never appear in the command, shell history, or process list; the CLI still writes them to `agentcore/.env.local` either way — see the security note below). Passing `--manager`/`--name`/`--provider` does NOT trigger the wizard — those flags switch the CLI to non-interactive mode and it then requires every secret flag too, failing with "Missing required options" otherwise:
```bash
agentcore add payment-connector
# the wizard prompts for everything interactively — manager, connector name, provider, then the secrets:
# CoinbaseCDP : API Key ID, API Key Secret, Wallet Secret
# StripePrivy : App ID, App Secret, Authorization Private Key, Authorization ID
```
Non-interactive alternative (CI/scripted) — pass the secrets as flags. These land in shell history and the process list, so prefer the wizard for local setup:
```bash
# Coinbase CDP (dummy values — replace with your own)
agentcore add payment-connector --manager <ManagerName> --name <ConnectorName> --provider CoinbaseCDP \
--api-key-id 11111111-2222-3333-4444-555555555555 \
--api-key-secret cdp_sk_EXAMPLEexampleEXAMPLEexampleEXAMPLE0000 \
--wallet-secret cdp_wallet_EXAMPLEexampleEXAMPLEexample1111
# Stripe Privy (dummy values — replace with your own)
agentcore add payment-connector --manager <ManagerName> --name <ConnectorName> --provider StripePrivy \
--app-id clxxxxxxxxxxxxxxxxxxxxxxxx \
--app-secret privy_sk_EXAMPLEexampleEXAMPLEexample2222 \
--authorization-private-key MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwEXAMPLE... \
--authorization-id ezzzzzzzzzzzzzzzzzzzzzzzz
```
> **Wizard vs flags:** The flags `--manager`, `--name`, and `--provider` are marked `[non-interactive]` — if you provide any of them, the CLI switches to **non-interactive mode** and expects **all required secrets as flags**. Running it with those three flags but omitting the secrets errors with missing-required-flags rather than dropping back to the wizard. For the interactive wizard, run the command with no flags: `agentcore add payment-connector`. Then wait for the developer to confirm it's done.
Security:
- **QuickCreate stores no secrets locally.** With Coinbase QuickCreate there are no provider keys to generate, paste, or store — AgentCore Payments provisions and holds the credential provider for you, so nothing lands in `agentcore/.env.local`. The bullets below apply to the **Manual** path.
- **`agentcore/.env.local` holds the provider secrets in plaintext.** The CLI writes it when the connector is added (wizard or flags) and uploads it to AgentCore Identity at `agentcore deploy`. Ensure it is gitignored — the Python scaffold's default `.gitignore` only lists `.env`, so add `.env.local` (or `.env.*`). The agent must not read `agentcore/.env.local` — where Step 7b needs values from it, the developer copies them across.
- The agent presents the command but never runs it or handles the credentials; never paste credentials into chat.
### Step 4: Deploy (create the resources) — agent runs
```bash
agentcore deploy -y
```
`agentcore deploy` provisions the project's resources to your AWS account: the payment manager/connector via the AgentCore control plane, and supporting IAM (the `Payment<Name>ProcessPaymentRole`) and any runtime via a CloudFormation stack (CDK). **Coinbase QuickCreate:** if you added the connector with `--provision-mode QUICK_CREATE`, deploy is when it is created — the CLI opens the Coinbase authorization flow in your browser; after the developer authorizes, the connector moves `PENDING_AUTHENTICATION` → `READY` (this is the developer-involved point from Step 3b). After deploy, the manager ARN, connector ID, and role ARN are written to `agentcore/.cli/deployed-state.json`. On CLI 0.20.x these live under `targets.<target>.resources.payments[]` (`managerArn`, `connectors[].connectorId`, `processPaymentRoleArn`); the Step 6 script reads this shape automatically.
### Step 5: Wire the agent
#### Step 5a: Native integration (Strands or LangGraph) — agent runs
If the project uses Strands or LangGraph, use the framework's native payments integration. This is simpler than the generic tool — no `process_payment_tool.py` needed, no `x402_fetch` registration, and the middleware/plugin automatically handles ALL tool calls (not just a dedicated payment tool).
**Strands:**
```python
from strands import Agent
from strands_tools import http_request
from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
from bedrock_agentcore.payments.integrations.strands.plugin import AgentCorePaymentsPlugin
config = AgentCorePaymentsPluginConfig(
payment_manager_arn=os.environ["PAYMENT_MANAGER_ARN"],
user_id=os.environ["PAYMENT_USER_ID"],
payment_instrument_id=os.environ["PAYMENT_INSTRUMENT_ID"],
payment_session_id=os.environ["PAYMENT_SESSION_ID"],
region=os.environ.get("AWS_REGION", "us-west-2"),
)
plugin = AgentCorePaymentsPlugin(config=config)
agent = Agent(
system_prompt="You are a helpful assistant that can access paid APIs.",
tools=[http_request],
plugins=[plugin],
)
```
The plugin intercepts 402 responses from ANY tool, signs payment, and retries automatically. No special tool needed — the agent just uses `http_request` normally.
**LangGraph:**
```python
from langchain.agents import create_agent
from bedrock_agentcore.payments.integrations.langgraph import (
AgentCorePaymentsConfig,
AgentCorePaymentsMiddleware,
)
# Choose ONE of the following configurations
# Option A: explicit session (production)
config = AgentCorePaymentsConfig(
...
payment_session_id=os.environ["PAYMENT_SESSION_ID"],
)
# Option B: auto-session (dev/test convenience)
config = AgentCorePaymentsConfig(
...
auto_session=True,
auto_session_budget="5.00",
auto_session_expiry_minutes=60,
)
payments = AgentCorePaymentsMiddleware(config)
agent = create_agent(
model=model,
tools=[], # middleware auto-registers http_request + payment query tools
middleware=[payments],
)
```
The middleware wraps ALL tool calls, detects 402 from any response format (no `PAYMENT_REQUIRED:` marker needed), signs payment, and retries. It also auto-registers an `http_request` tool and payment query tools.
**LangGraph simplifications vs the generic tool path:**
- No `process_payment_tool.py` script needed — the middleware IS the payment tool
- No special system prompt — no need to tell the model to use a specific tool for paid URLs; all tools are payment-aware
- `auto_session=True` can lazily create a session on first 402 (dev/test convenience — requires `CreatePaymentSession` IAM permission on the runtime role)
- Error recovery — optional `on_payment_error` callback for programmatic recovery (create new session, swap instrument) without the LLM seeing errors
> **Note on `auto_session`**: This creates exactly one session per middleware instance with the developer-set budget. The LLM cannot trigger or control this. In production with IAM role separation (recommended ProcessPaymentRole), the `CreatePaymentSession` call would be denied — use explicit `payment_session_id` instead. See [IAM roles for AgentCore payments](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-iam-roles.html).
#### Step 5b: Framework-agnostic local tool (any framework) — agent runs
Payments are wired with a small local tool, not a framework-specific plugin — so the same code works in any framework.
1. **Copy [`scripts/process_payment_tool.py`](../scripts/process_payment_tool.py) into the agent project.** It exposes `x402_fetch(url, method="GET")`, which on a `402` calls the SDK's `PaymentManager.generate_payment_header` — the SDK validates the 402, selects the network, processes the payment, and builds the version-aware proof (v1 `X-PAYMENT` / v2 `PAYMENT-SIGNATURE`) — then retries with a fresh client. Base Sepolia settlement is intermittently transient (the header is valid but the paid retry still returns 402), so the tool re-runs the settle+replay flow up to `X402_MAX_PAYMENT_ATTEMPTS` times (default 5, env-overridable) before giving up. It reuses a single idempotency token across those retries, so `ProcessPayment` stays idempotent — every attempt replays the same on-chain authorization/nonce and the user is never charged twice (a retry either settles the not-yet-settled payment or, if it was already settled, reverts on-chain). It reads its config from environment variables (set in Step 8): `PAYMENT_MANAGER_ARN`, `PAYMENT_INSTRUMENT_ID`, `PAYMENT_SESSION_ID`, `PAYMENT_USER_ID`, `AWS_REGION`.
2. **Register `x402_fetch` as a tool** in the agent's framework. The tool function is identical; only the registration decorator differs:
```python
# Strands
from strands import Agent, tool
from process_payment_tool import x402_fetch as _x402
x402_fetch = tool(_x402)
agent = Agent(model=..., tools=[x402_fetch], system_prompt="... use x402_fetch for paid URLs ...")
```
```python
# LangGraph
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from process_payment_tool import x402_fetch as _x402
graph = create_react_agent(model, tools=[tool(_x402)])
```
```python
# OpenAI Agents SDK
from agents import Agent, function_tool
from process_payment_tool import x402_fetch as _x402
agent = Agent(name="PaymentAgent", tools=[function_tool(_x402)], instructions="... use x402_fetch for paid URLs ...")
```
For any other framework, register `x402_fetch` using that framework's tool mechanism — the function is plain Python.
The agent calls `x402_fetch` instead of a generic HTTP tool; payment is handled inside the tool. (Tell the model, via the system prompt, to use `x402_fetch` for URLs that may require payment.)
### Step 6: Provision the per-user instrument and session (SDK script — data plane) — agent runs
The instrument (per-user wallet) and session (budget-bounded spend window) are data-plane resources — there is no CLI command for them. First ask the developer for the **user id** and **email** to provision the wallet for (if not already collected after Step 3b). Then run the provided script [`scripts/setup_payment_user.py`](../scripts/setup_payment_user.py) once per user. It auto-reads the manager ARN/connector ID from `deployed-state.json` (or accepts `--manager-arn`/`--connector-id`):
```bash
python scripts/setup_payment_user.py --user-id alice --email alice@example.com --budget 5
```
It creates the instrument (with the email in `linkedAccounts`) and a budget-bounded session, then prints the `export` lines for `PAYMENT_INSTRUMENT_ID` / `PAYMENT_SESSION_ID` / `PAYMENT_USER_ID` (used in Step 8), plus the `wallet_address` and `redirect_url` (used in Step 7). The script is the canonical data-plane path — do not hand-write the SDK calls.
**LangGraph with `auto_session=True`**: If you used Step 5a with LangGraph and set `auto_session=True`, you only need the instrument from this step — skip the session creation. The middleware creates a session automatically on the first 402. You still need to run `setup_payment_user.py` for the instrument (do NOT use the --budget flag as that will create a session).
### Step 7: Delegation and funding (one-time per wallet) — developer does this
Using the `wallet_address` / `redirect_url` the script printed:
1. **Delegation** — authorize the agent to spend from the wallet.
- **Coinbase CDP**: the end user visits `redirect_url`, logs in, and grants permissions to `wallet_address`.
- **Stripe Privy**: Delegation requires a frontend app where the end user authenticates with Privy and approves the agent as a session signer on their wallets. Use the reference frontend at <https://github.com/privy-io/aws-agentcore-sdk>. The agent automates the mechanical parts (7a, 7c, 7f); the developer handles the two steps that touch secrets or the dashboard (7b, 7d) and the browser approval (7e). No credential is requested twice — 7b reuses what Step 3b already captured:
**7a. Clone and install the delegation frontend — agent runs:**
```bash
git clone https://github.com/privy-io/aws-agentcore-sdk.git agentcore-privy-frontend
cd agentcore-privy-frontend
pnpm install
```
**7b. Configure environment — reuse the Step 3b credentials. The DEVELOPER runs this, not the agent.**
**Do not ask the developer to re-provide the Privy credentials.** All three were already captured when the payment connector was created in Step 3b, and the CLI wrote them to `agentcore/.env.local` in the agent project.
**The connector's key names are namespaced — match on the suffix, not the full name.** `agentcore add payment-connector` does not write bare `STRIPE_PRIVY_*` keys. It writes one key per secret in the form:
```text
AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_<FIELD>
```
`<MANAGER>` and `<CONNECTOR>` are the names the developer chose in Step 3b, so the fully-qualified key names are different in every project. Never search for a hardcoded full name — match on the `_STRIPE_PRIVY_<FIELD>` **suffix**:
| Frontend `.env.local` variable | Suffix to match in `agentcore/.env.local` | Visibility |
|----------|--------|------------|
| `NEXT_PUBLIC_PRIVY_APP_ID` | `*_STRIPE_PRIVY_APP_ID` | Public (client) |
| `PRIVY_APP_SECRET` | `*_STRIPE_PRIVY_APP_SECRET` | Server-only |
| `NEXT_PUBLIC_PRIVY_SIGNER_ID` | `*_STRIPE_PRIVY_AUTHORIZATION_ID` | Public (identifier only) |
| `NEXT_PUBLIC_NETWORK_MODE` | not a connector credential — set `testnet` for Base Sepolia / Solana Devnet, `mainnet` for production | Public |
The frontend does **not** need the authorization private key — only the connector signs, so `*_STRIPE_PRIVY_AUTHORIZATION_PRIVATE_KEY` stays where it is.
`PRIVY_APP_SECRET` is a real secret and `agentcore/.env.local` holds it in plaintext, so the **developer** runs the commands below — the agent must not read that file (same rule as Step 3b).
First confirm the keys are there. This prints key **names** only, never a value — substitute the absolute path to the agent project:
```bash
grep -oE '^[^=]*_STRIPE_PRIVY_[A-Z_]+' /absolute/path/to/agent-project/agentcore/.env.local
```
Expect four lines ending in `_APP_ID`, `_APP_SECRET`, `_AUTHORIZATION_PRIVATE_KEY`, and `_AUTHORIZATION_ID`. If it prints nothing, skip to the dashboard fallback below.
Then write the frontend's `.env.local`. One command, absolute paths on both sides (relative paths and `cd` are what break here — the two projects are different directories), no values printed:
```bash
sed -nE 's/^[^=]*_STRIPE_PRIVY_APP_ID=/NEXT_PUBLIC_PRIVY_APP_ID=/p;s/^[^=]*_STRIPE_PRIVY_APP_SECRET=/PRIVY_APP_SECRET=/p;s/^[^=]*_STRIPE_PRIVY_AUTHORIZATION_ID=/NEXT_PUBLIC_PRIVY_SIGNER_ID=/p' /absolute/path/to/agent-project/agentcore/.env.local > /absolute/path/to/agentcore-privy-frontend/.env.local && printf 'NEXT_PUBLIC_NETWORK_MODE=testnet\n' >> /absolute/path/to/agentcore-privy-frontend/.env.local
```
It writes the file outright, so there is no need to `cp .env.example .env.local` first and no duplicate keys to reason about. Verify by listing the key names — again no values:
```bash
cut -d= -f1 /absolute/path/to/agentcore-privy-frontend/.env.local
```
All four of `NEXT_PUBLIC_PRIVY_APP_ID`, `PRIVY_APP_SECRET`, `NEXT_PUBLIC_PRIVY_SIGNER_ID`, `NEXT_PUBLIC_NETWORK_MODE` must be present. Fewer than four means a suffix didn't match — use the fallback rather than a partial file.
**Dashboard fallback.** If `agentcore/.env.local` is missing or the suffixes don't match — the connector was created on another machine, or the CLI's key format changed — fill in the values by hand instead. `cp .env.example .env.local` in the frontend, then take the App ID and App Secret from the Privy Dashboard under **Configuration > App settings**, and the signer ID from **Wallet infrastructure > Authorization keys**. It must be the **same app and same authorization key** used in Step 3b.
The `NEXT_PUBLIC_PRIVY_SIGNER_ID` is the Authorization Key ID (looks like `zr17anh9dpiqno1iaref9jpx`) — the same key whose private key went to the payment connector. It is safe to expose publicly (it's an identifier, not a secret).
> **Important:** Taking these values straight out of `agentcore/.env.local` is what guarantees the frontend uses the same Privy app and authorization key as the connector. If they are set by hand and diverge, delegation succeeds but payments fail with "Wallet policy denied the transaction."
**7c. Start the frontend — agent runs:**
```bash
pnpm dev
```
The app starts at `http://localhost:3000`. If port 3000 is occupied (e.g. by the agent's own dev server), Next.js auto-selects the next available port — **read the actual URL from the terminal output** before the next step.
**7d. Allow the local origin in the Privy Dashboard — developer does this:**
Privy restricts which origins may use an App ID from the browser. Unless the local origin is allowlisted, login in Step 7e fails client-side even though every credential is correct.
In the [Privy Dashboard](https://dashboard.privy.io/apps?setting=domains&page=settings), go to **Configuration > App settings > Domains**, and under **Allowed origins** select **Web & mobile web**, then add the URL the dev server printed:
```
http://localhost:3000
```
Requirements (Privy matches the browser's **origin**, so anything beyond scheme + host + port is rejected):
- **No trailing slash and no path** — `http://localhost:3000`, not `http://localhost:3000/`.
- **The port is required** and must be the port the dev server actually bound (Step 7c). `http://localhost` alone does not match.
- Add each port separately if the dev server moves between runs.
> **Check what's already in the field first.** Privy's default is permissive — an app with an **empty** allowed-origins list accepts every origin, so delegation works without this step. Adding the first entry switches the app to allowlist-only:
>
> - **Dedicated AgentCore app** (what Step 3b recommends): the list is normally empty and nothing else uses the App ID, so adding `http://localhost:3000` is safe. Remember to also add the deployed origin before going to production, or the hosted frontend will break.
> - **Shared app** with existing entries: append the localhost URL, don't replace the list. Remove it again once delegation testing is done.
**7e. Complete delegation — developer does this in browser:**
1. Open `http://localhost:3000` in a browser
2. Log in with the **same email** used in the `setup_payment_user.py` `--email` flag (Step 6) — Privy creates embedded wallets for this user
3. On the "Complete setup" screen, click **"Connect agent"**
4. In the modal, click **"Give access"** — this calls `addSessionSigners` which registers the authorization key as a session signer on all the user's Privy embedded wallets
5. Once the success toast appears ("Agent connected successfully"), the agent is authorized to sign transactions on behalf of this user
After delegation succeeds, the developer can optionally fund the wallet directly from the same UI (click "Add funds" > use the Circle faucet address shown, or transfer from an external wallet).
> **How it works under the hood:** The frontend calls Privy's `addSessionSigners` API with the `NEXT_PUBLIC_PRIVY_SIGNER_ID`. This adds the AgentCore authorization key as an approved signer on the user's embedded wallets. When AgentCore later calls `ProcessPayment`, it uses the corresponding private key to sign transactions — Privy's wallet infrastructure validates that the signer is authorized and executes the transaction.
**7f. Verify delegation — agent can validate:**
After the developer confirms delegation is complete, the agent can verify by calling the same check-signers endpoint the frontend uses:
```bash
curl -s -X POST http://localhost:3000/api/check-signers \
-H "Content-Type: application/json" \
-d '{"walletIds": ["<wallet-id-from-step-6>"]}' | python3 -m json.tool
```
Expected: `{"connected": true}`. If `false`, the developer needs to repeat step 7e.
**Deployed alternative:** For production, deploy the frontend (e.g. to Vercel: `vercel --prod`) and direct end users to the hosted URL. The same `.env.local` values go into Vercel's environment variables settings, and the **deployed origin must be added to Allowed origins** the same way `http://localhost:3000` was in Step 7d (`https://your-app.vercel.app`, no trailing slash). Privy does not allow generic preview-deployment wildcards like `https://*.vercel.app`, so map previews to a subdomain you control if they need to work. Each end user logs in with their own email, delegates once, and is then ready for agent-initiated payments.
2. **Funding** — send testnet USDC to `wallet_address` via the Circle faucet (<https://faucet.circle.com/>), Base Sepolia.
### Step 8: Set env vars and test — agent runs
Set the tool's config from the `export` lines the Step 6 script printed — it emits all of them (`PAYMENT_MANAGER_ARN`, `PAYMENT_INSTRUMENT_ID`, `PAYMENT_SESSION_ID`, `PAYMENT_USER_ID`, `AWS_REGION`), so just copy them into the agent's environment:
```bash
export PAYMENT_MANAGER_ARN=... # all five printed by setup_payment_user.py
export PAYMENT_INSTRUMENT_ID=...
export PAYMENT_SESSION_ID=...
export PAYMENT_USER_ID=...
export AWS_REGION=...
```
**LangGraph (Step 5a with `auto_session=True`)**: You only need these env vars:
```bash
export PAYMENT_MANAGER_ARN=...
export PAYMENT_INSTRUMENT_ID=...
export PAYMENT_USER_ID=...
export AWS_REGION=...
# PAYMENT_SESSION_ID is not needed — auto_session manages it internally
```
Run the agent and prompt it to fetch a paid endpoint:
```
Fetch https://sandbox.node4all.com/v1/x402-test and tell me what you find.
```
Run it however your agent runs — directly in your framework, or `agentcore dev` for a local server / `agentcore invoke` for the deployed runtime (set the same `PAYMENT_*` env vars on the runtime). A successful run shows `x402_fetch` hitting `402`, settling payment, and the retry returning `200`.
## The `upto` scheme and Permit2 allowance
The setup and wiring above are scheme-agnostic: the agent passes through whichever x402 scheme the merchant's `402` advertises. Most endpoints use `exact` (a fixed price known up front). Some use **`upto`**, for metered or usage-based pricing such as pay-per-inference — the agent authorizes a spending ceiling and the merchant settles the actual amount consumed, up to that ceiling. No configuration change is required to pay an `upto` endpoint.
The `upto` scheme has one additional prerequisite: it settles through the [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) contract, so the payer wallet must hold a Permit2 allowance for the asset. The optional **`permit2_allowance_limit`** field is an add-on for `upto` that grants this allowance — when set, `ProcessPayment` submits the one-time on-chain `approve` before signing. Set it on the native integration config (Step 5a):
```python
config = AgentCorePaymentsPluginConfig( # AgentCorePaymentsConfig for LangGraph
...,
permit2_allowance_limit="1000000", # decimal string, asset's smallest unit (1000000 = 1 USDC)
)
```
- A decimal string in the asset's smallest denomination. The uint256 maximum (`115792089237316195423570985008687907853269984665640564039457584007913129639935`) grants an unlimited allowance.
- Applies only to `upto`; supplying it for an `exact` payment returns a validation error.
- It broadcasts a real on-chain `approve` transaction — gas is paid from the wallet's native-token balance (not its USDC balance).
- Needed only for a wallet's first `upto` payment. `approve` sets the allowance rather than adding to it, so omit it on later calls to avoid a redundant transaction.
- Requires an SDK build whose integration config (or `generate_payment_header`) accepts `permit2_allowance_limit`.
For the generic `x402_fetch` tool (Step 5b), pass `permit2_allowance_limit="..."` to its `generate_payment_header` call when paying an `upto` endpoint.
## Debugging payments
**QuickCreate: the Coinbase authorization URL shows "Invalid request" (or does nothing):**
- The `authorizationUrl` is valid for only **10 minutes** and is **single-use** — this error means it expired, was already used, or was consumed by a link preview before you clicked it.
- **Link unfurling is the most common cause**: pasting the URL into Slack/Teams/chat (or letting the agent fetch it) fires a preview request that spends the one-time consent session. Share the URL as plain text and open it directly in a browser, once, promptly.
- Check the connector with `GetPaymentConnector` (e.g. `aws bedrock-agentcore-control get-payment-connector --payment-manager-id <id> --payment-connector-id <id> --region <AWS_REGION>`) and poll until the status is terminal: `READY` = it already succeeded (no action); `AUTHENTICATION_EXPIRED`/`AUTHENTICATION_FAILED` = the consent window lapsed or failed — delete the connector and recreate it via QuickCreate to get a fresh URL; `PENDING_AUTHENTICATION` = still waiting, keep polling.
**Agent sees 402 but does not pay:**
1. Verify `PAYMENT_MANAGER_ARN` env var is set and not None
2. Check that the agent is using `x402_fetch` tool (not a generic `http_request`)
3. Verify the x402 challenge is present in either the response body (`x402Version` + `accepts` fields) or the `payment-required` header
**ProcessPayment fails with "Failed to obtain resource payment token":**
- The IAM service role is missing permissions. Ensure it has `GetResourcePaymentToken` on the token-vault and `secretsmanager:GetSecretValue` on the secrets.
- Wait 15+ seconds after creating the role before calling ProcessPayment (IAM propagation).
**ProcessPayment fails with "Failed to obtain workload access token":**
- The service role is missing `GetWorkloadAccessToken` permission on the workload-identity-directory resources.
**ProcessPayment fails with "Failed to assume payment execution role":**
- The service role's trust policy is incorrect. Ensure it trusts `bedrock-agentcore.amazonaws.com` with the correct `aws:SourceAccount` condition.
- Verify the role ARN passed to the Payment Manager matches the actual role.
**ProcessPayment succeeds but merchant still returns 402:**
- **Transient on‑chain settlement failure** (common on Base Sepolia): the tool already re‑settles up to `X402_MAX_PAYMENT_ATTEMPTS` times (default 5). If still 402s, raise the cap (`export X402_MAX_PAYMENT_ATTEMPTS=8`) or retry shortly.
- **Cookie contamination**: The retry is sending cookies from the initial 402 request. Ensure you use a fresh httpx client: `httpx.Client(cookies=None).request(...)` — do NOT reuse the same client/session.
- **Wrong x402 version / header**: The merchant is x402 v2 but the proof was sent as v1 (or vice versa). v1 expects an `X-PAYMENT` header with a flat proof (top-level `scheme`/`network`); v2 expects a `PAYMENT-SIGNATURE` header where `accepted` is a top-level sibling of `payload`, and `payload` holds only `signature` + `authorization` (no top-level `scheme`/`network`). A v2 merchant that receives a v1 `X-PAYMENT` header ignores it and re-issues the same 402 — often with an empty `{}` body and no error, which is hard to diagnose. Read `x402Version` from the challenge (body or `payment-required` header) and build the matching proof.
- **Proof format mismatch (network field)**: For **v1**, the proof `network` must use the merchant's human label (e.g., `"base-sepolia"` not `"eip155:84532"`). For **v2**, the proof keeps the CAIP-2 identifier from the challenge unchanged (e.g., `"eip155:84532"`). Note: the `ProcessPayment` input always uses CAIP-2 regardless of version — only the proof presented to the merchant differs.
- **Proof expired**: The proof has a ~60 second validity window (`validBefore`). If the agent loop is slow, the proof may expire before the retry.
**ProcessPayment succeeds (PROOF_GENERATED) but merchant returns 402 with an empty `{}` body and no error:**
- The merchant is x402 **v2** and is ignoring the v1 `X-PAYMENT` header. Detect the version from the challenge (`x402Version: 2`, present in the body or the `payment-required` response header) and send a `PAYMENT-SIGNATURE` header. The v2 proof puts `accepted` (the full requirements, CAIP-2 network) as a top-level sibling of `payload`, with `payload` containing only `signature` + `authorization`. Note: if ProcessPayment returns `PROOF_GENERATED` and the proof shape is correct but the merchant still 402s, it may be a transient on-chain settlement failure — retry once before assuming a format problem.
**MPP: `ProcessPayment` fails with `ValidationException` mentioning gas/network fees:**
- The MPP challenge does not offer seller-sponsored fees (`methodDetails.feePayer` is `false` or absent), so AgentCore will not silently charge the buyer for network fees. Either set `paymentInput.mpp.buyerPaysGasFees: true` to authorize paying them from the buyer's wallet, or obtain a challenge whose seller sponsors fees.
**MPP: `ProcessPayment` fails with `ValidationException` on the challenge header:**
- `wwwAuthenticateHeaders` must contain the raw `WWW-Authenticate: Payment …` value **verbatim** and **exactly one** entry. Do not decode, reassemble, or re-encode it — altering the bytes breaks the challenge HMAC binding. If the `402` returned several `WWW-Authenticate: Payment` lines, send only the single option your instrument can satisfy.
- Confirm `paymentType` is `MPP` and the payload is under the `mpp` arm of `paymentInput` (not `cryptoX402`).
**MPP: agent gets a fresh `402` after retrying with the credential:**
- Attach the credential exactly as returned: `Authorization: Payment <token>`, using `paymentOutput.mpp.paymentCredential` verbatim (it already includes the `Payment` scheme prefix). Retry with a fresh HTTP client so cookies from the initial `402` are not resent.
- MPP credentials are single-use — a replay of an already-consumed `challenge.id` is rejected. Re-run `ProcessPayment` against the new challenge from the fresh `402`.
**MPP: `ProcessPayment` fails with `SubscriptionRequiredException` (403):**
- The account is not subscribed to the required AWS Marketplace offering. Follow the `subscriptionUrl` in the error to subscribe, then retry.
**ProcessPayment fails with "Payment session not found":**
- The session ID is invalid or the session was deleted. Create a new session.
- Ensure the `paymentManagerArn` in the session creation matches the one used in ProcessPayment.
**ProcessPayment fails with "PaymentSessionExpired":**
- Payment sessions are time-bounded. Create a fresh session with `expiryTimeInMinutes`.
**ProcessPayment fails with "Payment instrument not found" or "does not belong to user":**
- Verify the instrument ID is correct and belongs to the same Payment Manager.
- Check that the `userId` passed to ProcessPayment matches the `userId` used when the instrument was created.
**ProcessPayment fails with "Payment connector is not active":**
- The connector may still be provisioning. Check its status and wait.
- If the connector was deleted or deactivated, create a new one.
**ProcessPayment fails with "Network mismatch":**
- The x402 challenge specifies a network that does not match the instrument's network.
- Instruments created with `network: "ETHEREUM"` support Base, Base Sepolia, and Ethereum chains.
- Instruments created with `network: "SOLANA"` support Solana and Solana Devnet chains.
**ProcessPayment fails with "Payment asset not supported USDC token address":**
- The USDC contract address in the x402 challenge does not match the expected address for that network.
- Base Sepolia USDC: `0x036CbD53842c5426634e7929541eC2318f3dCF7e`
- Only USDC is supported.
**ProcessPayment fails with "Wallet does not have a USDC balance":**
- The wallet has no USDC on the specified chain.
- Fund via Circle faucet (testnet): https://faucet.circle.com/
- For mainnet: the end user must fund the wallet directly.
**Coinbase: "Delegated signing grant is not active":**
- The end user has not completed the delegation step.
- Redirect them to the `redirectUrl` returned during instrument creation (Coinbase Hub).
- They must log in and grant permissions to the wallet.
**Coinbase: "Delegated signing is not enabled":**
- The Coinbase CDP project does not have delegated signing enabled.
- Go to portal.cdp.coinbase.com > Project > Wallet > Embedded Wallets > Policies > Enable Delegated signing.
**Stripe Privy: "Privy credentials are invalid":**
- The App ID or App Secret stored in the credential provider is wrong.
- Verify in Privy Dashboard that the credentials match.
- Recreate the credential provider with the correct values.
**Stripe Privy: "Privy appId is invalid or missing":**
- The `appId` in the credential provider configuration is incorrect.
- Check Privy Dashboard for the correct App ID.
**Stripe Privy: "Privy signing key is invalid or expired":**
- The Authorization Private Key or Authorization ID is invalid or has expired.
- Generate a new P-256 key pair in Privy Dashboard > Wallet Infrastructure > Authorization.
- Update the credential provider with the new key.
**Stripe Privy: "Wallet policy denied the transaction":**
- A wallet policy configured in Privy is blocking the transaction.
- Review wallet policy settings in Privy Dashboard.
- Check if the transaction amount, recipient, or frequency exceeds policy limits.
**Stripe Privy: "The linked account data is invalid":**
- The email or phone number used in `linkedAccounts` when creating the instrument is malformed.
- Verify the email format is valid.
**Stripe Privy: "Rate limited by Privy":**
- The Privy API is rate limiting your requests.
- Back off and retry. Check Privy's rate limits documentation.
**ProcessPayment fails with "Payment amount exceeds maximum":**
- The x402 challenge requests more than the maximum allowed per transaction.
- Check the amount in the challenge and verify your session budget allows it.
**ProcessPayment fails with "Rate exceeded":**
- Too many API calls. Back off and retry after a few seconds.
**Coinbase: "Delegation not completed":**
- The end user has not granted the agent permission to spend from their wallet.
- Visit the `redirectUrl` returned during instrument creation, log in, and grant permissions.
**Stripe Privy: "Delegation not completed":**
- The agent auth key has not been added as a session signer on the embedded wallet.
- Follow Step 7 (Stripe Privy sub-steps 7a–7e) to set up the delegation frontend, log in with the end user email provided during setup, and approve delegation for the wallet.
- Verify delegation status with the `/api/check-signers` endpoint (Step 7f).
**Stripe Privy: Delegation frontend setup issues:**
- **Login fails, the Privy modal won't open, or the browser console shows an origin/CORS or "not a valid origin for this app" error**: the local origin is not allowlisted. Add the dev server's exact URL under Privy Dashboard > Configuration > App settings > Domains > Allowed origins > Web & mobile web (Step 7d). Privy matches the browser **origin**, so `http://localhost:3000` works but `http://localhost:3000/` (trailing slash) and `http://localhost` (no port) do not. If the dev server moved off port 3000, the allowlisted port must move with it.
- **"Missing server configuration" from /api/check-signers**: One or more env vars (`NEXT_PUBLIC_PRIVY_APP_ID`, `PRIVY_APP_SECRET`, `NEXT_PUBLIC_PRIVY_SIGNER_ID`) are not set in the frontend's `.env.local`. Map them from the `*_STRIPE_PRIVY_APP_ID` / `*_STRIPE_PRIVY_APP_SECRET` / `*_STRIPE_PRIVY_AUTHORIZATION_ID` keys in `agentcore/.env.local` (Step 7b). Two things make a straight file copy fail: the frontend uses different key names, and the connector's keys are namespaced `AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_*` — so grep the suffix, not the bare name. `cut -d= -f1` on the frontend's `.env.local` shows which keys actually landed.
- **Login fails or no wallets appear**: The Privy app may not have embedded wallets enabled. In Privy Dashboard > Wallet Infrastructure, ensure embedded wallets are configured for the relevant chains (Ethereum/Solana).
- **"Give access" succeeds but payments still fail with "Wallet policy denied"**: The `NEXT_PUBLIC_PRIVY_SIGNER_ID` in the frontend doesn't match the Authorization ID used in the payment connector (Step 3b). Re-derive it from the `*_STRIPE_PRIVY_AUTHORIZATION_ID` key in `agentcore/.env.local` rather than retyping it from the dashboard.
- **User logged in with wrong email**: The email must match the one passed to `setup_payment_user.py --email`. If mismatched, the instrument points to a different Privy user's wallets. Log out, log back in with the correct email.
- **Port conflict**: If the agent's own server is on port 3000, the frontend auto-selects another port. Check the terminal output for the actual URL — and allowlist that port (Step 7d), otherwise login fails.
## Security Considerations
- **Prefer QuickCreate for Coinbase**: QuickCreate avoids the developer handling long-lived provider secrets — you authorize through Coinbase and AgentCore Payments provisions and stores the credential provider for you, removing the plaintext-secret step that manual entry requires.
- **Credential rotation**: Rotate payment provider credentials periodically. Recreate the credential provider with updated values.
- **Budget/spend limits**: Use Payment Session `expiryTimeInMinutes` and per-session budget controls to prevent runaway payments.
- **Audit logging**: Verify CloudTrail is logging all `bedrock-agentcore` API calls, especially `ProcessPayment`. For production, set up a CloudWatch alarm for failed payment attempts as a potential abuse indicator.
- **SSRF mitigation**: The `x402_fetch` tool enforces HTTPS-only and blocks private IP ranges to prevent fetching internal endpoints.
- **Least privilege**: The IAM service role should only have the minimum permissions required (token-vault, workload-identity, secrets access).
- **Session expiry**: Keep payment sessions short-lived (60 minutes or less). Create fresh sessions per user interaction rather than reusing long-lived ones.
- **Encryption in transit**: All payment requests must use HTTPS. The `x402_fetch` tool rejects non-HTTPS URLs.
For comprehensive security guidance, see the [AgentCore Security documentation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/security.html).
## How x402 Payment Works (End-to-End)
```
Agent calls x402_fetch("https://paid-api.example.com/data")
│
├─ 1. HTTP GET → 402 Payment Required
│ Body: {"x402Version": 1, "accepts": [{"scheme": "exact", "network": "base-sepolia", ...}]}
│
├─ 2. Extract x402 challenge
│
├─ 3. ProcessPayment(paymentManagerArn, instrumentId, sessionId, challenge)
│ → Returns signed proof (signature + authorization)
│
├─ 4. Build payment header (X-PAYMENT for v1, PAYMENT-SIGNATURE for v2)
│
├─ 5. Retry with payment header (fresh HTTP client, no cookies)
│ → 200 OK + paid content
│
└─ 6. Return content to agent
```
## MPP (Machine Payments Protocol)
MPP is the second payment protocol AgentCore Payments speaks, alongside x402. It is a protocol-neutral, HTTP-native scheme for machine-to-machine payments (an IETF-track draft; see <https://mpp.dev>). AgentCore acts on the **buyer** side: the agent hits a paid endpoint, receives an MPP challenge in a `402 Payment Required` response, and calls `ProcessPayment` to mint the credential that satisfies it — the same lifecycle as x402, over a different wire format.
### How MPP differs from x402 on the wire
x402 carries its challenge in the response **body** (`x402Version` + `accepts`) and the proof in an `X-PAYMENT` (v1) or `PAYMENT-SIGNATURE` (v2) header. MPP uses the standard HTTP auth handshake instead:
| Primitive | Direction | HTTP header | Encoding |
|---|---|---|---|
| **Challenge** | server → agent (402) | `WWW-Authenticate: Payment ...` | RFC 9110 auth-params (`id="…", realm="…", method="…", intent="…", request="<base64url>", …`) |
| **Credential** | agent → server (retry) | `Authorization: Payment <token>` | `base64url(JSON)`, no padding |
| **Receipt** | server → agent (200) | `Payment-Receipt: <token>` | `base64url(JSON)`, no padding |
Each `402` may carry **one or more** `WWW-Authenticate: Payment` header lines — one per payment option (each a distinct `method`/`intent`). The agent picks one it can satisfy and returns exactly one `Authorization: Payment` header. `method` (`tempo`, `evm`, `solana`, `stripe`, `card`, …) and `intent` (`charge`, `session`, `subscription`) are open IANA registries — MPP is method- and currency-agnostic (crypto or fiat), where x402 is USDC-only. The per-method `request` payload rides inside the challenge as an opaque base64url blob; AgentCore parses it and mints the matching proof, so you forward the challenge verbatim rather than decoding it yourself.
### The MPP ProcessPayment contract
Call the same `ProcessPayment` operation used for x402, with `paymentType` set to `MPP` and the `mpp` arm of `paymentInput`:
```jsonc
// ProcessPayment request (MPP)
{
"paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:111122223333:payment-manager/pm-abc123",
"paymentSessionId": "payment-session-…",
"paymentInstrumentId": "payment-instrument-…",
"paymentType": "MPP",
"paymentInput": {
"mpp": {
"version": "1",
// The raw WWW-Authenticate: Payment header value(s) from the 402, passed verbatim.
// Exactly one entry in this release (ACP fulfills a single challenge per call).
"wwwAuthenticateHeaders": [
"Payment id=\"qB3…\", realm=\"api.example.com\", method=\"evm\", intent=\"charge\", request=\"eyJ…\""
],
// Optional. Authorizes ACP to sign when the buyer pays the blockchain (gas) fees.
"buyerPaysGasFees": false
}
}
}
```
```jsonc
// ProcessPayment response (MPP) — status PROOF_GENERATED
{
"paymentType": "MPP",
"status": "PROOF_GENERATED",
"paymentOutput": {
"mpp": {
"version": "1",
// Echoes the id of the challenge that was paid, so you can correlate without decoding.
"selectedPaymentId": "qB3…",
// Ready-to-send Authorization header value: "Payment <base64url-token>".
// Attach it verbatim and retry the original request — no assembly required.
"paymentCredential": "Payment eyJ…"
}
}
}
```
Notes grounded in the API model:
- **Forward the header verbatim.** Pass the raw `WWW-Authenticate: Payment …` value(s) unchanged. AgentCore parses the auth-params itself — you do no field-mapping or base64 handling — and forwarding as-is preserves the exact bytes the challenge's HMAC binds to.
- **One challenge per call.** `wwwAuthenticateHeaders` accepts exactly one entry in this release. When a `402` offers several options, select the one the instrument can satisfy and send just that line. (It is modeled as a list so the contract can widen to multiple options later without a breaking change.)
- **`paymentCredential` is the finished `Authorization` header.** No assembly needed — attach it to the retry as `Authorization: Payment <token>`. It is a bearer-like secret; do not log it.
- **`buyerPaysGasFees` controls fee sponsorship.** A crypto challenge advertises who pays network (gas) fees via `methodDetails.feePayer`: `true` = the seller sponsors, `false`/absent = the buyer pays from their own wallet on top of the amount. Because that extra cost is not in the challenge `amount`, AgentCore will not assume the buyer accepts it — if the challenge does not offer seller-sponsored fees, it signs only when you set `buyerPaysGasFees: true`, otherwise it fails with `ValidationException`. Omit it (or `false`) for fee-sponsored challenges; it has no effect there.
- **`version`** is the MPP protocol version (a bare numeric string, e.g. `"1"`), distinct from the x402 version.
### MPP end-to-end flow
```
Agent GETs https://paid-api.example.com/data
│
├─ 1. 402 Payment Required
│ WWW-Authenticate: Payment id="qB3…", realm="api.example.com", method="evm", intent="charge", request="eyJ…"
│
├─ 2. ProcessPayment(paymentType="MPP", paymentInput.mpp.wwwAuthenticateHeaders=[<that header, verbatim>])
│ → status PROOF_GENERATED, paymentOutput.mpp.paymentCredential = "Payment eyJ…"
│
├─ 3. Retry with Authorization: Payment eyJ… (fresh HTTP client, no cookies)
│ → 200 OK + paid content (optional Payment-Receipt: <token>)
│
└─ 4. Return content to agent
```
## Supported Networks
Two concepts: **network** (blockchain family, used when creating instruments) and **chain** (specific chain, used in x402 challenges and balance queries).
**Networks (for instrument creation):**
| Network | Instrument Value | Providers |
|---|---|---|
| Ethereum (includes Base, Base Sepolia) | `ETHEREUM` | Coinbase, Stripe |
| Solana (includes Solana Devnet) | `SOLANA` | Coinbase, Stripe |
**Chains (in x402 challenges and balance queries):**
| Chain | Identifier (x402) | Balance API value | Type | Provider |
|---|---|---|---|---|
| Base Sepolia | `base-sepolia` or `eip155:84532` | `BASE_SEPOLIA` | Testnet | Coinbase |
| Base | `eip155:8453` | `BASE` | Mainnet | Coinbase |
| Ethereum Mainnet | `eip155:1` | `ETHEREUM` | Mainnet | Coinbase, Stripe |
| Solana Mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `SOLANA` | Mainnet | Coinbase, Stripe |
| Solana Devnet | `solana-devnet` | `SOLANA_DEVNET` | Testnet | Stripe |
For testing, start with **Base Sepolia** (network: `ETHEREUM`, chain: `BASE_SEPOLIA`) — free testnet tokens from https://faucet.circle.com/.
## Quality criteria
- CLI is installed via `npm install -g @aws/agentcore`, not pip
- Control plane (credential provider, manager, connector) is provisioned via the CLI; the manager non-interactively. For a Coinbase connector, **QuickCreate (`--provision-mode QUICK_CREATE`, no secrets) is offered first**; manual secret entry is the alternative and the only path for Stripe (Privy). Only the connector step involves the developer — QuickCreate: browser authorization; Manual: entering secrets
- The `agentcore:onboarding-source: agent-toolkit-skill` tag is added to `agentcore/agentcore.json` (Step 3a) before deploy — this is mandatory, so the provisioned resources are attributable to this skill
- Data plane (instrument, session) is created via the SDK script, not hand-written code
- If the project is Strands or LangGraph, the native integration (Step 5a) is offered first as the simpler path
- The generic tool path (Step 5b) is used only for other frameworks or when the developer explicitly wants manual control
- Payments are wired via the framework-native integration (Step 5a) or the framework-agnostic `x402_fetch` tool (Step 5b)
- Both x402 and MPP merchants are payable through the same manager/connector/instrument/session and the same `ProcessPayment` API — no protocol is chosen up front
- For MPP, the raw `WWW-Authenticate: Payment` header is forwarded verbatim (one per call) and the returned `paymentCredential` is attached as `Authorization: Payment <token>` unchanged
- Credentials never pass through the agent or the chat
references/request-headers.md
# request-headers
Pass custom HTTP headers from the caller through to your agent's invocation code.
## When to use
- You need to pass a tenant ID, correlation ID, or feature flag from your app to your agent
- You're implementing a protocol that requires specific headers (A2A, MCP, vendor-specific)
- You want OpenTelemetry baggage or trace headers to propagate from the caller
- You tried adding a header to the request and your agent code never sees it
- You're integrating with an external system that uses idempotency keys or similar headers
## The default: most headers are stripped
AgentCore Runtime strips all incoming headers from the request before it reaches your agent code **except**:
- `Authorization` — always passed through
- Any header matching `X-Amzn-Bedrock-AgentCore-Runtime-Custom-*` — this is the reserved prefix for custom headers
Anything else — `X-Tenant-Id`, `X-Correlation-Id`, `traceparent`, `A2A-Version`, `Idempotency-Key`, whatever — will not appear in your invocation context unless you explicitly add it to the runtime's request header allowlist.
This is an intentional security boundary: the runtime doesn't forward arbitrary caller-supplied headers by default. It's also the #1 reason developers ask "why can't my agent see the header I'm sending?"
## Two ways to pass custom data
### Option 1: Use the reserved prefix
Rename headers at the caller to use the `X-Amzn-Bedrock-AgentCore-Runtime-Custom-` prefix. These pass through without any runtime configuration change.
```
# Caller sends:
X-Amzn-Bedrock-AgentCore-Runtime-Custom-Tenant-Id: acme-corp
X-Amzn-Bedrock-AgentCore-Runtime-Custom-Correlation-Id: 8b2e3d...
# Agent code sees the same headers in the invocation context
```
This is the simplest option for headers you control end-to-end (your app, your agent).
### Option 2: Add headers to the request header allowlist
If the header names are fixed by a protocol or external system (A2A requires `A2A-Version` and `A2A-Extensions`; OpenTelemetry uses `traceparent` and `baggage`; some APIs use `Idempotency-Key`), you can't rename them. Configure the runtime to allow them explicitly.
**Edit `agentcore/agentcore.json`** and add `requestHeaderAllowlist` to the runtime entry:
```json
{
"runtimes": [
{
"name": "MyAgent",
"requestHeaderAllowlist": [
"X-Amzn-Bedrock-AgentCore-Runtime-Custom-X-Tenant-Id",
"X-Amzn-Bedrock-AgentCore-Runtime-Custom-A2A-Version"
]
}
]
}
```
Then `agentcore deploy`. The `$schema` URL at the top of the file (`https://schema.agentcore.aws.dev/v1/agentcore.json`) gives IDE autocomplete and validation for every field.
**CLI shortcut** — `agentcore add agent --request-header-allowlist "X-Tenant-Id,A2A-Version"` writes the same array. **Important:** the CLI auto-prefixes entries with `X-Amzn-Bedrock-AgentCore-Runtime-Custom-` as they land in `agentcore.json`. If you're editing the JSON by hand, write the prefixed form directly. If you're using the CLI, pass the short name and let the CLI add the prefix.
`Authorization` passes through by default and doesn't need to be in the allowlist.
### Constraints
- **Maximum 20 headers** in the allowlist (including `Authorization` if you include it explicitly)
- **Header name length:** up to 256 characters
- **Header value size:** up to 4 KB per header
- **Names are case-sensitive** — list them exactly as they'll be sent
- **Changes take effect after the next deploy** of the runtime
If you hit the 20-header cap, combine related data into one JSON-encoded header rather than using many separate ones.
## Common use cases
### Multi-tenancy
```
Caller: X-Tenant-Id: acme-corp
Agent code: reads tenant from the header, scopes memory/data/tools per tenant
```
Add `X-Tenant-Id` to the allowlist. The agent can then isolate memory namespaces, database queries, and tool-call authorization per tenant.
### Distributed tracing propagation
```
Caller: traceparent: 00-<trace-id>-<span-id>-01
baggage: userId=alice,env=prod
Agent code: uses OTel SDK to continue the parent trace
```
Add `traceparent` and `baggage` to the allowlist. Your OTel SDK instrumentation will pick them up automatically and produce spans connected to the caller's trace.
### A2A protocol compliance
```
Caller: A2A-Version: 1.0
A2A-Extensions: x-capability-foo
Agent code: branches behavior based on protocol version
```
A2A v1.0 requires these headers. Add both to the allowlist; A2A v0.3 doesn't need either.
### Idempotency keys
```
Caller: Idempotency-Key: 7f3a...
Agent code: deduplicates or caches based on the key
```
For agents that call external APIs with idempotency, propagating the caller's key through to the agent's outbound calls avoids duplicate side effects on retry.
## Reading the headers in agent code
Headers arrive in the runtime's `context` object passed to your invocation handler. The exact accessor depends on the framework — check the bedrock-agentcore SDK docs for your language. In Python:
```python
@app.entrypoint
def invoke(payload, context):
tenant = context.headers.get("X-Tenant-Id")
correlation_id = context.headers.get("X-Correlation-Id")
# ... use as needed
```
Headers that weren't in the allowlist will be absent (not empty string) from the context.
## What won't work
- **Sending headers without configuring the allowlist** — anything outside the default pass-through set is silently dropped. Your agent code won't see the header, and there's no error. Check the runtime's `requestHeaderConfiguration` if a header you expect to see isn't arriving.
- **Using this for secrets** — 4 KB values and the allowlist configuration are designed for metadata, not credentials. Use the AgentCore Identity credential provider for API keys, OAuth tokens, and secrets. See `agents-connect` Path D.
- **Dynamic headers** — the allowlist is static runtime configuration. You can't vary it per-request.
## Troubleshooting
**"My agent doesn't see the header I'm sending"**
Check (in order): (1) Is the header in the allowlist? (2) Is the spelling an exact match including case? (3) Did you redeploy the runtime after updating the allowlist? (4) Is the caller actually sending the header — `curl -v` or equivalent network inspection.
**"I hit the 20-header limit"**
Consolidate related data into a single JSON-encoded header. For example, instead of `X-Region`, `X-Environment`, `X-Service-Name` as three separate headers, use `X-Context: {"region":"us-west-2","env":"prod","service":"billing"}`.
**"Allowlist update didn't take effect"**
Redeploy the runtime. The header allowlist is config that applies on the next `agentcore deploy`, not immediately after editing `agentcore.json`.
## Output
- Decision on prefix vs. allowlist approach
- CLI command to update the allowlist if needed
- Agent code pattern for reading the headers
references/teardown.md
# teardown
Remove individual resources from your project or tear down the entire deployment.
## When to use
- You want to remove a gateway, memory, credential, evaluator, or other resource from your project
- You want to delete a deployed agent and clean up all AWS resources
- You're iterating in a sandbox account and want to start fresh
- You need to remove a resource that's stuck or no longer needed
## Process
### Removing individual resources from your project
Use `agentcore remove` to remove a resource from `agentcore.json`. This marks the resource for deletion — the actual AWS resource is removed on the next `agentcore deploy`.
```bash
# Remove a memory resource
agentcore remove memory --name MyMemory
# Remove a gateway target
agentcore remove gateway-target --name WeatherTools --gateway MyGateway
# Remove a gateway (remove all its targets first)
agentcore remove gateway --name MyGateway
# Remove a credential
agentcore remove credential --name MyAPIKey
# Remove an evaluator
agentcore remove evaluator --name ResponseQuality
# Remove an online eval config
agentcore remove online-eval --name production_monitor
# Remove a policy
agentcore remove policy --name SpendingLimit --engine MyPolicyEngine
# Remove a policy engine (remove all its policies first)
agentcore remove policy-engine --name MyPolicyEngine
```
After removing, deploy to apply the changes:
```bash
agentcore deploy -y
```
Check what's pending removal before deploying:
```bash
agentcore status --state pending-removal
```
### Removing an agent from a multi-agent project
If your project has multiple agents (runtimes), you can remove one:
```bash
agentcore remove agent --name SecondAgent
agentcore deploy -y
```
This deletes the agent's runtime, endpoint, and associated resources from AWS. The agent's code in `app/<AgentName>/` is not deleted — remove it manually if you no longer need it.
### Tearing down the entire deployment
To remove all deployed AWS resources for a project:
```bash
# Preview what will be destroyed
agentcore deploy --diff
# Destroy all resources
npx cdk destroy --app "npx ts-node agentcore/cdk/bin/cdk.ts" --force
```
Alternatively, delete the CloudFormation stack directly:
```bash
# Find the stack name
aws cloudformation list-stacks \
--stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
--query "StackSummaries[?contains(StackName, '<ProjectName>')].StackName"
# Delete it
aws cloudformation delete-stack --stack-name <StackName>
# Wait for deletion to complete
aws cloudformation wait stack-delete-complete --stack-name <StackName>
```
### What gets deleted and what doesn't
| Resource | Deleted by `cdk destroy` | Notes |
|---|---|---|
| AgentCore Runtime(s) | ✅ | Includes all endpoints and versions |
| Memory resource(s) | ✅ | Memory data is deleted permanently |
| Gateway(s) and targets | ✅ | |
| Credentials | ✅ | Secrets Manager entries are removed |
| Policy engine(s) and policies | ✅ | |
| Evaluator definitions | ✅ | |
| Online eval configs | ✅ | |
| IAM roles | ✅ | Created by CDK |
| CloudWatch log groups | ❌ | Persist after deletion — delete manually if needed |
| ECR images (Container builds) | ❌ | Persist — delete the repository manually |
| CDK bootstrap stack | ❌ | Shared across projects — don't delete unless you're done with CDK entirely |
| Local project files | ❌ | `agentcore/`, `app/` — delete manually |
### Cleaning up CloudWatch log groups
Log groups persist after stack deletion. To clean them up:
```bash
# List AgentCore log groups
aws logs describe-log-groups \
--log-group-name-prefix /aws/bedrock-agentcore/ \
--query "logGroups[].logGroupName"
# Delete a specific log group
aws logs delete-log-group --log-group-name /aws/bedrock-agentcore/runtimes/<AGENT_ID>-DEFAULT
```
### Cleaning up ECR repositories (Container builds)
```bash
# List AgentCore ECR repositories
aws ecr describe-repositories \
--query "repositories[?contains(repositoryName, 'bedrock-agentcore')].repositoryName"
# Delete a repository and all its images
aws ecr delete-repository --repository-name <repo-name> --force
```
### Handling stuck resources
If a runtime is stuck in DELETING state for more than 30 minutes, see the "Runtime stuck in DELETING" section in `agents-debug`. The short version: don't keep retrying — open an AWS Support case with the runtime ARN and the original delete request ID from CloudTrail.
## Common issues
**"Can't remove gateway — targets still attached"**
Remove all gateway targets first, then remove the gateway:
```bash
agentcore remove gateway-target --name Target1 --gateway MyGateway
agentcore remove gateway-target --name Target2 --gateway MyGateway
agentcore remove gateway --name MyGateway
```
**"Can't remove policy engine — policies still attached"**
Remove all policies first, then remove the engine:
```bash
agentcore remove policy --name Policy1 --engine MyEngine
agentcore remove policy-engine --name MyEngine
```
**"Resource shows pending-removal but deploy doesn't delete it"**
Check `agentcore status --state pending-removal` and verify the resource is listed. If deploy completes without removing it, check the CDK output for errors — the deletion may have failed silently due to a dependency.
## Output
- CLI commands to remove the specific resource(s)
- Guidance on what persists after deletion and how to clean it up
- Warnings about irreversible data loss (memory data, credentials)
references/vpc.md
# vpc
Configure your AgentCore agent to connect to private AWS resources inside a VPC.
## When to use
- Your agent needs to connect to an RDS database
- Your agent needs to call internal APIs not exposed to the internet
- You want to keep your agent's network traffic private
- VPC connectivity is configured but connections are timing out
## Input
`$ARGUMENTS` is optional:
```
/vpc # interactive — asks what you're connecting to
/vpc rds # RDS database connectivity
/vpc debug # diagnose VPC connectivity issues
```
## How AgentCore VPC connectivity works
When you configure VPC mode, AgentCore creates **Elastic Network Interfaces (ENIs)** in your VPC subnets. These ENIs give your agent a private IP address in your VPC, enabling it to reach private resources.
**Key facts:**
- VPC connectivity directly affects **outbound traffic** — ENIs route your agent's outbound calls through your VPC. For **inbound traffic**, you can optionally add an AgentCore VPC endpoint to keep API calls private via PrivateLink (this is separate from the `networkMode` setting).
- AgentCore creates ENIs via the service-linked role `AWSServiceRoleForBedrockAgentCoreNetwork` (auto-created on first VPC deployment)
- Subnets must be in **supported Availability Zones** — not all AZs are supported. The supported AZ list changes as AgentCore expands to new regions.
---
## Step 0: Verify CLI version
Run `agentcore --version`. This skill requires v0.9.0 or later. If the version is older, tell the developer to run `agentcore update` before proceeding.
---
## Step 1: Verify your subnets are in supported AZs
AgentCore only supports specific Availability Zone IDs per region. The supported AZ list changes as AgentCore expands — **always check the current docs** for the latest table.
Check your subnet's AZ ID:
```bash
# Check the AZ ID of your subnet
aws ec2 describe-subnets \
--subnet-ids subnet-12345678 \
--query 'Subnets[0].{AZ:AvailabilityZone,AZId:AvailabilityZoneId,SubnetId:SubnetId}'
```
**To find the current supported AZ IDs:** See the AgentCore VPC configuration guide: https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-vpc.html — look for the "Supported Availability Zones" section. The table lists AZ IDs (e.g., `use1-az1`, `usw2-az2`) per region — use AZ IDs, not AZ names, because AZ name-to-ID mappings differ across AWS accounts.
If your subnet is in an unsupported AZ, the deployment will fail. Use subnets in supported AZs.
**Best practice:** Use at least two subnets in different supported AZs for high availability.
---
## Step 2: Configure security groups
Security groups control what your agent can connect to. Configure them based on what you're connecting to.
### Connecting to RDS PostgreSQL
**AgentCore agent security group** (outbound rule):
```
Type: Custom TCP
Port: 5432
Destination: RDS security group ID (not CIDR)
```
**RDS security group** (inbound rule):
```
Type: PostgreSQL
Port: 5432
Source: AgentCore agent security group ID
```
```bash
# Create a security group for the agent
aws ec2 create-security-group \
--group-name agentcore-agent-sg \
--description "AgentCore agent security group" \
--vpc-id vpc-12345678
# Add outbound rule to reach RDS
aws ec2 authorize-security-group-egress \
--group-id sg-agent123 \
--protocol tcp \
--port 5432 \
--source-group sg-rds456
# Add inbound rule to RDS security group
aws ec2 authorize-security-group-ingress \
--group-id sg-rds456 \
--protocol tcp \
--port 5432 \
--source-group sg-agent123
```
### Connecting to internal APIs (HTTP/HTTPS)
**AgentCore agent security group** (outbound rules):
```
Type: HTTPS, Port: 443, Destination: API security group or CIDR
Type: HTTP, Port: 80, Destination: API security group or CIDR (if needed)
```
---
## Step 3: Configure the agent for VPC
### New project
```bash
agentcore create \
--name MyAgent \
--defaults \
--network-mode VPC \
--subnets subnet-abc123,subnet-def456 \
--security-groups sg-agent123
```
### Existing project
```bash
agentcore add agent \
--name MyAgent \
--network-mode VPC \
--subnets subnet-abc123,subnet-def456 \
--security-groups sg-agent123
```
Or edit `agentcore/agentcore.json` directly — add the `networkMode` and `networkConfig` fields to the runtime's entry:
```json
{
"runtimes": [
{
"name": "MyAgent",
"networkMode": "VPC",
"networkConfig": {
"subnets": ["subnet-abc123", "subnet-def456"],
"securityGroups": ["sg-agent123"]
}
}
]
}
```
The `$schema` URL at the top of `agentcore.json` (`https://schema.agentcore.aws.dev/v1/agentcore.json`) gives IDE autocomplete and validation for every field — including the subnet/security-group ID patterns.
### Deploy
```bash
agentcore deploy -y
```
---
## Internet access from VPC
> [!WARNING]
> Connecting AgentCore to a VPC does NOT provide internet access by default.
> Public subnets do NOT provide internet access for AgentCore ENIs.
> To reach the internet from VPC mode, you MUST use private subnets with a NAT gateway.
**Architecture for internet + VPC access:**
```
AgentCore agent (private subnet)
↓ outbound traffic
NAT Gateway (public subnet)
↓
Internet Gateway
↓
Internet
```
```bash
# Create NAT gateway in a public subnet
aws ec2 create-nat-gateway \
--subnet-id subnet-public123 \
--allocation-id eipalloc-12345678
# Update private subnet route table to use NAT gateway
aws ec2 create-route \
--route-table-id rtb-private123 \
--destination-cidr-block 0.0.0.0/0 \
--nat-gateway-id nat-12345678
```
---
## Fully private VPC (no internet)
If your VPC has no internet access, you need VPC endpoints for AWS services. These endpoints are **required** without internet access and **strongly recommended** even with a NAT gateway to avoid NAT gateway data processing charges:
```bash
# ECR Docker endpoint (required for container image pulls)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.ecr.dkr \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123
# ECR API endpoint (required for container image pulls)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.ecr.api \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123
# S3 Gateway endpoint (required — ECR stores image layers in S3)
# This is a free Gateway endpoint. Without it, ECR image refreshes
# route through NAT and incur data processing charges.
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids rtb-private123
# CloudWatch Logs (required for agent logging)
aws ec2 create-vpc-endpoint \
--vpc-id vpc-12345678 \
--service-name com.amazonaws.REGION.logs \
--vpc-endpoint-type Interface \
--subnet-ids subnet-abc123 \
--security-group-ids sg-agent123
```
---
## Cold-start connectivity checklist
A common pattern: `UpdateAgentRuntime` returns READY, the network configuration looks right, but invocations return 502 or hang. Requests never reach your container. This almost always means a new VM can start but can't complete the work needed to be ready for traffic.
Cold-start VMs need outbound HTTPS (port 443) to these AWS service endpoints. In public or NAT-routed VPCs, a correctly configured NAT gateway covers all of them. In fully private VPCs, every one of these needs an interface VPC endpoint or gateway endpoint:
- `com.amazonaws.<region>.ecr.api` — pull image metadata
- `com.amazonaws.<region>.ecr.dkr` — pull container layers
- `com.amazonaws.<region>.s3` (Gateway endpoint) — ECR layers live in S3
- `com.amazonaws.<region>.logs` — emit CloudWatch logs
- `com.amazonaws.<region>.monitoring` — emit CloudWatch metrics
- `com.amazonaws.<region>.sts` — assume the execution role
Plus whichever endpoints your agent's tools and dependencies need (Bedrock, DynamoDB, Secrets Manager, etc.).
### Security group outbound rule
The agent's security group needs an outbound rule to reach 443 on each VPC endpoint's prefix list, or `0.0.0.0/0` if the endpoints are reachable directly:
```bash
aws ec2 authorize-security-group-egress \
--group-id sg-agent123 \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
```
If you scope egress more tightly (to specific endpoint prefix lists or CIDR blocks), double-check that every endpoint above is covered.
### NACLs — the gotcha
Network ACLs are **stateless**. A security group allowing outbound 443 implicitly allows the response traffic. A NACL does not.
If your subnet uses a restrictive NACL, you need both directions explicitly:
- **Outbound:** allow TCP 443 to the destination
- **Inbound:** allow **ephemeral ports 1024–65535** (TCP) from the destination — these are the return-traffic ports
Forgetting the inbound ephemeral-port rule produces the exact symptom of "connection works sometimes, hangs other times" because TCP handshakes succeed (SYN goes out, SYN-ACK comes back on low port ranges) but the actual data response on an ephemeral port gets dropped.
### Transit Gateway and custom egress
If your subnet routes outbound through a Transit Gateway to a central firewall, NAT, or network virtualization layer, the TGW attachment and downstream must have a working route to the internet (or to each VPC endpoint individually).
Symptoms of a missing TGW route:
- Invocations hang for the full client-side timeout (~300 seconds for default Lambda clients)
- No 502, no `ConnectionClosedError` — the request just doesn't come back
- `ping` from a test EC2 in the same subnet/SG works, but actual invocations don't
- Warm environments (already initialized, so already have all their egress done) succeed, new cold starts fail
The test-from-an-EC2 pattern is useful here: launch a t3.micro in the same subnet with the same security group, and try `curl https://s3.<region>.amazonaws.com`, `curl https://ecr.<region>.amazonaws.com`, etc. If any of those hang or fail, the agent will fail to cold-start too.
### Expect higher cold-start time in VPC mode
VPC mode adds ENI attachment and setup time to cold start on top of container image pull and application startup. First invocations in a freshly-configured VPC are noticeably slower than in public mode.
Mitigation is the same as for all cold-start latency: reuse sessions, keep the image lean, defer heavy initialization. See `agents-harden` Initialization time section.
---
## Troubleshooting
**Connection timeouts to RDS or internal APIs:**
1. Verify security group rules — outbound from agent SG, inbound on target SG
2. Check route tables — private subnet must route to NAT gateway (for internet) or have direct routes to targets
3. Verify DNS resolution is enabled in the VPC: `aws ec2 describe-vpc-attribute --vpc-id vpc-12345678 --attribute enableDnsSupport`
**"Unsupported Availability Zone" error during deploy:**
Your subnet is in an AZ that AgentCore doesn't support. Check the AZ ID (not the AZ name) and use a subnet in a supported AZ.
**Agent can't reach internet after VPC configuration:**
You're using a public subnet or missing a NAT gateway. AgentCore ENIs in public subnets don't get internet access. Use private subnets with a NAT gateway.
**"AccessDenied" when using VPC endpoints:**
The execution role is missing permissions for the service behind the VPC endpoint. Check the endpoint's resource policy and the execution role's IAM policy.
**Code Interpreter timeouts calling public endpoints:**
Code Interpreter also needs VPC configuration if your agent is in a VPC. Configure it with the same subnets and a NAT gateway for internet access.
**DNS resolution failures:**
Enable DNS resolution and DNS hostnames in your VPC:
```bash
aws ec2 modify-vpc-attribute --vpc-id vpc-12345678 --enable-dns-support
aws ec2 modify-vpc-attribute --vpc-id vpc-12345678 --enable-dns-hostnames
```
## Output
- Subnet AZ validation results
- Security group rules for the specific target (RDS, internal API, etc.)
- CLI commands to configure VPC mode
- NAT gateway setup if internet access is needed
- VPC endpoint list for fully private deployments
## Quality criteria
- Subnet AZ IDs are validated against supported AZs (not AZ names — names vary by account)
- Security group rules cover both directions (agent outbound + target inbound)
- NAT gateway is recommended for internet access (not public subnets — AgentCore ENIs don't get public IPs)
- VPC endpoint list is complete for fully private deployments
- The developer understands that `networkMode: VPC` primarily affects outbound traffic
scripts/process_payment_tool.py
"""Framework-agnostic payment tool for AgentCore Payments (x402 and MPP).
Copy this file into your agent project and register `x402_fetch` as a tool in
whatever framework you use (Strands, LangGraph, OpenAI Agents SDK, etc.). The
core logic is pure Python with no framework dependency.
Flow:
request -> detect 402 -> PaymentManager.generate_payment_header (the SDK
validates the 402, selects the network, processes the payment, and builds the
version-aware v1/v2 proof header) -> retry with a fresh client.
Transient settlement: the SDK builds a valid header, but the merchant's
on-chain settlement is occasionally transient and the paid retry still returns
402. The SDK does not make the merchant HTTP call (it only builds the header),
so it cannot retry that — this tool re-runs the settle+replay flow up to
X402_MAX_PAYMENT_ATTEMPTS times before giving up. A single idempotency token
(client_token) is reused across all attempts of one fetch, so ProcessPayment is
idempotent: every retry replays the SAME on-chain authorization/nonce. That
recovers a not-yet-settled transient failure, and if the merchant actually did
settle but still returned 402, the replay simply reverts on-chain (nonce already
used) rather than charging the user a second time.
Control-plane resources (payment manager/connector) are created by the AgentCore
CLI; the per-user instrument/session are created by setup_payment_user.py. This
tool only consumes them, via these environment variables:
PAYMENT_MANAGER_ARN payment manager ARN (from deployed-state.json)
PAYMENT_INSTRUMENT_ID per-user wallet ID (from setup_payment_user.py)
PAYMENT_SESSION_ID per-conversation session (from setup_payment_user.py)
PAYMENT_USER_ID end-user identity (required)
AWS_REGION region (default us-west-2)
X402_MAX_PAYMENT_ATTEMPTS transient-402 retry cap (default 5)
"""
import ipaddress
import json
import os
import socket
import uuid
from urllib.parse import urlparse
import httpx
from bedrock_agentcore.payments import PaymentManager
PAYMENT_MANAGER_ARN = os.getenv("PAYMENT_MANAGER_ARN")
PAYMENT_INSTRUMENT_ID = os.getenv("PAYMENT_INSTRUMENT_ID")
PAYMENT_SESSION_ID = os.getenv("PAYMENT_SESSION_ID")
PAYMENT_USER_ID = os.environ.get("PAYMENT_USER_ID") # required — no insecure default
REGION = os.getenv("AWS_REGION", "us-west-2")
# Transient on-chain settlement can leave the paid retry at 402 even though the
# header was valid; re-settle (fresh header + idempotency token) up to this many times.
MAX_PAYMENT_ATTEMPTS = int(os.getenv("X402_MAX_PAYMENT_ATTEMPTS", "5"))
# AgentCore Payments data-plane client (SDK). Created when configured.
_manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION) if PAYMENT_MANAGER_ARN else None
def _validate_url(url):
"""Return an error string if the URL is not HTTPS or targets a private/internal IP."""
parsed = urlparse(url)
if parsed.scheme != "https":
return "Only HTTPS URLs are supported for payment requests"
try:
for _family, _, _, _, sockaddr in socket.getaddrinfo(parsed.hostname, parsed.port or 443):
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local:
return "Cannot fetch private/internal network addresses"
except socket.gaierror:
return "Cannot resolve hostname"
return None
def _settle_and_retry(url, method, response, client_token):
"""Build the payment header from a 402 response via the SDK, then replay the request.
The SDK's generate_payment_header does the whole settle workflow (validate the
402, pick the network, ProcessPayment, build the v1 `X-PAYMENT` / v2
`PAYMENT-SIGNATURE` proof) and returns {header_name: header_value}. We pass a
STABLE client_token (the same one for every attempt of a single fetch) so
ProcessPayment is idempotent — each retry replays the same authorization/nonce
and can never double-charge.
Returns the retry httpx.Response. Raises on a header-generation failure.
"""
payment_header = _manager.generate_payment_header(
payment_instrument_id=PAYMENT_INSTRUMENT_ID,
payment_session_id=PAYMENT_SESSION_ID,
user_id=PAYMENT_USER_ID,
client_token=client_token,
payment_required_request={
"statusCode": response.status_code,
"headers": dict(response.headers),
"body": response.text,
},
)
# Retry with a FRESH client so cookies from the 402 response don't contaminate it.
with httpx.Client(verify=True) as client:
return client.request(method, url, headers=payment_header, timeout=30)
def x402_fetch(url, method="GET"):
"""Fetch a URL, automatically settling any x402 402 Payment Required response.
Returns a JSON string with status_code, body, and (on payment) payment_made.
"""
url_error = _validate_url(url)
if url_error:
return json.dumps({"error": url_error})
if not PAYMENT_USER_ID:
return json.dumps({"error": "PAYMENT_USER_ID environment variable is required"})
response = httpx.request(method, url, timeout=30)
if response.status_code != 402:
return json.dumps({"status_code": response.status_code, "body": response.text})
if not _manager:
return json.dumps({
"status_code": 402,
"error": "No payment configuration. Set PAYMENT_MANAGER_ARN.",
"body": response.text,
})
# One idempotency token for the whole fetch: every retry replays the SAME
# authorization/nonce, so a transient 402 can be re-settled without ever double-charging.
client_token = str(uuid.uuid4())
for attempt in range(1, MAX_PAYMENT_ATTEMPTS + 1):
try:
retry_response = _settle_and_retry(url, method, response, client_token)
except Exception as e: # noqa: BLE001 - surface any payment failure (incl. typed SDK errors) to the agent
return json.dumps({"status_code": 402, "error": f"Payment header generation failed: {e}"})
if retry_response.status_code != 402:
# Success (2xx) or a non-transient error — return it; payment_made reflects the actual status.
return json.dumps({
"status_code": retry_response.status_code,
"body": retry_response.text,
"payment_made": 200 <= retry_response.status_code < 300,
"payment_attempts": attempt,
})
# Transient post-payment 402 — retry with the same idempotency token (same
# authorization/nonce), giving settlement another chance without double-charging.
response = retry_response
return json.dumps({
"status_code": 402,
"error": f"Paid and retried {MAX_PAYMENT_ATTEMPTS} times but the merchant still returned 402 "
"(transient on-chain settlement). Try again shortly.",
"body": response.text,
"payment_made": False,
"payment_attempts": MAX_PAYMENT_ATTEMPTS,
})
scripts/setup_payment_user.py
#!/usr/bin/env python3
"""Provision per-user AgentCore Payments data-plane resources (instrument + optional session).
Control-plane (manager/connector/credential provider) is created by the AgentCore CLI.
This script uses the AgentCore SDK for the data plane:
- one payment instrument (wallet) per end user
- optionally one budget-bounded payment session
Usage:
python setup_payment_user.py --user-id alice --email alice@example.com [--budget 5] \
[--manager-arn ...] [--connector-id ...] [--region us-east-1] [--network ETHEREUM]
Manager ARN / connector ID are auto-read from agentcore/.cli/deployed-state.json if not passed.
"""
import argparse
import json
import os
import sys
from pathlib import Path
from bedrock_agentcore.payments import PaymentManager
def _from_deployed_state():
"""Best-effort: read manager ARN + connector ID from the CLI's deployed state.
CLI 0.20.x writes targets.<target>.resources.payments[]; older shapes used a
top-level payments[]. Handle both.
"""
path = Path("agentcore/.cli/deployed-state.json")
if not path.exists():
return None, None
try:
data = json.loads(path.read_text())
payments = None
targets = data.get("targets") or {}
target = targets.get("default") or (next(iter(targets.values()), {}) if targets else {})
if isinstance(target, dict):
payments = (target.get("resources") or {}).get("payments")
if not payments:
payments = data.get("payments") # legacy/top-level fallback
if not payments:
return None, None
pay = payments[0]
connectors = pay.get("connectors") or []
return pay.get("managerArn"), (connectors[0].get("connectorId") if connectors else None)
except Exception:
return None, None
def main():
ap = argparse.ArgumentParser(description="Provision a per-user AgentCore Payments instrument")
ap.add_argument("--user-id", required=True, help="Stable end-user identifier")
ap.add_argument("--email", required=True, help="End-user email (linked to the wallet; required for delegation)")
ap.add_argument("--budget", default=None, help="Optional session spend cap in USD, e.g. 5")
ap.add_argument("--expiry-minutes", type=int, default=60, help="Session expiry, 15-480")
ap.add_argument("--network", default="ETHEREUM", help="Wallet network family: ETHEREUM or SOLANA")
ap.add_argument("--manager-arn", default=os.environ.get("PAYMENT_MANAGER_ARN"))
ap.add_argument("--connector-id", default=os.environ.get("PAYMENT_CONNECTOR_ID"))
ap.add_argument("--region", default=os.environ.get("AWS_REGION", "us-east-1"))
args = ap.parse_args()
manager_arn, connector_id = args.manager_arn, args.connector_id
if not manager_arn or not connector_id:
ds_arn, ds_conn = _from_deployed_state()
manager_arn = manager_arn or ds_arn
connector_id = connector_id or ds_conn
if not manager_arn or not connector_id:
sys.exit("Could not resolve manager ARN / connector ID. Pass --manager-arn and --connector-id, "
"or run from the project dir with agentcore/.cli/deployed-state.json present.")
manager = PaymentManager(payment_manager_arn=manager_arn, region_name=args.region)
# Data plane: per-user instrument (wallet). Email -> linkedAccounts.
instrument = manager.create_payment_instrument(
user_id=args.user_id,
payment_connector_id=connector_id,
payment_instrument_type="EMBEDDED_CRYPTO_WALLET",
payment_instrument_details={
"embeddedCryptoWallet": {
"network": args.network,
"linkedAccounts": [{"email": {"emailAddress": args.email}}],
}
},
)
instrument_id = instrument["paymentInstrumentId"]
wallet = instrument.get("paymentInstrumentDetails", {}).get("embeddedCryptoWallet", {})
wallet_address = wallet.get("walletAddress")
redirect_url = wallet.get("redirectUrl") # Coinbase delegation URL; None for Privy
# Data plane: optional budget-bounded session. NOTE: cap key is "value", not "amount".
session_id = None
if args.budget:
session = manager.create_payment_session(
user_id=args.user_id,
expiry_time_in_minutes=args.expiry_minutes,
limits={"maxSpendAmount": {"value": str(args.budget), "currency": "USD"}}, # cap currency is USD
)
session_id = session["paymentSessionId"]
print("Instrument ID :", instrument_id)
print("Wallet address:", wallet_address)
print("Session ID :", session_id or "(none - use `agentcore invoke --auto-session`)")
print("\nExport these for the x402 tool (Step 8):")
print(f" export PAYMENT_MANAGER_ARN={manager_arn}")
print(f" export PAYMENT_INSTRUMENT_ID={instrument_id}")
if session_id:
print(f" export PAYMENT_SESSION_ID={session_id}")
print(f" export PAYMENT_USER_ID={args.user_id}")
print(f" export AWS_REGION={args.region}")
print("\nOne-time per wallet:")
if redirect_url:
print(f" 1. Delegation (Coinbase): visit {redirect_url}, log in, grant access to {wallet_address}")
else:
print(" 1. Delegation (Privy): approve delegation via the Privy frontend SDK")
print(f" 2. Funding: send testnet USDC to {wallet_address} via https://faucet.circle.com/ (Base Sepolia)")
if __name__ == "__main__":
main()
SKILL.md
---
name: agents-build
description: >
Use to extend an existing agent project with memory, app integration,
VPC, multi-agent, migration, model, browser, code interpreter,
payments, or resource removal. Triggers: "add memory",
"remember across sessions", "call agent from app", "invoke agent from
code", "agent auth", "streaming", "VPC", "VPC
connectivity", "can't reach from VPC", "multi-agent",
"A2A", "A2A auth", "orchestrator not delegating", "specialist not
called", "migrate Bedrock Agent", "migration issue", "change model",
"browser tool", "code
interpreter", "delete agent", "tear down", "agentcore remove",
"cross-account memory",
"add payments capability to my agent", "wire payments plugin",
"integrate x402 payments with the agent I'm building",
"add MPP payments", "Machine Payments Protocol".
External APIs via Gateway: use agents-connect. New project:
use agents-get-started. CLI/dev-server errors: use agents-debug.
Runtime x402/MPP payments: use agents-pay. Migration-specific Strands vs
LangGraph routes here.
allowed-tools: Read Grep Glob Bash
metadata:
type: skill
version: "1.0.0"
author: aws-agentcore
requires-cli: ">=0.9.0"
---
# build
Add capabilities to your AgentCore agent project.
## When to use
- Adding cross-session memory to your agent
- Calling your deployed agent from a web app, mobile app, or backend service
- Configuring VPC networking for private resources (RDS, internal APIs)
- Building multi-agent systems with orchestrator/specialist patterns
- Migrating an existing Bedrock Agent to AgentCore
- Adding the Browser tool so the agent can navigate websites
- Adding the Code Interpreter so the agent can execute code in a sandbox
- Adding AgentCore Payments so the agent can pay for x402- or MPP-protected APIs, tools, or content
- Removing resources from your project or tearing down a deployment
Do NOT use for:
- Connecting to external tools/APIs via Gateway (OpenAPI specs, Lambda, MCP servers, credentials, policies) → use `agents-connect`
- Scaffolding a new project → use `agents-get-started`
- Deploying → use `agents-deploy`
## Input
`$ARGUMENTS` can be:
- A capability: "memory", "integrate", "vpc", "multi-agent", "migrate", "browser", "code-interpreter", "payments", "teardown"
- A description of what they want: "remember user preferences", "call from React app", "scrape a website", "run pandas in the agent", "delete my agent", "clean up resources"
- Empty — the skill will determine the workflow from context
## Process
### Step 0: Verify CLI version
Run `agentcore --version`. This skill requires v0.9.0 or later.
If older: "Run `agentcore update` to get the latest version."
### Step 1: Read project context
Read `agentcore/agentcore.json` to understand the current project — framework, existing resources, agent configuration.
If `agentcore/agentcore.json` is not found:
1. **Check if the developer is in the wrong directory.** Look for `agentcore/agentcore.json` in parent directories (up to 3 levels). If found, tell them: "Found an AgentCore project at `<path>`. Are you working in that project?"
2. **If no project exists anywhere nearby**, ask what capability they wanted to add. Then offer two paths:
- "I can walk you through creating a project first and then adding CAPABILITY — want to do that?" (run the get-started flow inline, then continue with the build workflow)
- "If you already have a project elsewhere, `cd` into it and try again."
Do not just say "go use agents-get-started" and stop — that loses the developer's context about what they actually wanted to do.
### Step 2: Determine the workflow
**Important disambiguation** — before routing to a build reference, check if the prompt is actually a connect or debug concern:
- If the phrase mentions external APIs, Lambda functions, OpenAPI specs, gateways, credentials, MCP servers, or policies → this is `agents-connect`, not build
- If the developer says something is broken (wrong answers, errors, tool failures) → this is `agents-debug`, not build
- Build is for **adding new capabilities** to a working project, not fixing broken ones
Based on the developer's prompt and `$ARGUMENTS`, load the appropriate reference:
| Developer intent | Reference to load |
|---|---|
| Add memory, remember things, user preferences, cross-session | [`references/memory.md`](references/memory.md) |
| Call agent from app, invoke from code, streaming, SDK client, agent URL, execute shell in session | [`references/integrate.md`](references/integrate.md) |
| VPC, private network, RDS, internal API, subnet, security group | [`references/vpc.md`](references/vpc.md) |
| Multi-agent, orchestrator, specialist, A2A, delegation, agent handoff | [`references/multi-agent.md`](references/multi-agent.md) |
| Custom headers from caller to agent, header allowlist, tenant ID/correlation ID/trace propagation | [`references/request-headers.md`](references/request-headers.md) |
| Migrate Bedrock Agent, import agent, move to AgentCore | [`references/migrate.md`](references/migrate.md) |
| Browser tool, web navigation, form filling, scraping, Nova Act, Playwright, live view | [`references/browser.md`](references/browser.md) |
| Code Interpreter, execute code, sandbox, run Python/JS/TS, data analysis in agent, pandas | [`references/code-interpreter.md`](references/code-interpreter.md) |
| Payments, pay for x402 or MPP content, 402 Payment Required, Machine Payments Protocol, WWW-Authenticate: Payment, microtransactions, paid API/tool, payment manager/connector | [`references/payments.md`](references/payments.md) |
| Delete agent, remove resource, tear down, clean up, destroy, start fresh | [`references/teardown.md`](references/teardown.md) |
| Change model, switch model, use Haiku/Sonnet/Nova, different model | Inline — see "Changing the model" below |
If the developer asks about the difference between local dev and deployed (e.g., "why does my memory work after deploy but not locally?"), load [`references/local-vs-deployed.md`](references/local-vs-deployed.md) alongside the specific workflow reference.
Read the matching file into context and follow its Process section step by step — do not summarize.
If the intent is ambiguous, ask the developer which capability they want to add.
### Changing the model
The model is configured in `app/<AgentName>/model/load.py` (scaffolded by `agentcore create`). To change it:
1. Open `app/<AgentName>/model/load.py`
2. Change the `model_id` parameter in the `BedrockModel()` constructor
```python
# Default (scaffolded by CLI)
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
# Switch to Haiku for cost savings
return BedrockModel(model_id="us.anthropic.claude-3-5-haiku-20241022-v1:0")
# Switch to Nova Lite
return BedrockModel(model_id="amazon.nova-lite-v1:0")
```
Cross-region inference profile prefixes (`us.`, `eu.`, `apac.`, `global.`) control where inference runs. Use `global.` for maximum throughput, or a geographic prefix for data residency. Not all models support all prefixes — check the Bedrock inference profiles docs.
After changing the model:
- Verify the model is enabled in your region: AWS Console → Amazon Bedrock → Model access
- For cross-region profiles, enable in all destination regions
- If using `agents-harden`, update the IAM policy to scope to the new model ARN
- Run `agentcore dev` to test locally, then `agentcore deploy` to update the deployed agent
No `agentcore.json` change is needed — the model is configured in code, not in the project config.
### Pre-flight: validate any `--name` before generating the CLI command
Whichever reference you load, most end up producing an `agentcore add <resource> --name <something>` command. The CLI fails **late** on invalid names — you'll see the error after walking through prompts, not before running the command. Validate up front:
| Resource | Max chars | Allowed | Starts with |
|---|---|---|---|
| Agent (`add agent`) | 48 | alphanumeric + `_` | letter |
| Memory, gateway, gateway-target, credential, evaluator, online-eval, policy, policy-engine, payment-manager, payment-connector | 48 | alphanumeric + `_` | letter |
Count the characters before constructing the command. If the name is over the limit or contains hyphens, dots, or spaces, push back: "`<name>` is N characters / uses `-`, which the CLI rejects. How about `<suggestion>`?" Never run the command with an invalid name hoping the CLI message will be clear.
Note: `agentcore create --name` (the project name) has a **stricter 23-char limit** and does not allow underscores. That's covered in `agents-get-started`; if you see the developer re-running create, flag the 23-char limit specifically.
## Output
Depends on the workflow — see the loaded reference for specific outputs.
## Quality criteria
- The correct reference was loaded based on the developer's intent
- All output follows the loaded reference's quality criteria
- Cross-references to other skills (agents-connect, agents-deploy) are included where relevant