Skip to content

feat(routing): A2A policy support #73

Description

@terylt

Description

Several places in the tree describe the CMF entity types as covering "the MCP, LLM and A2A entities": crates/ppe-apl-runtime/src/lib.rs, crates/ppe-apl-runtime/src/route_handler.rs, crates/ppe-apl-runtime/src/cmf_invoker.rs, and the reserved-entity doc on ENTITY_HTTP. The taxonomy is MCP's three, llm, and http, and there is no a2a: selector, no a2a.* attribute namespace, no A2A hook, and nothing that normalizes an A2A method into a CMF message. The one piece of A2A-aware code is a comment in session_resolver.rs noting that the tier-0 session id is X-Session-Id or the A2A contextId.

The 2026-08-27-apl-canonical-form.md brainstorm settled that there is no missing a2a: selector, on the grounds that A2A traffic is normalized into the CMF entity types before routing and an A2A method is therefore selected by tool: or prompt: like anything else. I think that conclusion is right for the call itself and incomplete for the protocol. Nothing performs that normalization in this tree, nothing tests it, and several A2A concepts have no obvious CMF entity to land on. Until someone writes the mapping down, "A2A is supported" is a claim resting on an unwritten host contract.

The ask is to flesh out the A2A protocol and how it maps onto PPE.

Where the mapping is not obvious

Who the callee is. A2A addresses a remote agent and a skill on it. Under the tool: selector, meta.entity_name would be the skill name, or the agent plus skill, or the agent card URL. Nothing says which, so two hosts would pick differently and the same policy would mean different things. The remote peer's identity has no attribute namespace either: agent.* is our own session and lineage (session_id, conversation_id, agent_id, parent_agent_id), and security.caller_workload is the attested inbound peer, so an outbound call to a named remote agent has no key to match on beyond the entity name.

Task lifecycle. An A2A task is long running, with state transitions, streamed updates, and a terminal state that can arrive much later than the call that created it. A route is a pre phase and a post phase around one call. Where does policy sit for a status update on a task submitted two minutes ago, and which hook fires? If the answer is "each protocol message is its own request through the same route", that is a legitimate answer and it needs to be stated, because it changes what post_invocation means for that route.

Artifacts. An A2A task returns artifacts made of parts. CMF has Resource, Document, Image, and text parts to hold them, but the result projection reads ToolResult only, so an artifact's contents are invisible to result for exactly the reason a fetched resource's are. A multi-part artifact is the third instance of the same missing primitive and the single-part case is prompt/resource/payload. Both are arguments for sequencing this issue after them.

Push notifications. A2A can deliver task updates to a callback URL. That is an egress path with its own authorization question, and if the answer is "it is an http: route" then it should be written down as such.

contextId and taskId. contextId already has a home as the tier-0 session id through AgentExtension.session_id, which is what session-scoped taints key on. taskId has none. Correlating a policy decision on the response with the one taken on the request needs it.

Acceptance criteria

  • A written mapping from A2A methods to (entity_type, entity_name, hook), in docs/, including the long-running task case and the artifact case. This is the deliverable even if the code answer turns out to be "no new selector".
  • The comments claiming the entity types cover A2A either become true or get corrected. Four files say it today.
  • A worked config governing an A2A exchange, and e2e tests over a normalized A2A request and response that assert on a deny.
  • A decision on whether a2a.* attributes are needed for the remote agent, skill, task id, and task state, or whether they fold into agent.* and meta.*. If they fold, the bridge doc comment in praxis-policy-apl-cmf says so. a2a.* is probably the right answer.
  • If A2A turns out to need a fifth entity type, note that HookFamily::for_entity and hook_pair_for_entity are the two places that have to learn it together, and both already log and skip an unknown entity type rather than guessing.
  • Pin the A2A specification version and supported transport binding(s). The mapping table should include direction, operation/event, entity type/name, hook, payload projection, correlation IDs, and attribute provenance.
  • Decide who owns wire-to-CMF normalization. An e2e test beginning with an already-normalized CMF message tests routing, not A2A normalization. If normalization remains a host responsibility, PPE should provide contract fixtures/tests and qualify its "A2A support" claims.
  • contextId may not have a complete home yet: session_resolver only mentions the mapping, and AgentExtension.session_id is ignored without security.subject.id. Workload-only A2A traffic therefore would not get session state. Could the design explicitly cover trust, subject binding, and anonymous/workload-only calls?
  • Now that feat(cmf): prompts and resources are routable but their payloads are not addressable #75 and feat(apl): address one content part of a message instead of the whole thing #76 exist, link them as explicit dependencies or split artifact handling into follow-up work.
  • Treat push-notification registration, outbound delivery, and inbound callback handling separately; calling all of them an http: route hides different SSRF, egress, and authentication questions. (not entirely sure how we want to handle this one)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions