Skip to content

PR #387 Phase-B: hardened A2A v1.0 transport + signed Agent Card (reconciled, single commit) - #23

Closed
plauzy wants to merge 1 commit into
kiro/pr387-phase-a-agui-corefrom
kiro/pr387-phase-b-a2a-hardened
Closed

plauzy wants to merge 1 commit into
kiro/pr387-phase-a-agui-corefrom
kiro/pr387-phase-b-a2a-hardened

Conversation

@plauzy

@plauzy plauzy commented Jul 12, 2026

Copy link
Copy Markdown
Owner

This pull request was created by @kiro-agent on behalf of @plauzy 👻

Comment with /kiro fix to address specific feedback or /kiro all to address everything.
Learn about Kiro Web


What this is

This closes out Phase-B of awslabs#387 as a single, net-new commit, stacked on top of Phase-A (#22). It reproduces the reconciled A2A hardening work — "best of both implementations" plus review 4638092590, adjudicated in #20 — squashed into one authored change.

  • Authored by plauzy + Kiro (Co-authored-by: Kiro Agent), matching awslabs/cli-agent-orchestrator@33c593d.
  • Base branch is kiro/pr387-phase-a-agui-core (Phase-A), so this PR shows only the incremental A2A surface.
  • 40 files, +4,630 / −10 — purely the hardened A2A/agent-card surface, its tests, docs, and wiring.

