Agent Blueprint Protocol — normative protocol document

Copy Markdown View Source

Status: release candidate (unpublished). This document is normative for the shipped reference implementation and its conformance corpus (digest sha-256:k0ltF0KWcORTzkyr6dP7TPU9M2VdpewAvf_O8w2D7Wo, 88 cases). Where this document and the package's compiled data disagree, the package and its corpus are the truth and this document is a defect.

The protocol is non-authorizing. It validates structure, canonical bytes, bounds, compatibility, and evidence. Protocol validity never grants authority: a consuming host remains responsible for identity, tenancy, live policy, effect ownership, execution, and evidence retention. Every verification result — including a fully green one — is evidence, never a decision.

1. Scope and non-goals

Two language-neutral artifacts:

  • Blueprint Core — stable identity, typed ports, logical capability requirements, bounds, evidence commitments, registered extensions.
  • Deployment Manifest — environment-local tool, principal, data, authority, effect, evaluation, and exact-build bindings for one Blueprint release digest.

Deliberately out of scope (host-owned): runtime kernel, live authority and grants, effect admission/replay/recovery, product schemas, transport clients, billing, evaluation truth. The package compiles to an inert library: no supervision tree, no application callback, zero third-party production dependencies.

2. Artifacts and the closed world

Both artifacts are JSON objects decoded under bounded ceilings (depth, members, items, nodes, string bytes, key bytes, number lexemes — the ceiling family). Decoding is fail-closed with a pinned failure precedence: unknown member → missing required → type → constraint → cardinality → nested → cross-field hooks.

The registries are closed worlds — the exact member sets the shipped tables define:

  • Blueprint Core (18 members). Required: blueprint_id, protocol_revision, producer, release_number, capability_requirements, input_ports, output_ports, output_contract, triggers, ceilings, classification_ceiling, effect_intents, evaluation_assertions, extensions, required_core_fields, content_digest. Optional: signatures, attestations.
  • Deployment Manifest (19 members). Required: protocol_revision, blueprint_release, scope_projection, tool_bindings, data_bindings, authority_requirement, effect_owner, signer_custody, eligibility, model_policy, host_bounds, lifecycle, build_identities, evaluation_binding, extensions, required_core_fields, deployment_digest. Optional: signatures, attestations.

Every core member addition requires a protocol revision increment: an older consumer rejects an unknown member, so the vocabulary may only evolve at a revision boundary. No identifier in the package — module, function, path, corpus, or config — carries a version token; the Hex package version is the sole version number.

3. Bytes

  • Encoding: UTF-8 JSON, bounded by the decoder ceilings. Duplicate members deny :duplicate_member (I-JSON). Trailing bytes deny :trailing_bytes.
  • Integer window: a pure-digit lexeme above ±(2^53−1) decodes as a float iff that double's canonical ECMAScript serialization reproduces the lexeme byte-exactly; every other above-bound lexeme denies :number_not_double_expressible. Core fields are integer-typed by schema: a float-tagged window value in a core field denies :invalid_type.
  • base64url: unpadded, exact alphabet; padded input denies :base64url_padded, non-alphabet input denies :base64url_invalid.
  • Canonicality: interchange bytes must already be canonical — decode → re-encode → byte-compare; non-canonical bytes deny :non_canonical_bytes before any semantic read, and digests are computed only over the exact received bytes.

4. Canonicalization

RFC 8785 JSON Canonicalization Scheme:

  • No whitespace between tokens (§3.2.1).
  • Control characters U+0000–U+001F escaped as lowercase \uXXXX except the five short escapes; " and \ escaped; a lone surrogate denies (§3.2.2.2).
  • Numbers per ECMA-262 7.1.12.1 including the Note 2 enhancement: 1.0 → "1", 1.0e22 → "1e+22", -0.0 → "0", 5.0e-324 → "5e-324", 1.7976931348623157e308 → "1.7976931348623157e+308"; NaN and Infinity deny (§3.2.2.3).
  • Object members sorted as UTF-16 code units compared as unsigned integers (§3.2.3) — NOT by UTF-8 byte order: for U+FF3A vs U+10000, UTF-16 order puts U+10000 first.

5. Digest

digest_bytes = SHA-256( domain_separator || <<0>> || JCS(digest_input) )

