Skip to content

reconcile PR #387 remediation — A2A hardened (PR-B, best of both + review 4638092590)#20

Draft
plauzy wants to merge 14 commits into
feat/agentic-protocols-generative-uifrom
reconcile/pr387-a2a-hardened
Draft

reconcile PR #387 remediation — A2A hardened (PR-B, best of both + review 4638092590)#20
plauzy wants to merge 14 commits into
feat/agentic-protocols-generative-uifrom
reconcile/pr387-a2a-hardened

Conversation

@plauzy

@plauzy plauzy commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Why (answer before "what")

  1. What outcome are we trying to change?
    The reconciled PR-B: both blocking findings of the upstream feat: AG-UI protocol adapter + agentic protocol surface & generative UI (#386) awslabs/cli-agent-orchestrator#387 review closed with the objectively better implementation per axis from the two independent remediations (PRs remediate PR #387 review — A2A hardened (PR-B shape, Claude implementation) #12/[DRAFT][Kiro] remediate PR #387 review — A2A hardened (PR-B shape) #13), per the approved plan (#17) — plus the two findings of review 4638092590 that post-dated both remediations.

  2. What would have to be true for that outcome to look different than today?
    Per-method auth verified against real JWTs with the strictest enforcement order; capacity refusals that HTTP-native infrastructure understands; a mount guard that is both unit-testable and integration-tested; peer-supplied task ids that can never overwrite existing tasks; and a diff that rebases cleanly onto a landed PR-A.

  3. What is the smallest change that makes those things true?
    Start from claude/pr387-a2a-hardened (real-JWT test foundation, auth-before-parse + WWW-Authenticate, the reset_jwks_cache repair) and port the Kiro implementation's adjudicated wins (429/RESOURCE_EXHAUSTED, extracted guard + decision table, router-level stream dependency, the admin/unknown-method matrix cases) as five focused commits, adding the 4638092590 fixes.

  4. How will we know it worked?
    The union auth matrix (~40 real-JWT cases, nothing in the token path stubbed), the id-integrity suite, the 8-case guard decision table + 3 lifespan integration tests, and the authenticated e2e roundtrip — all green (below).


Summary of changes (on top of the claude/pr387-a2a-hardened history)

  • No dependency changes (Kiro's sequencing): the pyproject/uv.lock hygiene hunk is reverted — authlib→dev and the multipart removal land exactly once, in the core PR, so this branch rebases cleanly onto a landed PR-A per the agreed core-first order.
  • Store-full = RESOURCE_EXHAUSTED at HTTP 429 + Retry-After: 30 (Kiro's semantics): capacity is a transport/backoff condition — proxies and retry middleware react to 429 without understanding A2A codes — while domain errors still ride 200; this matches how 401/403 already carry their HTTP statuses here. The JSON-RPC error body is kept for clients that ignore transport status. TaskLimitExceededTaskStoreFull.
  • Tolerant env bounds: from_env survives malformed or non-positive CAO_A2A_MAX_TASKS/CAO_A2A_TASK_TTL values — default + warning, never a startup crash, and never "disable the bound" (a zero cap would silently reopen the DoS; previously int('abc') raised and 0 meant refuse-everything, both undocumented).
  • Review 4638092590 (post-dated both remediations): task.send is now idempotent-create — a resubmitted id is refused with INVALID_PARAMS and the stored task is untouched (previously any peer could overwrite/cancel another peer's in-flight task by resubmitting its id); Task.from_dict uses data.get("id", "") so an omitted id takes the server-UUID path instead of raising KeyError. test/a2a/test_task_id_integrity.py pins both.
  • Mount guard (both implementations): the fail-closed decision is a pure _should_mount_a2a() again (Kiro's shape) with the 8-case parametrized decision table, while the three lifespan integration tests (real listener + caplog on the refusal) keep covering the wiring.
  • Router-level stream auth (Kiro): the cao:read gate moves to the APIRouter so any future /a2a/v1 route inherits it instead of shipping unauthenticated by omission.
  • Auth matrix completion: the sibling's unique cases re-minted as real RS256 JWTs (their originals ran on a stubbed token layer) — cao:admin satisfies every method; authenticated unknown-method → METHOD_NOT_FOUND (404 + JSON-RPC body) while anonymous callers learn nothing — plus the bearer edges neither suite covered: case-insensitive scheme, empty bearer → 401, duplicate Authorization headers pinned to first-wins.
  • Docs: docs/auth.md merges the per-method scope table with the curl walkthrough, updated for 429/Retry-After and create-only ids; the CHANGELOG bullet matches the final behavior with a precise anchor.

Unchanged from the base history: per-method enforcement resolved before any body parse (WWW-Authenticate: Bearer on 401), real-JWKS test fixtures, the reset_jwks_cache() repair, timestamp-based oldest-terminal eviction, and the authenticated e2e roundtrip.

Test plan

  • Unit / integration tests pass — 3,609 passed / 21 skipped / 0 failed (uv run pytest; one environmental failure in an earlier run was a stale :9890 listener from manual demo servers, not reproducible with clean ports)
  • Authenticated e2e round-trip (pytest -m e2e test/e2e/test_a2a_roundtrip.py2 passed): 401 untokened, then the full send→poll lifecycle with real JWTs
  • mypy strict passes (uv run mypy src/ → no issues, 142 files); black + isort clean
  • Auth suite 17, store bounds 8, id-integrity 4, mount guard 11 (3 integration + 8 decision-table) — all real-token paths, nothing stubbed
  • Default-off unchanged: auth layer off → untokened behavior byte-identical; non-loopback + no-auth → task routes refuse to mount (unit + integration)

Provider tier

  • Tier 1 — unit/integration, no provider boot needed

🤖 Generated with Claude Code

https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s


Generated by Claude Code

claude added 10 commits July 6, 2026 16:19
Blocking finding from the awslabs#387 review (independently reproduced by
@fanhongy): the A2A routers mounted with no authentication while their
docstrings claimed JWKS enforcement — any network peer could
submit/cancel/read tasks with zero credentials once the listener was bound
non-loopback.

- POST /a2a/v1/rpc resolves the caller's scopes from the Authorization
  bearer BEFORE parsing or method dispatch (an unauthenticated peer learns
  nothing about the method surface), then gates per method: task.send /
  task.cancel require cao:write, task.get requires cao:read (write implies
  read, matching every other CAO read endpoint). Failures carry both the
  HTTP status (401/403) and a JSON-RPC error body (UNAUTHENTICATED /
  PERMISSION_DENIED — new code) for clients that ignore transport status.
- GET /a2a/v1/tasks/{id} and /a2a/v1/stream/{id} gain the standard
  require_any_scope(cao:read, ...) dependency.
- Token-parse failures (malformed/expired/JWKS outage) map to clean 401s,
  never 500s. Default-off (auth disabled) behavior is byte-identical.
- Module docstrings now describe the enforcement that actually exists;
  tests assert the old false claim is gone.
- security/auth.py gains the reset_jwks_cache() that test/conftest.py's
  auth_enabled_env fixture has referenced since it landed (latent break).

Tests: 12-case matrix (missing/malformed/expired bearer, read-vs-write
scope per method, auth-off unchanged, stream/REST routes) against a live
in-process JWKS with real RS256 tokens — nothing in the auth path stubbed —
plus an authenticated e2e round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jh2kqS95Qg7nP69DfH9zKb
Blocking finding from the awslabs#387 review: the store was an unbounded dict, so
a network-reachable task.send was a remote memory-exhaustion vector.

- max_tasks (CAO_A2A_MAX_TASKS, default 1000): on overflow the oldest
  terminal tasks are evicted first; when the store is full of live tasks a
  new insert raises TaskLimitExceeded, which the RPC layer maps to the new
  TASK_LIMIT_EXCEEDED JSON-RPC error so peers back off — live work is never
  silently dropped
- ttl_seconds (CAO_A2A_TASK_TTL, default 3600): idle tasks are lazily swept
  on access
- updates to an existing task id never hit the capacity check

Note the severity coupling the review itself acknowledged: remote
exploitability required the missing auth (previous commit); the bound is
defense-in-depth behind it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jh2kqS95Qg7nP69DfH9zKb
…out auth

Encodes the review's fail-closed alternative: CAO_AGENT_CARD_HOST set to a
non-loopback address with auth unconfigured used to expose the
unauthenticated task API to any network peer. The lifespan now logs an
error and skips mounting the A2A routers in that configuration (Agent Card
discovery still serves); loopback + no auth remains allowed as the
default-off dev posture. The listener store is now built via
InMemoryTaskStore.from_env() so the new bounds are operator-tunable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jh2kqS95Qg7nP69DfH9zKb
- authlib moves to the dev dependency group: no src code imports it (the
  Agent Card signer uses cryptography directly; test fixtures mint tokens
  with it). python-multipart is removed — nothing declares Form/File
  params. Base install stays lean; uv.lock regenerated.
- CHANGELOG A2A bullet now describes the scope gating, fail-closed mount,
  and store bounds.
- docs/auth.md gains the A2A transport section: per-method scope table,
  the two safety properties, and a 401 -> 403 -> 200 curl walkthrough.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jh2kqS95Qg7nP69DfH9zKb
Reconciliation plan P2-B step 2 (the Kiro sequencing): authlib->dev and
the python-multipart removal land exactly once, in the AG-UI core PR,
so this branch rebases cleanly onto a landed PR-A per the agreed
core-first order. pyproject/uv.lock return to the f40933d state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
…; tolerant env bounds

Reconciliation plan P2-B steps 3-4 — the Kiro error semantics on the
Claude store internals:

- a full-of-live-tasks store refuses task.send with the gRPC-canonical
  RESOURCE_EXHAUSTED code carried on HTTP 429 + Retry-After: 30, plus
  the JSON-RPC error body. Capacity is a transport/backoff condition —
  HTTP-native retry middleware and proxies react to 429 without
  understanding A2A codes — while domain errors (TASK_NOT_FOUND,
  TASK_ALREADY_TERMINAL) still ride 200; this matches how 401/403
  already carry their HTTP statuses on this endpoint. Exception renamed
  TaskLimitExceeded -> TaskStoreFull.
- InMemoryTaskStore.from_env survives malformed or non-positive env
  values: fall back to the default with a warning — never a startup
  crash, and never 'disable the bound' (a zero cap would silently
  reopen the unbounded-store DoS; previously int('abc') raised and
  max_tasks=0 meant refuse-everything, both undocumented).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
Closes the two findings of upstream review 4638092590 that post-dated
both remediations (reconciliation plan P2-B step 5):

- must-fix: task.send accepted a peer-supplied id verbatim and upserted
  it — any peer could overwrite or effectively cancel another peer's
  in-flight task by resubmitting its id. task.send is now
  idempotent-create: an existing id is refused with INVALID_PARAMS and
  the stored task is untouched (asserted). Internal state transitions
  (store.upsert/transition) are unaffected.
- nit: Task.from_dict used data['id'] and raised KeyError when a peer
  omitted the field; data.get lets the omitted/empty id take the
  documented server-generated-UUID path.

test/a2a/test_task_id_integrity.py pins both, plus the survival of the
original task after a stomp attempt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
…th + matrix completion

Reconciliation plan P2-B steps 6-8:

- the fail-closed mount decision is a pure function again
  (_should_mount_a2a, from the Kiro remediation) called by the lifespan
  guard — the 8-case decision table is unit-tested without binding a
  socket while the three lifespan integration tests (real listener,
  caplog on the refusal) keep covering the wiring;
- the stream router's auth dependency moves to ROUTER level: a future
  route added to /a2a/v1 inherits the cao:read gate instead of shipping
  unauthenticated by omission (both routes' per-route tests unchanged);
- the auth matrix gains the sibling remediation's unique cases,
  re-minted as real RS256 JWTs (their originals ran on a stubbed token
  layer): cao:admin satisfies send/get/cancel; an AUTHENTICATED caller
  gets METHOD_NOT_FOUND (HTTP 404 + JSON-RPC body) for unknown methods
  while anonymous callers still learn nothing; plus the bearer-parsing
  edges neither suite covered — case-insensitive scheme, empty bearer
  → 401, duplicate Authorization headers pinned to first-header-wins so
  a smuggled second credential cannot upgrade a rejected first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
…, create-only ids

Reconciliation plan P2-B step 9: the Kiro remediation's per-method scope
table joins this branch's curl walkthrough in docs/auth.md, updated for
the reconciled semantics (RESOURCE_EXHAUSTED at HTTP 429 + Retry-After;
tolerant env bounds; create-only task ids per review 4638092590). The
CHANGELOG bullet adopts the richer wording and the precise section
anchor, truthful to this branch's final behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
plauzy pushed a commit that referenced this pull request Jul 7, 2026
…12,#17,#19,#20)

Add PR #19/#20 (Claude's executed reconciliations) to the branch inventory, add a dedicated Section 2.1 mandatory-audit guarantee with a per-PR checklist (full-diff read, claims-vs-code verification, real gate run, wins/regressions/gaps, review 4638092590 + media verification), reframe the reconciliation approach so #19/#20 are the strongest existing baseline Kiro must audit rather than regenerate, extend the audit matrix and /cao-eval rubric (new claims-vs-code fidelity criterion) to score #19/#20, and add hard constraints to audit all five and not modify #19/#20.
plauzy pushed a commit that referenced this pull request Jul 7, 2026
…erify-and-author mission

- Add docs/reviews/pr387-kiro-handoff.md: authoritative, self-contained
  kickoff prompt making Kiro the finishing author (verify #19/#20, fix on
  reconcile/*, author final upstream submission).
- Realign Section 0 of the orchestration plan to the reframed mission:
  independently verify the already-existing reconcile branches, fix directly
  on them (no new variants / no fresh reconciliation), author the final PR
  bodies + upstream reply, then retarget upstream with maintainer approval.
- Point Section 0 at the handoff as the operative document; note plan #18 is
  superseded by #19/#20 and retained as reference. Sections 1-11, media gate,
  and Definition of Done left intact.
…on stop

uvicorn's Server.serve() returns immediately after startup() (which binds
the listening socket) when should_exit is already set, skipping the
shutdown() that closes it. AgentCardListener.stop() only set should_exit,
so a fast start->stop could leave :9890 bound until GC — surfacing as an
EADDRINUSE flake when consecutive lifespans start the listener (e.g. the
A2A mount-guard tests). Close any bound sockets explicitly after the serve
task ends, and log (not raise) a start-up failure so shutdown still runs.
The round-trip drives two peers as httpx.ASGITransport apps and needs no
terminal-backed agent, but the session-autouse require_tmux fixture in
test/e2e/conftest.py skipped it wholesale on tmux-less hosts. Override
require_tmux locally (matching the existing require_cao_server /
warmup_mcp_server_cache overrides) so the real-JWT round-trip actually
executes in minimal CI/sandboxes instead of silently skipping.
…odies, follow-ups)

Independent Phase-1 verification record, draft PR bodies for PR-A/#19 and
PR-B/#20, the four follow-up issue drafts the upstream reply commits to, and
git-am patches for the two a2a verification fixes. Docs only; no code change.
plauzy pushed a commit that referenced this pull request Jul 7, 2026
PR #19 body covers: AG-UI typed-event stream, generative UI, standalone
PWA, mock_cli provider, opt-in OTel, WS auth hardening, example fixes.
Gate results: 3519 passed / mypy 132 files / PWA 24 tests / lint clean.

PR #20 body covers: A2A JSON-RPC transport, signed Agent Card, per-method
auth enforcement, bounded store with 429+RESOURCE_EXHAUSTED, idempotent-
create, Task.from_dict fix, real-JWT test matrix.
Gate results: 3590 passed / mypy 142 files / lint clean.

Both drafts credit the two source implementations neutrally and mark
unverified claims (Playwright/Chromium) explicitly.
Phase-1 verification fixes for PR-B (a2a-hardened) + finishing docs
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.

3 participants