AgentBlueprintProtocol.Federation (Agent Blueprint Protocol v0.1.1)

Copy Markdown View Source

The federation profile: the 23-member TaskEnvelope, the lossy-aware A2A/MCP state codecs, and the carrier placement laws — the executable half of the field-by-field mapping published as federation_mapping/0 data.

The envelope is a closed world: one wire member per logical field of the protocol's federation seam, decoded through the shared Registry engine under this module's field table. Transport clients and wire I/O stay with host adapters; the carriers built here are pure JSON shapes — the Task-level placement on each transport (Task.metadata[a2a_uri] on A2A, _meta[namespace] on MCP; the _meta key grammar forbids a URI key, so the two transports key the same JCS object differently).

Placement verdicts (live-re-derived against the pinned sources; see docs/federation-mapping.md): task_identity/recovery_handle map to the task id on both transports (a divergent handle is unmappable — :federation_mapping_conflict), the observed state maps to status.state, and everything else rides the extension body. A Task always carries a status, so an envelope with no observed state has no Task image — message-context payloads are host-adapter territory (rows 3/7 document the Message-level homes).

Lossy-state rules: A2A TASK_STATE_UNSPECIFIED denies at intake; to_mcp denies :rejected (terminal with no MCP counterpart) and :submitted/:auth_required (no MCP counterpart) with :federation_state_unmappable — never a silent degrade. Cancellation is a request, never a terminal receipt: no codec path synthesizes a terminal member from a cancel request or acknowledgment.

This is a NEW surface, so failures are %Error{} records from day one ("the only failure shape"; the older decode surfaces' bare atoms are the recorded migration). Federation receipts are evidence records — verification never authorizes anything. Federation receipts are evidence records — verification never authorizes anything.

Summary

Functions

The A2A-side carrier key: the registry entry's a2a_uri (A2A's own URI-keyed metadata convention).

The canonical bytes of the whole envelope (the raw Canonicalization result, matching Blueprint.canonical_bytes/1 — the encode-error shape is that layer's to report).

Decode and fully verify envelope bytes: canonical verify → registry validation → the federation portability pass. Total and never-raising; failures are %Error{} records.

The envelope's closed world: the 23 wire member names, table order.

Rebuild the envelope from an A2A Task carrier: the native homes supply task_identity/recovery_handle/the observed state, the body supplies the rest, and a native member appearing in the body denies :federation_mapping_conflict (a double placement exists only for tampering to exploit).

An A2A TaskState spelling into the logical state. The nine proto values map 8-up; TASK_STATE_UNSPECIFIED denies :federation_state_unmappable (never a guess), and an unknown spelling denies :invalid_constraint.

Rebuild the envelope from an MCP Task carrier (the upward state mapping; cancelled folds to :canceled).

An MCP task status into the logical state (all five map upward; cancelled folds to :canceled).

Validate an already-decoded tagged value (registry walk + portability pass; no canonicality — there are no bytes). The codec reconstruction path; values from the wire go through decode/2.

The 23-row field-by-field A2A/MCP Tasks mapping as data (the published table in docs/federation-mapping.md mirrors these rows).

The MCP-side carrier key: the registry namespace (the _meta key grammar forbids a URI key).

The envelope's members as a map (wire names → tagged values).

The Terminal Commitment: the domain-separated digest over exactly the seven named components — task identity, terminal state, result digest, result classification, compatibility reference, authority-proof references, checkpoint-history commitment. Two receipts for one task identity diverge in commitment iff they diverge in any component, which is why equivocation needs no separate code.

The Task-level A2A carrier: id, status.state, and the extension body under metadata[a2a_carrier_key/0]. Native members never ride the body. Requires an observed state (a Task always has one) and recovery_handle == task_identity (the task id IS the handle on both transports — a divergent handle is unmappable).

The logical state into its A2A spelling (all eight map; there is no logical UNSPECIFIED).

The Task-level MCP carrier: taskId, status, and the extension body under _meta[mcp_carrier_key/0] (the flat Result & Task merge the CreateTaskResult shape defines). Requires an observed state; the lossy states deny before placement.

The logical state into its MCP spelling — the lossy edge: :rejected (terminal with no MCP counterpart) and :submitted/:auth_required (no counterpart) deny :federation_state_unmappable rather than silently degrading.

Verify a terminal receipt against the receiving context. Facts, never a decision — correlation grants nothing, so the success value carries the task identity and terminal commitment and nothing else.

Types

t()

@type t() :: %AgentBlueprintProtocol.Federation{
  value: AgentBlueprintProtocol.Json.value()
}

Functions

a2a_carrier_key()

@spec a2a_carrier_key() :: binary()

The A2A-side carrier key: the registry entry's a2a_uri (A2A's own URI-keyed metadata convention).

canonical_bytes(federation)

@spec canonical_bytes(t()) :: {:ok, binary()} | {:error, term()}

