Skip to content

feat(a2a): governed inbound for donkey.serve() tasks (#225) - #1225

Merged
tommasobolis merged 1 commit into
developfrom
feat/225-governed-inbound
Oct 25, 2026
Merged

tommasobolis merged 1 commit into
developfrom
feat/225-governed-inbound

Conversation

@tommasobolis

Copy link
Copy Markdown
Collaborator

Closes #225.

Every task that donkey.serve() / build_app() runs is now governed the same way an outbound donkey.run() is.

What a served task gets

  • Correlation ID. The task adopts the caller's X-Correlation-Id, or the header named by the correlation_header setting. It mints a new ID when the header is missing or malformed (header-safe, at most 128 characters). Handlers read it as A2ATask.correlation_id, and every downstream call sends it.
  • Cost tags. The configured tags apply to every downstream call. A caller's own cost headers are not adopted, because who pays is the serving agent's configuration, not the caller's claim.
  • One span per task. A donkey.a2a.task SERVER span parents the downstream donkey.llm.chat spans. It joins the caller's trace when they send a W3C traceparent. A failure sets only error.type and ERROR status, never the exception message.
  • Typed refusals. A downstream refusal fails the task with a well-formed message: a text part plus a data part, {"error": {"type", "refusal", "correlation_id", "call_id"}}. No stack trace or exception text reaches the caller. Donkey.serve() passes the framework-aware typed_refusals() bridge.

Design notes

  • New core module. The shared logic lives in the new core/inbound.py, so it can be reused by a future MCP or HTTP inbound surface. It is placed in the core layers contract as transport | approvals | inbound. I added no ADR because this follows the approvals/spanflush precedent; say if you want one.
  • No remediation on the wire. Remediation hints and the gateway's response body go to the server log only.
  • Docs as a section. The docs are the "Governed inbound" section of website/content/a2a.mdx, not a separate page. ARCHITECTURE.md is updated too.
  • Ledger row UNVERIFIED. The new docs/verified-apis.md row is marked UNVERIFIED. Whether an Omni Gateway A2A proxy forwards the caller's X-Correlation-Id is untested live.
  • Only part of telemetry: adopt an inbound correlation ID and propagate it across MCP and A2A hops #308. This covers telemetry: adopt an inbound correlation ID and propagate it across MCP and A2A hops #308's inbound side for A2A only. The public run_context_from_headers and MCP remain open there.
  • Out of scope: inbound policy enforcement, which is the gateway's job.

Verification

  • pytest -q: 3717 passed, 118 skipped. There are 19 new tests in tests/unit/test_a2a_governed_inbound.py.
  • ruff check, ruff format --check, mypy, lint-imports (9 contracts kept) and vulture are clean.
  • generate_docs.py --check and check_docs_sync --strict are in sync, and the llms files are regenerated.
  • The site link check was not run locally (it needs a site build); CI runs it.

🤖 Generated with Claude Code

Each inbound A2A task now binds a run, as an outgoing donkey.run() does:

- correlation: adopt the caller's X-Correlation-Id (or the
  correlation_header name) when well-formed, else mint one; exposed as
  A2ATask.correlation_id and sent on every downstream call
- cost tags: the configured tags ride every downstream call (span
  attributes; headers with send_cost_headers); caller cost headers are
  never adopted
- one donkey.a2a.task SERVER span per task, parenting the downstream
  donkey.llm.chat spans and joining a caller's W3C traceparent; failures
  mark it with error.type only, never the message
- a downstream refusal is re-raised as its typed DonkeyError and fails
  the task with a text part plus a structured error data part (type,
  refusal, correlation_id, call_id); remediation and bodies stay in logs

The framework-free core lives in core/inbound.py (placed in the core
layers contract beside transport/approvals); Donkey.serve() passes the
telemetry and correlation_header settings and typed_refusals(). Inbound
policy enforcement stays the gateway's job.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Oct 25, 2026
@tommasobolis
tommasobolis merged commit f3eade8 into develop Oct 25, 2026
48 checks passed
@tommasobolis
tommasobolis deleted the feat/225-governed-inbound branch October 25, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a2a: governance-aware inbound — correlation, tags, spans and typed refusals on served tasks

1 participant