Field Notes: 25 seconds to detect and hand off. Nothing executes until a human says yes.
Two agents, on two different frameworks, detect a real AWS incident and hand it off over A2A. One proposes a fix. Nothing executes until a human approves, enforced by Cedar.
On this page
A Strands agent on Amazon Bedrock AgentCore found a real CloudWatch alarm, worked out what it meant, and handed the finding to a LangGraph agent over A2A. Median time, five runs: 25.3 seconds. The LangGraph agent proposed a fix. It did not apply it. Nothing applies anywhere in this system until a human types y, and that is enforced by a Cedar policy on an AgentCore Gateway, not by a line in a prompt asking nicely.
I had Claude Code build and deploy the whole thing in my own AWS account, including hitting the IAM gotchas below for real. The demo’s first working version had the agents built, the A2A protocol measured, and the real CloudWatch tools deployed, and still fed both agents a hardcoded finding instead of a real one. It ran, it measured something true, and it was not the story: nothing had detected anything. Two review passes caught that before anything shipped, and the fix is what this post is about.
In short
- Two agents, built in different frameworks, detect a real CloudWatch alarm and hand it off to each other over A2A: 25.3 seconds median, five of five real runs.
- The remediation agent only proposes an action. Nothing executes until a human approves, enforced by a Cedar policy on an AgentCore Gateway, tested by trying to break it: no approval denied, wrong resource denied.
- A2A’s native-invoke shortcut and the full protocol turn out to be the same API call, and the AWS SDK cannot make either once OAuth is on.
The setup
A monitoring agent on Strands has real, read-only CloudWatch tools: list alarms in ALARM state, pull metric statistics, filter logs. A remediation agent on LangGraph matches a finding against a small local runbook and asks a model for a grounded recommendation. Different frameworks on purpose, because the point of A2A is that it does not care what you built the other side in. Not Bedrock Agents’ own supervisor/sub-agent collaboration, which has done this natively since March 2025, because that requires both sides to be Bedrock Agents; the whole reason to reach for A2A instead is when they are not. Both run as AWS::BedrockAgentCore::Runtime A2A servers behind a shared Cognito user pool, and the monitoring agent authenticates itself to the remediation agent through an AgentCore Identity outbound credential provider, never a secret in its code, environment, or image.
Phase one: detection and hand-off
Five runs. Each one, the monitoring agent called its own CloudWatch tools, found the real test alarm, and confirmed the metric value against it. Not a hardcoded string: an actual describe-alarms and get-metric-statistics check against ground truth, every run, with misses counted rather than retried away. Five out of five found it.
What 25.3 seconds measures, precisely: the clock starts when the test client asks the monitoring agent to check for incidents, and stops when that same client has the remediation agent’s recommendation back in hand, already including detection, the A2A hand-off, and the recommendation itself. Not the time the alarm had already been sitting in ALARM before anyone asked. Five runs: 23.741s, 25.270s, 23.929s, 26.418s, 25.398s. Median 25.270s, straight from docs/transcript.txt, the run shown below.
The median run’s real timeline, timestamps from AWS (StateUpdatedTimestamp) and from the containers themselves (UTC, not the laptop running the test):
| Step | Time | Elapsed |
|---|---|---|
| Test client’s request sent (clock starts here) | 03:37:10.71 | 0.0s |
| Monitoring agent’s request begins | 03:37:12.06 | ~1.4s |
| CloudWatch tools confirm the real alarm and metric | 03:37:17.94 | ~5.9s |
| AgentCore Identity token fetch | 03:37:18.01 | ~6.0s |
| Remediation agent’s card fetched | 03:37:25.52 | ~13.4s |
message/send reaches the remediation agent | 03:37:32.99 | ~20.9s |
Runbook matched (error_rate) | 03:37:32.99 | ~20.9s |
| Recommendation returned (Bedrock call) | 03:37:34.59 | ~22.5s |
| Full round trip complete | 03:37:35.97 | 25.3s |
That hand-off is the thing the demo’s first working version could not claim. The monitoring agent fetches the remediation agent’s own card and sends the message/send itself, with its own OAuth token. The test script only starts the run; it never calls the remediation agent directly. Two agents, two frameworks, talking to each other, not a script talking to both of them in turn.
Phase two: propose, approve, enforce
The remediation agent’s recommendation is a proposal, nothing more. Executing it needs three things to line up: a human approval, logged with a timestamp and the approver’s real IAM identity; the target resource carrying the right tag; and a Cedar policy on an AgentCore Gateway that evaluates both before anything runs.
I tested both ways it should fail, not just the way it should succeed:
- No approval. A call with
approved: falseagainst the correct, tagged resource. The real response my client captured, exact text:-32002, "Tool Execution Denied: Tool call not allowed due to policy enforcement [No policy applies to the request (denied by default).]". AWS’s own devguide shows the same “Tool Execution Denied” message wrapped differently, as a success envelope withisError: truerather than a numbered JSON-RPC error; treat the message text as the documented signal and the-32002as what this Gateway target sent back in practice, not something AWS’s docs assign a fixed meaning to. Checked independently with a rawssm:GetParameterread: the value never moved. - Wrong resource. A second, genuinely untagged SSM parameter, called with
approved: true. Same Cedar denial. The only difference between this parameter and the real one is its tags, confirmed withssm list-tags-for-resource, not assumed. - Approved, on the correct resource. Allowed. The flag flips from
falsetotrue, confirmed by reading the parameter directly, not by trusting the Lambda’s own return value. - Rollback. A second logged approval reverses it, through the identical Gateway and policy path, confirmed back to
falsethe same way.
The denial is a Cedar AuthorizeAction default-deny, evaluated by the policy engine before the Lambda target is ever invoked. runbooks.json’s own default entry already says, in English, “do not take an automated remediation action on an unclassified finding.” That line does nothing on its own. It is advice sitting next to a model that can ignore advice. The Cedar policy is what stops the call, and I built both tests specifically so that claim had to survive an attempt to break it, not just a reading of the prompt.
What it cost
Phase one: around $0.27 for the full session, almost all of it Bedrock tokens across the real detection, hand-off, and recommendation calls, pulled from CloudWatch’s own AWS/Bedrock usage metrics. Phase two made zero model calls (the proposal step is local logic against the runbook, not a new generation), so its entire cost is a handful of Gateway invocations and policy authorisations at AWS’s published list rates, a few hundredths of a cent. Neither session came close to the $5 cap I set before running either.
The proof: what A2A costs
Before any of the above worked, I measured what the A2A hand-off in phase one costs against two cheaper-looking alternatives, with the same hardcoded test finding run through all three for a clean comparison: a direct in-process call (both agents’ code, same process, no AgentCore hop at all), AgentCore’s native InvokeAgentRuntime with a raw payload and no A2A envelope, and the full A2A handshake (agent-card discovery, a Cognito OAuth token, then a JSON-RPC message/send).
| Pattern | Median (n=5) |
|---|---|
| Direct call | 2.4s |
| Native invoke, raw payload | 8.4s |
| Full A2A (control run, same finding as above) | 18.8s |
Native invoke and full A2A turned out to be the same operation, not two tiers: both go through the identical InvokeAgentRuntime call against the identical runtime ARN. The only difference is payload shape and whether agent-card discovery runs first. And the AWS SDK cannot make either call once a runtime has a JWT authoriser attached, which both of mine do: AWS’s own API reference says so directly, you drop to a raw HTTPS request with a bearer token instead. A runtime takes IAM SigV4 or JWT inbound auth, never both, and SigV4 only works if the other agent is already inside your AWS account, which defeats the entire premise of A2A, agents from different platforms talking to each other. For real cross-vendor A2A, OAuth is not a choice, it is the only option that keeps the protocol’s own promise, and that is the option that breaks boto3.
Sharp edges I hit
Getting the credential right took five live attempts, not one. The monitoring agent needs its own OAuth token to call the remediation agent, and the one rule that mattered most to me was that the token’s secret could never sit in code, an environment variable, or the container image. AgentCore Identity’s outbound-auth credential provider does this: the Cognito client secret lives in AgentCore’s own vault, fetched at call time. Getting the IAM right to let the monitoring agent fetch it took five separate AccessDeniedExceptions, each naming a different resource in the chain: the credential provider, the runtime’s workload identity, the workload-identity directory, the token vault, and finally a Secrets Manager secret AgentCore Identity stores the token in behind the scenes, which the calling role also needs direct read access to. None of that last part is documented anywhere I found ahead of hitting it.
The first working fix was itself an anti-pattern, caught in review. Faced with four different access denials, the fastest fix was two account-wide wildcards: token-vault/* and workload-identity-directory/*. It worked. It was also exactly the kind of overbroad grant this whole exercise exists to avoid shipping in a public repo. AWS publishes the narrow form for this specific case (scope-credential-provider-access in the AgentCore devguide): four exact ARNs, built from the stack’s own resource attributes, not a namespace wildcard. Redeployed with the narrow policy, reran the real hand-off twice, zero denials.
Two bugs nothing had ever exercised before. The monitoring agent’s container was missing the a2a-sdk dependency outright, latent since the very first build, because nothing had ever sent it a real A2A message before this one did. Strands’ own A2AServer doesn’t extract AgentCore’s per-request WorkloadAccessToken header on its own either; a small FastAPI middleware has to do it.
A region pin that silently did nothing. The project’s Makefile pinned the region with ?=, which is a no-op the moment the calling shell already exports AWS_REGION. Mine did, to a different region entirely. The first real test alarm was created somewhere I wasn’t looking. Fixed to :=.
AgentCore Gateway needs its own self-referential ARN before it exists. Attaching a Cedar policy engine to a Gateway at creation time triggers an authorisation check against the Gateway’s own ARN, mid-create, before AWS has finished assigning it one. The fix is the same shape as the credential-provider one above: a narrow, name-prefixed wildcard scoped to this stack’s own Gateway name, confirmed by reading the literal generated ID out of two real CREATE_FAILED deploys, not guessed.
What I now do
Treat an agent’s proposal and an agent’s permission to act as two different trust boundaries, built two different ways. The proposal can be as creative as the model wants; a runbook, a classifier, a recommendation in plain English. The permission cannot live in that same layer at all. It has to be a policy engine that evaluates a resource tag and an approval record before anything runs, and the only way I’d trust that claim about my own system is the way I tested it here: try to break it on purpose, twice, and watch it hold.
What this doesn’t tell you
Five real runs is a real result, not a load test; the 25.3 seconds will move under concurrent traffic and a cold start. Detection here is one synthetic alarm, one metric, one runbook category, in an otherwise quiet test account with nothing else in ALARM to compete for the agent’s attention. A noisy production account with several real alarms firing at once is a different, harder problem I haven’t tested, and a model-driven detector can still misread a finding type I didn’t try. The approval step in this build is a script prompting for y with a logged identity behind it, not a UI, a Slack bot, or a change-management system, though the Cedar enforcement underneath would look the same either way. And the one action I let the system execute, a feature-flag toggle, is about as low-stakes as a real action gets; a system that can touch something expensive needs the same two tests run against that action specifically, not assumed to transfer from this one. There is no idempotency key on the Gateway-to-Lambda execution call either: a retried approval after a timeout would be invisible here, because flipping a flag true to true twice looks identical to flipping it once. A non-idempotent action needs that key before any of the rest of this applies to it.
Run it yourself
The repo is rajmurugan01/agentcore-a2a-incident-response. make deploy, make incident, make demo for phase one; make tier2-demo for phase two. make destroy tears down everything in both, independently verified each time, including the test alarm and both SSM parameters.
If your agents already detect and propose: what stops the next step, a policy that denies by default, or a sentence in a prompt asking the model not to?
Sources
- Invoke an AgentCore Runtime agent (AgentCore devguide: the SDK-vs-raw-HTTPS limitation once OAuth is on)
- Deploy A2A servers in AgentCore Runtime (AgentCore devguide: the port 9000, root-path convention)
- Scope down access to credential providers by workload identity (AgentCore devguide: the exact least-privilege ARNs for outbound auth)
- Set up permissions for Policy (AgentCore devguide: Gateway and Policy Engine IAM)
- Amazon Bedrock AgentCore is now generally available (13 October 2025)
Related reading
LangGraph builds the agent. AgentCore runs it.
LangGraph and AgentCore are different layers, not rivals. Who owns each design primitive, and why your runtime decides how hard checkpointing works.
Field Notes: A Gateway policy can't see the user if your agent calls with its own token
A re:Post question asked how to stop an AgentCore agent acting beyond the user who asked. I built a demo to find out. A Cedar policy does nothing if the agent calls with its own token. Two gotchas, and the real limit: a stale token claim.
Field Notes: The AgentCore Memory write that returns success and reads back empty
AgentCore long-term memory has a read-after-write gotcha the docs skip: a direct BatchCreateMemoryRecords write returns 201 and stays unsearchable for 15 to 30 seconds. Measured, with the two-tier model that explains it.
Newsletter
A new AWS and AI engineering write-up every Tuesday, directly to your inbox.
Comments
- Loading comments…