digest_input is the artifact object minus content_digest (or deployment_digest), signatures, and attestations — everything else is covered, including extensions and unknown-but-optional extension payloads retained verbatim.

PurposeSeparator
Blueprint contentagent-blueprint-protocol/blueprint-content
Deployment contentagent-blueprint-protocol/deployment-content
Federation envelopeagent-blueprint-protocol/federation-envelope
Signature inputagent-blueprint-protocol/signature
Conformance reportagent-blueprint-protocol/conformance-report
Corpus indexagent-blueprint-protocol/corpus-index

Separators carry no version token: protocol_revision is itself a covered member. The wire form is the self-identifying tagged string sha-256:<43-char unpadded base64url>, never a bare hex blob; an unknown algorithm tag denies :digest_algorithm_unsupported, a malformed body denies :digest_encoding_invalid. Algorithm succession is therefore a data change, not a format break.

6. Signature envelope

Signatures are detached and evidence-only. The package VERIFIES; it never signs, never accepts a private key on any public function, and never embeds public keys in artifacts (the host supplies trusted keys).

  • Format: RFC 7515 compact + RFC 7797 b64=false unencoded detached payload. Protected header: {alg: EdDSA, kid, crit: ["b64"], b64: false}. Ed25519 only; an unknown alg denies.
  • signed_attributes = {algorithm, content_digest, created_at, key_id, purpose} with purpose one of blueprint | deployment | federation-envelope; the signing input is the signature domain separator, a zero byte, and JCS(signed_attributes).
  • Because the digest covers protocol_revision, identity members, extensions, and required_core_fields, a signature cannot be lifted onto a different artifact, revision, or purpose; key_id prevents key substitution. A key_id matching no supplied key is a verified: false check entry with :signature_key_unsupported — the package performs no key discovery, fetch, or trust selection.
  • Attestations use the identical envelope with a registered kind and a statement_digest. The attestation kind registry is empty by design in this release.
  • Small-order Ed25519 public keys (identity and order-2 encodings) reject :signature_key_unsupported.

7. Evolution and negotiation

protocol_revision is a digest-covered body member. This release is revision 1. A consumer's support declares an explicit revision SET (never a range); a revision outside the set denies :protocol_revision_unsupported — above-max and below-min alike, both fail-closed.

required_core_fields is the producer's declaration of which covered core members a consumer must honor. Three checks, all fail-closed: every entry names a known core member (:required_core_field_unsupported), is digest-covered (:required_core_field_not_digest_covered — an evidence-only member laundered into a requirement is a tamper blind spot), and is in the consumer's supported set.

Extension criticality

For each namespace → {criticality, payload} in extensions:

Registry stateDeclared criticalDeclared optional
unregistereddeny :extension_unknown_criticalretained verbatim, quarantined, never executed
reserveddeny :extension_unknown_criticalretained verbatim; typed notice
active, criticality matchessupportedretained and executable
active, criticality mismatchdeny :extension_criticality_conflictdeny :extension_criticality_conflict
deprecatedsupported + typed noticesupported + typed notice
retireddeny :extension_retiredretained verbatim; typed notice

An unknown optional extension round-trips byte-exactly (decode → to_value → encode is a fixed point, property-tested). Lifecycle asymmetry: optional→critical promotion requires a revision increment; demotion does not. A retired namespace is never reused.

8. Extension registry

Namespace form: reverse-DNS-plus-path (com.example.commerce/graph), lowercase, one /, length-ceilinged, and not parseable as an absolute URI with a network authority (a namespace can never double as an endpoint). The registry is COMPILED-IN data in the package — registry content is a code release; there is no registry directory or file, and drift between shipped code and shipped registry is unrepresentable. Registered at this release:

NamespaceOwnerCriticalityState
com.example.commerce/graphExampleCommercecriticalactive
com.example.commerce/classification-labelsExampleCommerceoptionalactive
com.example.commerce/rubric-assertionExampleCommerceoptionalactive
com.example.platform/estateExamplePlatformoptionalactive
com.example/federationAgent Blueprint Protocolcriticalactive

Critical-extension bodies validate only against a host-supplied schema whose digest matches the registry's schema_digest; no schema denies :extension_schema_unavailable, a mismatched digest denies :extension_schema_digest_mismatch. Entry changes are owner-made, changelog-recorded, and ADR-recorded. The registry's own digest is bound into the corpus index but not into artifact digests: an artifact must remain verifiable against a registry that legitimately gained entries after it was produced; hosts wanting pinning use build_identities.