The canonical bytes of the whole envelope (the raw Canonicalization result, matching Blueprint.canonical_bytes/1 — the encode-error shape is that layer's to report).

decode(binary, bounds \\ Bounds.maximum())

@spec decode(binary(), AgentBlueprintProtocol.Bounds.t() | map()) ::
  {:ok, t()} | {:error, AgentBlueprintProtocol.Error.t()}

Decode and fully verify envelope bytes: canonical verify → registry validation → the federation portability pass. Total and never-raising; failures are %Error{} records.

envelope_members()

@spec envelope_members() :: [binary()]

The envelope's closed world: the 23 wire member names, table order.

from_a2a_carrier(carrier)

@spec from_a2a_carrier(AgentBlueprintProtocol.Json.value()) ::
  {:ok, t()} | {:error, AgentBlueprintProtocol.Error.t()}

Rebuild the envelope from an A2A Task carrier: the native homes supply task_identity/recovery_handle/the observed state, the body supplies the rest, and a native member appearing in the body denies :federation_mapping_conflict (a double placement exists only for tampering to exploit).

from_a2a_state(spelling)

@spec from_a2a_state(binary()) ::
  {:ok, atom()} | {:error, AgentBlueprintProtocol.Error.t()}

An A2A TaskState spelling into the logical state. The nine proto values map 8-up; TASK_STATE_UNSPECIFIED denies :federation_state_unmappable (never a guess), and an unknown spelling denies :invalid_constraint.

from_mcp_carrier(carrier)

@spec from_mcp_carrier(AgentBlueprintProtocol.Json.value()) ::
  {:ok, t()} | {:error, AgentBlueprintProtocol.Error.t()}

Rebuild the envelope from an MCP Task carrier (the upward state mapping; cancelled folds to :canceled).

from_mcp_state(spelling)

@spec from_mcp_state(binary()) ::
  {:ok, atom()} | {:error, AgentBlueprintProtocol.Error.t()}

An MCP task status into the logical state (all five map upward; cancelled folds to :canceled).

from_value(value)

@spec from_value(AgentBlueprintProtocol.Json.value()) ::
  {:ok, t()} | {:error, AgentBlueprintProtocol.Error.t()}

Validate an already-decoded tagged value (registry walk + portability pass; no canonicality — there are no bytes). The codec reconstruction path; values from the wire go through decode/2.

mapping()

The 23-row field-by-field A2A/MCP Tasks mapping as data (the published table in docs/federation-mapping.md mirrors these rows).

mcp_carrier_key()

@spec mcp_carrier_key() :: binary()

The MCP-side carrier key: the registry namespace (the _meta key grammar forbids a URI key).

member_map(federation)

@spec member_map(t()) :: %{optional(binary()) => AgentBlueprintProtocol.Json.value()}

The envelope's members as a map (wire names → tagged values).

terminal_commitment(arg1)

@spec terminal_commitment(t()) ::
  {:ok, AgentBlueprintProtocol.Digest.t()}
  | {:error, AgentBlueprintProtocol.Error.t()}

The Terminal Commitment: the domain-separated digest over exactly the seven named components — task identity, terminal state, result digest, result classification, compatibility reference, authority-proof references, checkpoint-history commitment. Two receipts for one task identity diverge in commitment iff they diverge in any component, which is why equivocation needs no separate code.

to_a2a_carrier(envelope)

@spec to_a2a_carrier(t()) ::
  {:ok, AgentBlueprintProtocol.Json.value()}
  | {:error, AgentBlueprintProtocol.Error.t()}

The Task-level A2A carrier: id, status.state, and the extension body under metadata[a2a_carrier_key/0]. Native members never ride the body. Requires an observed state (a Task always has one) and recovery_handle == task_identity (the task id IS the handle on both transports — a divergent handle is unmappable).

to_a2a_state(logical)

@spec to_a2a_state(atom()) ::
  {:ok, binary()} | {:error, AgentBlueprintProtocol.Error.t()}

The logical state into its A2A spelling (all eight map; there is no logical UNSPECIFIED).

to_mcp_carrier(envelope)

@spec to_mcp_carrier(t()) ::
  {:ok, AgentBlueprintProtocol.Json.value()}
  | {:error, AgentBlueprintProtocol.Error.t()}

The Task-level MCP carrier: taskId, status, and the extension body under _meta[mcp_carrier_key/0] (the flat Result & Task merge the CreateTaskResult shape defines). Requires an observed state; the lossy states deny before placement.

to_mcp_state(logical)

@spec to_mcp_state(atom()) ::
  {:ok, binary()} | {:error, AgentBlueprintProtocol.Error.t()}

The logical state into its MCP spelling — the lossy edge: :rejected (terminal with no MCP counterpart) and :submitted/:auth_required (no counterpart) deny :federation_state_unmappable rather than silently degrading.

verify_commitment(arg1, context)

Verify a terminal receipt against the receiving context. Facts, never a decision — correlation grants nothing, so the success value carries the task identity and terminal commitment and nothing else.

The numbered steps (design note §4): terminal members present → JWS verify → the BINDING step (the signed content_digest must name THIS envelope's covered bytes — an honestly-signed statement naming a different digest is evidence over THAT digest, not this envelope) → terminal-commitment recompute → issuer/subject/audience comparison → conflict against prior receipts for the same task identity.