reconcile PR #387 remediation — A2A hardened (PR-B, best of both + review 4638092590)#20
Draft
plauzy wants to merge 14 commits into
Draft
reconcile PR #387 remediation — A2A hardened (PR-B, best of both + review 4638092590)#20plauzy wants to merge 14 commits into
plauzy wants to merge 14 commits into
Conversation
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
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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why (answer before "what")
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.
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.
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, thereset_jwks_cacherepair) 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.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-hardenedhistory)RESOURCE_EXHAUSTEDat 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.TaskLimitExceeded→TaskStoreFull.from_envsurvives malformed or non-positiveCAO_A2A_MAX_TASKS/CAO_A2A_TASK_TTLvalues — default + warning, never a startup crash, and never "disable the bound" (a zero cap would silently reopen the DoS; previouslyint('abc')raised and0meant refuse-everything, both undocumented).task.sendis now idempotent-create — a resubmitted id is refused withINVALID_PARAMSand the stored task is untouched (previously any peer could overwrite/cancel another peer's in-flight task by resubmitting its id);Task.from_dictusesdata.get("id", "")so an omitted id takes the server-UUID path instead of raisingKeyError.test/a2a/test_task_id_integrity.pypins both._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.cao:readgate moves to theAPIRouterso any future/a2a/v1route inherits it instead of shipping unauthenticated by omission.cao:adminsatisfies 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, duplicateAuthorizationheaders pinned to first-wins.docs/auth.mdmerges the per-method scope table with the curl walkthrough, updated for 429/Retry-Afterand 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: Beareron 401), real-JWKS test fixtures, thereset_jwks_cache()repair, timestamp-based oldest-terminal eviction, and the authenticated e2e roundtrip.Test plan
uv run pytest; one environmental failure in an earlier run was a stale:9890listener from manual demo servers, not reproducible with clean ports)pytest -m e2e test/e2e/test_a2a_roundtrip.py→ 2 passed): 401 untokened, then the full send→poll lifecycle with real JWTsmypystrict passes (uv run mypy src/→ no issues, 142 files);black+isortcleanProvider tier
🤖 Generated with Claude Code
https://claude.ai/code/session_0123WW42FsCBRwQrK2uwF27s
Generated by Claude Code