9. Bounds algebra

Two bound families, never conflated with parse ceilings (those are tighten-only decoder limits):

  • Operational (8 members, all REQUIRED — absent is :missing_ceiling, never infinity): max_attempts, max_concurrency, max_cost, max_depth, max_descendants, max_elapsed_ms, max_fan_out, max_tokens. Pointwise narrowest meet; every narrowing emits clamp evidence.
  • Protected (5 members: classification_ceiling, disclosure_ceiling, effect_impact_ceiling, authority_trait, approval_trait): ordinal lattices (plus set-monotone markers {pci, phi} on classification). Narrowing a protected bound denies :protected_bound_clamp_denied by default; a host may opt into an acknowledge posture which always records evidence. Obligation families (authority, approval, effect impact) meet at the STRICTEST value; markers are retained regulatory obligations whose effective set is the UNION of sources — dropping one is a widening.

Laws (property-tested): meet is idempotent, commutative, associative; not widens?(effective, host) universally; a clamp is emitted iff effective ≠ requested on an operational field. model_policy and data_bindings are not intersection inputs.

10. Compatibility and binding

A manifest's blueprint_release binds to exactly one Blueprint content digest (digest equality only — no fuzzy match). build_identities carry exact identities; compatibility verification is identity-exact or error: a range expression, missing entry, or duplicate denies (:compatibility_identity_inexact, :compatibility_entry_missing, :compatibility_duplicate_entry). Binding verification is a pinned deny-ordered check including attestation freshness (:binding_attestation_stale) and descriptor-digest equality (:binding_descriptor_mismatch — the tool rug-pull case).

11. Federation profile

A 23-member task envelope carried in A2A Task.metadata / MCP _meta under the registered com.example/federation extension; the full field-by-field A2A/MCP mapping (3 native, 5 partial, 15 extension members) ships as docs/federation-mapping.md. Zero native wire fields; no native transport. State codecs are lossy-aware: A2A REJECTED/AUTH_REQUIRED deny crossing into MCP, UNSPECIFIED is unmapped (:federation_state_unmappable); cancellation is a request, never a terminal receipt. A Terminal Commitment digests task identity, terminal state, result digest, result classification, compatibility ref, authority-proof refs, and checkpoint-history commitment; ANY divergence between receipts for one task identity denies :federation_terminal_conflict — not just state divergence. Federation.verify_commitment compares issuer/subject/audience against the receiving context (:audience_mismatch). AgentCard signing carries a protobuf field-presence pre-normalization on top of JCS (documented in the mapping so adapters do not inherit silent verification failure). Correlation grants nothing.

12. Error vocabulary

A closed typed set — %Error{code, subject, detail} — enforced two-directionally by a build gate (an unreachable code or an undeclared emission is a build failure). The 74 codes of this release:

base64url_invalid · base64url_padded · invalid_syntax · invalid_encoding · invalid_number · number_not_double_expressible · duplicate_member · trailing_bytes · unknown_bound · non_canonical_bytes · integer_magnitude · unknown_member · missing_required_field · invalid_type · invalid_constraint · invalid_cardinality · digest_algorithm_unsupported · digest_encoding_invalid · digest_mismatch · signature_algorithm_unsupported · signature_key_unsupported · signature_malformed · signature_not_verified · attestation_malformed · schema_dialect_unknown · schema_keyword_not_allowed · schema_keyword_value_invalid · schema_complexity_exceeded · schema_ref_unresolvable · schema_ref_cycle · schema_invalid_shape · predicate_op_unknown · predicate_path_unresolved · predicate_nodes_exceeded · extension_duplicate · extension_namespace_invalid · protocol_revision_unsupported · required_core_field_unsupported · required_core_field_not_digest_covered · extension_unknown_critical · extension_criticality_conflict · extension_retired · extension_schema_unavailable · extension_schema_digest_mismatch · extension_payload_forbidden · bound_unknown · missing_ceiling · bound_source_missing · bound_unit_mismatch · bound_value_invalid · protected_bound_clamp_denied · deployment_digest_mismatch · binding_incomplete · no_authoritative_recovery · binding_attestation_stale · binding_descriptor_mismatch · lifecycle_state_invalid · compatibility_identity_inexact · compatibility_entry_missing · compatibility_duplicate_entry · forbidden_portable_value · federation_state_unmappable · federation_mapping_conflict · nonportable_content · audience_mismatch · federation_terminal_conflict · corpus_index_invalid · corpus_hash_mismatch · corpus_file_set_mismatch · corpus_case_id_duplicate · corpus_count_mismatch · corpus_applicability_incomplete · corpus_empty · corpus_case_invalid

