o2r: implement gen_ai.authorization.* trust-boundary attributes + A2A->MCP span link (impl of #35) #42
Labels
No labels
burndown-2026-06
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/otel-a2a-relay#42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the #35 design (
docs/trust-boundary-semconv.md). #35 settles what the trust-boundary semantic conventions are. This issue is making the relay emit them. Gated by #35 (attribute names must settle first), unblocked-upstream by #40 (the novelty question, answered).Observability only. Enforcement is unchanged - an agent still refuses an unauthorized request exactly as it does today. The trace now records that it did.
Architecture constraint (load-bearing)
The relay is public and must stay droppable between any two A2A agents (
protocol.md). Two hard rules follow:coily channel verify, and the envelope schema / sign / verify logic does not live in coily. (The reverse-direction cleanup - purgingcoily channelfrom o2r docs and checking coily's repo for o2r-aware verbs - is tracked in the doc-purge issue, see Dependencies.)Where the envelope lives and how it is verified
channels/package (orcore, shipped viaotel-a2a-relay-core). Single-sourced. Both the relay (verify) and the issuer (sign) import the same lib.metadatabag (A2A's open key-value field). No new transport, no new protocol on relay ingress - the envelope arrives inside the A2A message that already opens the session..well-known/jwks.jsonfetch. Never on the per-request path.gen_ai.authorization.envelope_refresolves to a human-auditable copy. Verification never depends on that fetch - the forge copy is provenance for humans, not part of the trust check.Pluggable resolver
Define a
TrustRootResolverinterface with one method: verify-and-extract -> thegen_ai.authorization.*fields.Scope
gen_ai.authorization.action.class,.action.destructive,.acknowledged_by,.envelope_ref,.boundaryto the registry block indocs/protocol.md. Bump v0.4 -> v0.5. Regeneratedocs/generated/*viascripts/emit_protocol_artifacts.py.channels/(or core). Schema + JWS sign + JWS verify. The relay imports verify, tests import sign.metadata, write thegen_ai.authorization.*attributes onto thea2a.taskroot span withboundary=agent_to_agent. Non-destructive actions leaveacknowledged_byempty. A request that fails verification produces the existinga2a.relay.rejectspan, not an authorized task span. Decide whether reject spans should also carrygen_ai.authorization.*for forensic value.a2a.taskspan, carryingenvelope_refandboundary=agent_to_tool. The load-bearing part of the #35 contribution.The hard part - the relay does not see MCP traffic
The relay sits on the A2A wire. MCP tool-call spans are emitted by the agent process's own OpenInference instrumentation, not by the relay. So the relay cannot attach the back-link itself. The authorization context has to propagate into the agent process so its MCP spans can carry
envelope_refand link back to the task span.Candidate mechanisms (central design question, pick during impl):
using_session(context_id)propagation with the authorization envelope ref, so any OpenInference-instrumented MCP call inside the session inherits it.envelope_ref, set at relay ingress, read by agent-side MCP instrumentation.tracinghelper the agent calls to open an "authorized action" scope that stamps its child MCP spans.Whichever wins likely touches the OpenInference / MCP instrumentation seam, not just the relay. Call that out before writing code.
Sequencing gate (repo rule, do not skip)
Per
AGENTS.md, protocol-shape changes go doc-first.docs/protocol.md+ the harness together.make phoenix-up). Confirm via the GraphQL spans query thatgen_ai.authorization.*appear where the spec calls for them, and that the cross-boundary span link is exposed. Risk: Phoenix historically drops span links (v0.1 moved topology tograph.node.*for exactly this reason). If links are still dropped, the MCP-boundary correlation needs agraph.node.*-style attribute fallback. Resolve this in step 2 before committing to the design.Out of scope
channelslib provides sign (tests use it inline). A standalone o2r-owned issuer CLI is operator surface and can be a thin follow-up - file separately if wanted.Dependencies and cross-links
protocol.md/agent-channel-requests.md/AGENTS.mdshould be gone before this lands, so the impl is not documented against a tool that must not exist here).The doc-purge issue referenced above is #43. That should land (or at least the o2r-side doc sweep) before this impl, so #42 is not written against
coily channel, which must not exist in this repo.Decision record - rejected alternatives (so they are not re-litigated)
The body states the chosen design. Recording the paths considered and why they lost, since that reasoning otherwise only lived in the design conversation.
Verification transport: no new wire protocol. Considered an out-of-band forge fetch as the default (the request carries a URL, the relay GETs the envelope, then verifies). Rejected as default because it adds a fetch dependency on the ingress hot path. Chosen: the JWS-signed envelope rides in-band in the A2A request
metadatabag, verification is a local stateless call. Forge persistence stays optional, for human-auditableenvelope_refprovenance only - verification never depends on it.Default backend: signed-envelope (JWS), not CRD, not MCP.
examples/luca-flowhave nowhere to put a CR), and a human typing a verbatim destructive-action acknowledgment is an attestation event, not declarative desired state - an impedance mismatch with CRs. Kept as an optional in-cluster backend behind the resolver interface.coily fully out, no issuance carve-out. An earlier draft kept issuance/signing in coily as operator surface. Rejected: the dependency architecture forbids coily being aware of o2r at all. The envelope schema + sign + verify lib lives in o2r's own
channels/core package, single-sourced; issuance is an o2r-owned CLI. coily is never imported by the relay and never references o2r. (Reverse-direction cleanup: coilyco-bridge/coily#172; o2r-side doc purge: #43.)Reuse A2A's own JWS primitive, do not invent. Verification uses RFC 7515 over JCS-canonicalized JSON (RFC 8785) - the same mechanism A2A uses for signed AgentCards. Per the #40 finding, the contribution is the observability of the trust boundary, not the signing. Inventing a bespoke signature scheme here would be novel in the one place it should not be.
The o2r-owned CLI is filed as #44 (standalone Go repo, JSON Schema contract). It refines this issue: the envelope "schema + sign + verify lib" here splits into - schema as a published artifact (shared), Python verify in the relay (this issue), Go sign + verify in the #44 CLI repo. Fold that split in when this is implemented.
Backlog burndown 2026-06-17: closing low-priority (P3/P4) to bring the open count to a manageable level. Nothing lost — reopen if this resurfaces. Batch tag:
burndown-2026-06.