Negotiation: the evolution gate an importing host runs
after canonical verification, before any semantic read. In a standalone
pass it runs immediately after canonical verification; in the composed
import (Reconcile.reconcile/3) the pinned stage order is
canonical → digest → negotiation → structure → portability → signatures → bind → bounds — the digest stage runs BEFORE negotiation
(the supersession of the "integrity last" ordering).
Negotiate reads ONLY protocol machinery — protocol_revision,
required_core_fields, the extensions envelope — never semantic
members. Malformed machinery values reuse Blueprint's decode reason
rules so both entry points agree (absent → :missing_required_field,
wrong tag → :invalid_type, revision < 1 → :invalid_constraint; the
retired :protocol_revision_invalid stays retired). Internal
precedence is PINNED: revision → required_core_fields → extensions —
an artifact failing several stages reports the earliest.
Revision support is an explicit SET (Support.revisions, never ranges);
a miss denies :protocol_revision_unsupported (above-max and below-min
are the same miss). Outcome.protocol_revision is the artifact's own,
exact — never negotiated down.
required_core_fields runs three fail-closed checks in order: known
(:required_core_field_unsupported) → digest-covered
(:required_core_field_not_digest_covered) → supported by this
consumer (:required_core_field_unsupported).
Extensions run the positional state machine (criticality match before state handling for LIVE states; reserved/retired labels check state first — held and dead labels do not enforce criticality; every state × position cell decided):
| registry state | artifact critical | artifact optional |
|---|---|---|
| unregistered | deny :extension_unknown_critical | QUARANTINE (byte-exact, noticed) |
| criticality mismatch | deny :extension_criticality_conflict | deny :extension_criticality_conflict |
| active matching | supported | retained, executable |
| deprecated | supported + notice | retained + notice |
| retired | deny :extension_retired | retained + notice |
| reserved | deny :extension_unknown_critical | retained + notice |
Quarantined namespaces round-trip byte-exactly and are NEVER claimed scanned-or-portable beyond what the structural denylists check.
Critical bodies validate ONLY against a host-supplied schema whose
digest matches the registry's pin: none supplied (or the registry entry
has none authored) → :extension_schema_unavailable; digest mismatch →
:extension_schema_digest_mismatch; match → Schema.validate_instance
over the body. Validated critical namespaces are the legitimate channel
for encoded content — thread them into Blueprint.from_value/2's
:authored_extensions option.
A reserved-semantics denylist runs over ALL extension bodies (any
depth, camelCase twins included): bound-shaped member names deny
:extension_payload_forbidden — the bound-vocabulary smuggling the
portability scan cannot see.
negotiate/2 also accepts %Blueprint{} (reads to_value): a struct
from from_value carries no integrity proof — outcomes derive from the
given content.
Negotiation reports what a support posture accepts; it never authorizes an operation.
Negotiation reports what a support posture accepts; it never authorizes an operation.
Summary
Types
@type reason() :: :protocol_revision_unsupported | :missing_required_field | :invalid_type | :invalid_constraint | :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 | AgentBlueprintProtocol.Schema.schema_reason() | AgentBlueprintProtocol.Schema.instance_reason()
Functions
@spec negotiate( AgentBlueprintProtocol.Blueprint.t() | AgentBlueprintProtocol.Deployment.t() | AgentBlueprintProtocol.Json.value(), AgentBlueprintProtocol.Negotiation.Support.t() ) :: {:ok, AgentBlueprintProtocol.Negotiation.Outcome.t()} | {:error, reason()}