Plus the parameterized ceiling family {:ceiling, key} over the eight decoder limit names. No authorization vocabulary exists in any identifier — no :unauthorized in either polarity (source-scanned gate).

13. Evidence and reconcile

reconcile(blueprint, deployment, inputs) is the one call per import: canonical → digest → negotiation → structure → portability → signatures → bind → bounds, in that pinned order. Its result is an %Evidence{} of per-surface checks, effective bounds, clamp evidence, and not_verified — which is non-empty BY CONSTRUCTION, always naming at least the seven host-owned surfaces this protocol structurally cannot establish: tenancy, live_policy, authority, effect_ownership, execution, billing, evaluation_truth. A caller cannot read an Evidence record and conclude "everything is fine": the record itself names what it did not check.

14. Portability

Portable artifacts never contain secrets, private keys, live grants, tenant identifiers, raw endpoints, database primary keys, or backend engine identifiers — enforced structurally (member-name and value-shape denylists over the open regions, at any depth, name-spelling-normalized) and red-cased per class in the corpus. Honest limit, stated: the guard is necessary, not sufficient — opaque quarantined extension bodies are typed as unscanned, and portability claims attach only to schema-validated content. Concrete provider/model names, credentials, engine ids, and endpoints resolve host-side and are unrepresentable in self-fulfilling artifacts.

15. Conformance

The package ships a portable conformance corpus (priv/conformance/): 88 cases covering every required cell of the 16-surface × 29-class applicability floor, full-registry golden artifacts, RFC 8785 number vectors, and deterministic Ed25519 fixtures, at corpus digest sha-256:k0ltF0KWcORTzkyr6dP7TPU9M2VdpewAvf_O8w2D7Wo. Corpus identity is the digest of the domain-separated index — versioned by digest, not by name. The loader is pure over %{path => binary} and verifies per-file hashes, exact file set (both directions), counts, id-uniqueness, and applicability totality; the report refuses a vacuous green.

A repo-side second-language verifier (conformance/verifier/, TypeScript on Node ≥ 24, node: builtins only, never in the Hex archive) independently recomputes every corpus verdict and integrity check with its own scanner, canonicalizer, and node:crypto Ed25519 path; the release gate requires its report to be byte-identical to the Elixir escript's over both the repo corpus and the built archive's corpus. A mutation gate breaks the implementation at seven named points and requires the corpus to go red on each — a vacuous case set is a build failure.

16. The shipped surface

The facade (AgentBlueprintProtocol) delegates and never implements; every public function carries a @spec (build-gated), every module a boundary-stating moduledoc:

FunctionContract
decode_blueprint/1,2bounded, canonical-first Blueprint decode
decode_deployment/1,2bounded, canonical-first Deployment decode
decode_federation_envelope/1,2bounded TaskEnvelope decode
canonical_bytes/1the artifact's canonical JCS bytes
negotiate/2revision/required-field/extension evolution gate
intersect/1bounds meet over blueprint/deployment/host sources
verify_compatibility/2identity-exact build compatibility
reconcile/3the one composed non-authorizing pass per import
federation_mapping/0the 23-row A2A/MCP mapping as data

Layered under it: the bytes layer (Json, Canonicalization, Base64Url, Digest, Bounds), the artifacts (Blueprint, Deployment, Extension, Registry as the one generic table-driven engine, Schema as the bounded 2020-12 dialect + instance validator, Predicate, Portability), the algebra (BoundsAlgebra, Negotiation, Compatibility, Signature, Federation, Error, Evidence, Reconcile, ExtensionRegistry), and the conformance tooling (Conformance.Corpus/Runner/Report/Cli + the escript entry). The module tree is strictly downward (bytes → algebra → artifacts → conformance); the engine knows tables, never domains.