Skip to content

fix: serve the A2A protocol contract in the sub-agents - #28

Merged
Robobc merged 1 commit into
mainfrom
fix/a2a-protocol-contract
Aug 19, 2026
Merged

fix: serve the A2A protocol contract in the sub-agents#28
Robobc merged 1 commit into
mainfrom
fix/a2a-protocol-contract

Conversation

@Robobc

@Robobc Robobc commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Closes #16.

What

The A2A sub-agents have never been invocable. They were built on BedrockAgentCoreApp — the HTTP contract, /invocations on port 8080 — while AgentCore routes A2A traffic to a JSON-RPC server on port 9000 with an agent card. Every InvokeAgentRuntime returned HTTP 424, container logs were clean, and module 8's verify only read an SSM parameter — so nothing ever caught it.

Changes

  • agent-code/shared/a2a_serve.py — serves a Strands agent on the contract. A2AServer gives / (JSON-RPC) and /.well-known/agent-card.json but not /ping, which the contract also requires; this adds it.
    • The ping deliberately omits time_of_last_update. Per the contract docs' own warning, a timestamp that advances on every ping reads as a continuous status change: the idle timeout never fires, sessions run to MaxLifetime, and the account's session quota drains.
  • code-agent / research-agent — serve via the helper; gain name + description so they're discoverable on the agent card. research-agent keeps its gateway tools (web search).
  • DockerfilesEXPOSE 9000, healthcheck on :9000/ping, both copy shared/ (so code-agent's build context moves to agent-code/, matching every other shared-code pattern).
  • requirementsstrands-agents[a2a]; without the extra the import fails at container start.
  • invoke.py --a2a <component> — invokes a sub-agent properly. Two things differ from the orchestrator and each produces a misleading error: the payload is a JSON-RPC message/send envelope (wrong shape → 424), and A2A is not client-facing so those runtimes have no JWT authorizer and need SigV4 (bearer → Authorization method mismatch). I hit the auth half live.
  • MODULE_VERIFY[8] now runs that real invoke. A verify that cannot fail for the thing the module promises is what let this survive.

Verified

  • Live — the first successful A2A invocations in this project's history:
    • invoke.py --a2a code-agent "Reply with exactly: A2A OK"AGENT TEXT: A2A OK
    • invoke.py --a2a research-agent … → answered a current-events question through the gateway's web search, with a cited URL
    • both stacks CREATE_COMPLETE
  • Contract checked inside the container first: /ping returns exactly {"status":"Healthy"}, the agent card renders, and message/send returned a proper artifact.
  • tests/test_a2a_contract.py — 16 static guards (port, /ping, the timestamp trap, no BedrockAgentCoreApp, EXPOSE 9000 and no 8080, COPY shared/, the a2a extra, build context, and that module 8 actually invokes). 12 fail against the previous code.
  • pytest 115 passed · check-workshop-flow + check-deploy-config pass · hadolint clean at error threshold · ruff clean on changed files.
  • Docs updated: the 424 entry no longer describes this as a known break, and the auth-mismatch entry gained a both-directions table.

The A2A sub-agents have never been invocable. They were built on
BedrockAgentCoreApp — the HTTP contract, /invocations on port 8080 — while
AgentCore routes A2A traffic to a JSON-RPC server on port 9000 with an agent
card. Every InvokeAgentRuntime returned HTTP 424 with clean container logs, and
module 8's verify only read an SSM parameter, so nothing ever noticed.

- agent-code/shared/a2a_serve.py: serve a Strands agent on the contract.
  Strands' A2AServer provides "/" (JSON-RPC) and /.well-known/agent-card.json
  but NOT /ping, which the contract also requires, so this adds it. The ping
  deliberately omits time_of_last_update: a timestamp that advances on every
  ping reads as a continuous status change, the idle session timeout then never
  fires, sessions run to MaxLifetime and drain the account's session quota
  (per the contract docs' own warning).
- code-agent / research-agent: serve via the helper instead of
  BedrockAgentCoreApp, and gain a name + description so they are discoverable
  on the agent card. research-agent keeps its gateway tools (web search);
  the MCP client is built at startup and its OAuth token is fetched per
  session, so a long-lived process still gets fresh credentials.
- Dockerfiles: EXPOSE 9000, healthcheck on :9000/ping, and both copy shared/.
  code-agent's build context therefore moves to agent-code/ (app.py updated
  with dockerfile_pattern, matching how every other shared-code pattern builds).
- requirements: strands-agents[a2a] — A2AServer needs the a2a-sdk extra or the
  import fails at container start.
- scripts/invoke.py --a2a <component>: invoke a sub-agent properly. Two things
  differ from the orchestrator and each produces a misleading error: the
  payload is a JSON-RPC message/send envelope (wrong shape → 424), and A2A is
  not a client-facing protocol so those runtimes have no JWT authorizer and
  need SigV4 (bearer token → "Authorization method mismatch"). Found the auth
  half the hard way during live testing.
- MODULE_VERIFY[8] now runs that real invoke instead of reading an SSM
  parameter. A verify that cannot fail for the thing the module promises is
  what let this survive.

Verified:
- LIVE, first successful A2A invocations in this project's history:
  `invoke.py --a2a code-agent "Reply with exactly: A2A OK"` → "A2A OK", and
  `--a2a research-agent` answered a current-events question via the gateway's
  web search with a cited URL. Both stacks CREATE_COMPLETE.
- Contract checked inside the container before deploying: /ping returns
  exactly {"status":"Healthy"}, the agent card renders, and a JSON-RPC
  message/send returned a proper artifact.
- tests/test_a2a_contract.py: 16 static guards (port, /ping presence, the
  timestamp trap, no BedrockAgentCoreApp, EXPOSE 9000 and no 8080, COPY
  shared/, the a2a extra, build context, and that module 8's verify actually
  invokes). 12 of them fail against the previous code.
- pytest 115 passed; check-workshop-flow and check-deploy-config pass;
  hadolint clean at error threshold; ruff clean on changed files.
- docs/TROUBLESHOOTING.md: the 424 entry no longer describes this as a known
  break, and the auth-mismatch entry now has the both-directions table.
  PARTICIPANT_GUIDE module 8 row and verify list updated.
@Robobc
Robobc merged commit a6f1786 into main Aug 19, 2026
8 of 9 checks passed
@github-actions

Copy link
Copy Markdown

Commit: 16ce1d6 | Updated: 2026-08-19 18:13:29 UTC

Security Scan Results

Scanner S C H M L I Time Action Result Thresh
bandit 0 0 0 0 20 0 965ms 0 PASSED MED (g)
cdk-nag 0 0 0 0 0 0 7.5s 0 PASSED MED (g)
cfn-nag 0 0 0 0 0 0 8ms 0 PASSED MED (g)
checkov 0 0 0 0 0 0 6.1s 0 PASSED MED (g)
detect-secrets 0 0 0 0 0 0 963ms 0 PASSED MED (g)
grype 0 0 0 0 0 0 1m 3s 0 PASSED MED (g)
npm-audit 0 0 0 0 0 0 198ms 0 PASSED MED (g)
opengrep 0 2 0 0 0 0 20.1s 2 FAILED MED (g)
semgrep 0 0 0 0 0 0 <1ms 0 MISSING MED (g)
syft 0 0 0 0 0 0 2.8s 0 PASSED MED (g)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A sub-agents don't implement the A2A protocol contract (424 on invoke)

1 participant