AgentBlueprintProtocol.Deployment (Agent Blueprint Protocol v0.1.0)

Copy Markdown View Source

The Deployment Manifest artifact (base §7, re-derived 2026-08-22): binds exactly ONE Blueprint release digest to one local environment — portable as a shape, resolved values local. Decoded and validated through the ONE generic field-registry engine (AgentBlueprintProtocol.Registry) parameterized by this module's 19-member table : the table, the scan's open regions, and the digest domain are the only deltas from the Blueprint pipeline.

The decode pipeline, in order, each stage fail-closed:

  1. Canonicalization.verify/2 — non-canonical spellings deny :non_canonical_bytes before any semantic read (the canonicality ordering).
  2. AgentBlueprintProtocol.Registry.validate/2 against table/0 — closed world (:unknown_member), 16 required members, tag-strict integer typing, enums, cardinalities (tool_bindings ≤ 128, data_bindings ≤ 64, build_identities ≥ 1 and ≤ 128 — an empty identity manifest pins nothing and is the silent-fallback space the base red case names), the exact-only build-identity versions (:compatibility_identity_inexact on any range vocabulary), and the per-member custom checks (tagged digests, Z-form timestamps, the lifecycle temporal rules :lifecycle_state_invalid, the as_of total rule).
  3. The portability scan — extension bodies and eligibility expressions are the open regions (Portability.scan / scan_authored); every other string position — including every adapter_identity and profile_identity — is value-shape scanned strict: a network URI, PEM armour, a JWS shape, or raw-key entropy denies :forbidden_portable_value (the base's never-portable list, red in deployment positions).
  4. The content-digest comparison over the covered members' canonical bytes under the :deployment_content domain — everything except deployment_digest, signatures, attestations.

The binding surface

binds?/2 is digest equality AND NOTHING ELSE (base §7's binding rule): the declared release digest against the paired Blueprint's RECOMPUTED content digest — never the declared member — via constant-time compare. Total over the deployment side: malformed deployment input answers false. (A hand-built malformed %Blueprint{} raises in Blueprint's own digest surface — that module's contract, not this one's.)

verify_binding/3 is the six-stage deny set, order pinned (the second-language verifier contract):

  1. release digest equality → :deployment_digest_mismatch
  2. release identity → :binding_incomplete (the deployment names a different blueprint_id/release_number than the Blueprint it digests)
  3. tool-binding completeness → :binding_incomplete (a bound logical_operation absent from the Blueprint's capability families AND effect intents)
  4. mutation/recovery → :no_authoritative_recovery (a binding whose operation resolves to mutation under recovery: "none"; a name in both sources with disagreeing kinds takes the stricter reading)
  5. attestation staleness → :binding_attestation_stale (age > max or a FUTURE attested_at — the host's clock is authoritative; fail-closed)
  6. observed rug-pull → :binding_descriptor_mismatch (the host's observed descriptor digest vs the attested one)

Stages 5-6 run only on the host observations that feed them: now: nil skips EVERY staleness judgment including the future-deny; an empty observed map skips the rug-pull. That is the host's governance choice — the parameters ARE host policy (the base's stale-binding and rug-pull red cases are host-observation cases); decode-time integrity is never skippable.

Honest limits

Eligibility expressions are host policy DSLs: member names are denylist-scanned (tenant_id, user_id, … deny at any depth) but VALUES carry the authored exemption — a resolved principal UUID passes every scan mode (shape-exempt; undecidable-by-scan — necessary, not sufficient, same posture as extension bodies). Timestamps are Z-form whole-second RFC3339: sub-second attestations are unrepresentable, so max_attestation_age_ms is effectively second-granular. A Deployment Manifest is an inert binding description — it never authorizes execution. A Deployment Manifest is an inert binding description — it never authorizes execution.

Summary

Functions

Digest equality and nothing else (base §7's binding rule): the declared release digest against the paired Blueprint's RECOMPUTED content digest — never its declared member. Total over the deployment side: malformed deployment input answers false (a hand-built malformed %Blueprint{} raises in Blueprint's own digest surface — its contract).

The one bound release, parsed: %{blueprint_id, release_number, content_digest} with the digest as a Digest.t(). Total: a malformed or absent release denies :invalid_type (or the digest's own reason).

The canonical bytes of the whole artifact.

The honest content digest over the covered members' canonical bytes.

Decode and fully verify artifact bytes: canonical verify → registry validation → portability scan → content-digest comparison. Total and never-raising.

Whether member_name (a wire-level member name) is digest-covered.

The digest input: the artifact minus the three evidence members (§8.2).

Validate an already-decoded tagged value (stages 2-3; no canonicality — there are no bytes, so the canonicality ordering obligation does not apply here). For values that came from verified bytes, follow with verify_content_digest/1 — this function does NOT check the declared digest. opts carries :authored_extensions — namespaces whose critical bodies negotiation validated against a digest-pinned host schema (the validated-extension channel). Those bodies skip the portability value-shape heuristics; the channel is tied to THIS artifact's critical region, and the default ([]) keeps the strict posture everywhere.

The 19-member field registry (base §7, re-derived): data for the generic engine. Field order is the engine's precedence anchor for table-order stages.

The held tagged value — the identity, so the round-trip is byte-exact.

The bind-time deny set (order pinned; see the moduledoc): stage 0 validates the deployment's own shape — a non-object root, DUPLICATE root members (first-wins reads must never decide a binding), or a malformed tool_bindings member DENIES before any cross-artifact judgment. Stages 5-6 are observation-gated — absent host inputs skip exactly their stages.

Compare the declared deployment_digest member against the recomputed digest over the exact received (verified) bytes' canonical form: :digest_mismatch on divergence.

Types

reason()

t()

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

Functions

binds?(deployment, blueprint)

@spec binds?(t(), AgentBlueprintProtocol.Blueprint.t()) :: boolean()

Digest equality and nothing else (base §7's binding rule): the declared release digest against the paired Blueprint's RECOMPUTED content digest — never its declared member. Total over the deployment side: malformed deployment input answers false (a hand-built malformed %Blueprint{} raises in Blueprint's own digest surface — its contract).

bound_release(deployment)

@spec bound_release(t()) ::
  {:ok,
   %{
     blueprint_id: binary(),
     release_number: pos_integer(),
     content_digest: AgentBlueprintProtocol.Digest.t()
   }}
  | {:error, reason()}

The one bound release, parsed: %{blueprint_id, release_number, content_digest} with the digest as a Digest.t(). Total: a malformed or absent release denies :invalid_type (or the digest's own reason).

canonical_bytes(deployment)

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

The canonical bytes of the whole artifact.

content_digest(deployment)

@spec content_digest(t()) :: AgentBlueprintProtocol.Digest.t() | {:error, reason()}

The honest content digest over the covered members' canonical bytes.

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

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

Decode and fully verify artifact bytes: canonical verify → registry validation → portability scan → content-digest comparison. Total and never-raising.

digest_covered?(member_name)

@spec digest_covered?(binary()) :: boolean()

Whether member_name (a wire-level member name) is digest-covered.

digest_input(deployment)

@spec digest_input(t()) :: AgentBlueprintProtocol.Json.value()

The digest input: the artifact minus the three evidence members (§8.2).

from_value(value, opts \\ %{})

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

Validate an already-decoded tagged value (stages 2-3; no canonicality — there are no bytes, so the canonicality ordering obligation does not apply here). For values that came from verified bytes, follow with verify_content_digest/1 — this function does NOT check the declared digest. opts carries :authored_extensions — namespaces whose critical bodies negotiation validated against a digest-pinned host schema (the validated-extension channel). Those bodies skip the portability value-shape heuristics; the channel is tied to THIS artifact's critical region, and the default ([]) keeps the strict posture everywhere.

table()

The 19-member field registry (base §7, re-derived): data for the generic engine. Field order is the engine's precedence anchor for table-order stages.

to_value(deployment)

@spec to_value(t()) :: AgentBlueprintProtocol.Json.value()

The held tagged value — the identity, so the round-trip is byte-exact.

verify_binding(deployment, blueprint, obs)

The bind-time deny set (order pinned; see the moduledoc): stage 0 validates the deployment's own shape — a non-object root, DUPLICATE root members (first-wins reads must never decide a binding), or a malformed tool_bindings member DENIES before any cross-artifact judgment. Stages 5-6 are observation-gated — absent host inputs skip exactly their stages.

verify_content_digest(deployment)

@spec verify_content_digest(t()) :: :ok | {:error, reason()}

Compare the declared deployment_digest member against the recomputed digest over the exact received (verified) bytes' canonical form: :digest_mismatch on divergence.