Review Phase-A (#22) first; merge it, then this diff is exactly the A2A addition.

Scope (additive, default-off)

The Agent Card / A2A listener runs on its own port (:9890), gated by CAO_AGENT_CARD_ENABLED, binds loopback unless an operator opts into external discoverability. Untokened behavior stays byte-identical with the auth layer off.

  • A2A v1.0 JSON-RPC 2.0 (task.send/get/cancel), SSE streaming, REST polling.
  • Ed25519-signed Agent Card + JWKS on the dedicated listener.
  • Per-method scope enforcement resolved before any body parse (send/cancel → cao:write; get/reads → cao:read; cao:admin satisfies any), 401/403 with WWW-Authenticate + JSON-RPC bodies; the stream gate lives at the APIRouter.

Hardening (best-of-both + review 4638092590)

  • Fail-closed mount guard — pure _should_mount_a2a() refuses to mount task routes on a non-loopback bind while auth is disabled; the Agent Card doc still publishes.
  • Capacity = transport condition — a full store refuses task.send with RESOURCE_EXHAUSTED at HTTP 429 + Retry-After: 30 (domain errors still ride 200); bounded (CAO_A2A_MAX_TASKS) + TTL-evicting (CAO_A2A_TASK_TTL).
  • Tolerant env bounds — from_env survives malformed/non-positive values (default + warning), never disables the bound.
  • Create-only task ids — a resubmitted id is refused with INVALID_PARAMS and the stored task is untouched; Task.from_dict tolerates an omitted id.

Shift-left testing

  • Real-JWT auth matrix (test/a2a/test_auth_enforcement.py) — nothing in the token path stubbed.
  • Store bounds, create-only id integrity, RPC/stream + Agent Card builder/router/signing suites (test/a2a/*, test/agent_card/*).
  • 8-case parametrized mount-guard decision table + lifespan integration (test/api/test_a2a_mount_guard.py).
  • Authenticated e2e round-trip (test/e2e/test_a2a_roundtrip.py).
  • Phase-A's "A2A absent" default-off assertion is reconciled to "present but default-off" (test/api/test_default_off_listeners.py).
  • Verified locally: the A2A + agent-card unit suites pass. One lifespan test is sensitive to :9890 port-contention when run back-to-back in-process (a pre-existing quirk noted in reconcile PR #387 remediation — A2A hardened (PR-B, best of both + review 4638092590) #20); it passes in isolation.

Video verification proof

Builds on Phase-A's recorded walkthroughs (docs/media/agui-*.webm); the A2A verification artifacts and applied review patches are captured under docs/reviews/pr387-a2a-fixes/ and docs/reviews/pr387-phase1-verification-2026-07-07.md.

No dependency changes: authlib→dev and the python-multipart removal already landed in Phase-A, so this rebases cleanly.

Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>

…bs#387 Phase-B (reconciled)

Stacks on Phase-A (the AG-UI core) and closes out Phase-B of awslabs#387 as a
single, net-new commit. This squashes the reconciled A2A hardening work (the
"best of both implementations" plus review 4638092590, adjudicated in
#20) into one authored change, layered on top of the core PR so it can be
reviewed as a stacked PR.

Additive and default-off: the Agent Card / A2A listener runs on its own port
(:9890), is gated by CAO_AGENT_CARD_ENABLED, and binds loopback unless an
operator opts into external discoverability. With the auth layer off, the
untokened behavior stays byte-identical.

A2A transport & Agent Card
- A2A v1.0 JSON-RPC 2.0 (task.send / task.get / task.cancel), SSE streaming,
  and REST polling for agent-to-agent interop.
- Ed25519-signed Agent Card + JWKS published on the dedicated listener.
- Per-method scope enforcement resolved BEFORE any body parse
  (task.send/cancel -> cao:write; task.get and reads -> cao:read; cao:admin
  satisfies any), returning 401/403 with WWW-Authenticate and JSON-RPC error
  bodies; the stream gate lives at the APIRouter so future routes inherit it.

Hardening (best-of-both + review 4638092590)
- Fail-closed mount guard: the pure _should_mount_a2a() decision refuses to
  mount the task routes on a non-loopback bind while auth is disabled; the
  Agent Card discovery doc still publishes.
- Capacity as a transport condition: a full store refuses new task.send with
  RESOURCE_EXHAUSTED at HTTP 429 + Retry-After: 30 (domain errors still ride
  200); bounded (CAO_A2A_MAX_TASKS) + TTL-evicting (CAO_A2A_TASK_TTL).
- Tolerant env bounds: from_env survives malformed/non-positive values with a
  default + warning instead of a startup crash, and never disables the bound.
- Create-only task ids: a resubmitted id is refused with INVALID_PARAMS and the
  stored task is untouched, so a peer can never overwrite another peer's task;
  Task.from_dict tolerates an omitted id (server-UUID path).

Shift-left testing
- Real-JWT auth matrix (test/a2a/test_auth_enforcement.py) — nothing in the
  token path stubbed; per-method scopes, unknown-method METHOD_NOT_FOUND, and
  bearer edge cases (case-insensitive scheme, empty bearer, duplicate headers).
- Store bounds (test/a2a/test_store_bounds.py), create-only id integrity
  (test/a2a/test_task_id_integrity.py), RPC/stream/coverage suites, and the
  Agent Card builder/router/signing suites (test/agent_card/*).
- 8-case parametrized mount-guard decision table + lifespan integration
  (test/api/test_a2a_mount_guard.py); the core PR's "A2A absent" default-off
  assertion is reconciled to "present but default-off".
- Authenticated e2e round-trip (test/e2e/test_a2a_roundtrip.py): 401 untokened,
  then the full send -> poll lifecycle with real JWTs.

Video verification proof
- Builds on the Phase-A recorded walkthroughs under docs/media/
  (agui-generative-ui-demo.webm, agui-live-remediation-demo.webm); the A2A
  verification artifacts and applied review patches are captured under
  docs/reviews/pr387-a2a-fixes/ and docs/reviews/pr387-phase1-verification-*.

No dependency changes: authlib->dev and the python-multipart removal already
landed in the core PR, so this rebases cleanly onto Phase-A.

Co-authored-by: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
@plauzy

plauzy commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Closing at the author's own request, recorded in #25's body: 'Supersedes the unsigned #23 (same content, now SSH-signed → Verified). Please close #23.' A symbol-level containment check across all ten of this PR's a2a/agent_card modules finds zero symbols absent from #25. Seven non-code files that #25 does NOT carry have been archived to disk first: the six docs/reviews/ files (two of them literal .patch files) and docs/auth.md. For the record, this PR's 40-file diff remains the cleanest shape of the A2A delta and is the shape a fresh upstream submission should copy — #25 re-presents 104 files of already-landed Phase-A work alongside it. Branch preserved.

@plauzy plauzy closed this Sep 28, 2026
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.

1 participant