ADR-009: Machine-readable cross-product wire contracts as a suite candidate gate
Source of truth:
docs/adr/009-cross-product-wire-contracts.md(synced at build time — edit the original, not this copy). Status: Accepted (2026-07-28). Establishes the mechanism by which an explicit, versioned compatibility contract between two suite products becomes a machine-checked gate on suite release-candidate promotion (issue #161, child of #53). It does not change any runtime behaviour: no auth/session minting, token verification, role maps, claims, middleware, or #59 rollout is touched, and no product seam payload/route is added or altered. Builds on ADR-005 (the sharedveltro-suite-authcontract package) and ADR-008 (the identity/role contract); nothing in either is retracted.
Amended by ADR-016: the contract registry and readiness evidence are retired; each product tests its side of every contract with its own vectors.
Context
Section titled “Context”The suite already ships one fully-realised cross-product contract: the
Organization / session identity / RBAC contract of #59
(session_contract_version=2, canonical suite_org_id, per-app apps.<product>
grants), produced by VectorFlow and consumed by CHAD and Warden through the
shared veltro-suite-auth decoder. Its immutable evidence lives in
conformance/suite-org-v2-readiness.json
and is validated per-run by scripts/verify_suite_org_v2_manifest.py.
What was missing was a general, machine-readable notion of “these two products are wire-compatible at version N”, and a gate that refuses to promote a suite release candidate whose selected module versions and shared-dependency pins do not actually satisfy the contracts they claim to satisfy. #53 requires exactly that: explicit cross-product compatibility contracts checked before a suite candidate is promoted.
This ADR defines the contract shape, ownership, and gate once, and instantiates it with the single contract that is already real. The three operational product seams (VectorFlow→CHAD ingest, CHAD→Warden escalation, Warden→CHAD writeback) are not modelled here — inventing payloads for them now would be speculative. They are follow-ups under #53 that reuse this mechanism.
Decision 1 — Contract shape and vocabulary
Section titled “Decision 1 — Contract shape and vocabulary”A wire contract is a named, versioned compatibility guarantee between one
producer product and one or more consumer products, recorded as one entry
in a machine-readable registry
(conformance/suite-contracts-v1.json,
schema veltro-suite-contracts/v1). Each entry declares:
id— stable dotted identifier, unique within the registry (e.g.suite.identity-rbac).version— integer contract version. This is the contract version, not a product version; it bumps only on a breaking change to the wire shape (Decision 4).producer— the product that emits the contract’s artifacts.consumers— the products that decode/consume them.supported_versions— per participant (producer and every consumer), the set of contract versions that participant can speak. A candidate is compatible only when every consumer’s set includes the selected contract version.minimum_module_versions— per participant, the minimum released module SemVer that first shipped the contract at this version.decoder— the shared decoder package and minimum compatible version that every Python participant must pin (hereveltro-suite-auth), ornullwhen the contract has no shared decoder dependency. A selected decoder must remain in the same SemVer major and be greater than or equal to this version; a new major fails closed until the registry is deliberately revised. The Python participants (the contract’s decoder parties) are the modules that carry the decoder: Veltro, which publishes it, and CHAD and Warden, which pin it inbackend/requirements.txt(DECODER_MODULESinscripts/suite_contracts.py). VectorFlow is not one: since ADR-014 step 4 (#1066) it mints no session and verifies identity’s browser assertions with the TypeScript@veltrosecurity/suite-authpackage, so its manifest module recordsdecoder: nulland no embeddedselected_decoder_versionsmap names it.evidence— a repository path to the immutable proof that the contract was actually exercised end to end (here the #59 readiness manifest).
The registry’s top-level owner is the literal veltro. Veltro owns the
canonical cross-suite contract language and compatibility policy; producer
identifies the runtime module that emits bytes and is not a transfer of contract
governance.
The registry is a static declaration of guarantees. Released declarations
deliberately carry no rollout timestamps or mutable status; that information is
owned by the evidence manifest, which the registry only references. A contract
may be declared as implementation_status: "unreleased" with null module minima,
decoder, and evidence while its source contract is being frozen. Unreleased
declarations are discoverable but are not guarantees and are excluded from suite
candidate manifests until a later reviewed change supplies immutable release
evidence and converts them to the released shape.
Runtime handshake vocabulary. The over-the-wire handshake is the existing #59
mechanism, unchanged and merely named here: the producer stamps
session_contract_version into the shared session, and each Python consumer
decodes it with the pinned veltro-suite-auth version. session_contract_version
is the identity/RBAC contract’s on-the-wire version token; a future seam contract
declares its own analogous token. This ADR adds no new runtime field or exchange.
Decision 2 — Ownership
Section titled “Decision 2 — Ownership”- Veltro owns the canonical registry, schema semantics, versioning policy,
compatibility gate, and suite-level authority contracts. This ownership is
explicit as top-level
owner = "veltro"and is distinct from runtime roles. - The producer owns its emitting implementation and producer evidence — it is responsible for immutable proof that its released bytes work against the released consumer matrix. For ordinary product seams it proposes the wire shape; for suite identity/authority seams that shape conforms to Veltro’s canonical ADR rather than becoming producer-owned policy.
- Each consumer owns its supported-version declaration and the tests that prove it decodes and enforces the declared versions. A consumer that drops support for a version removes it from its declaration, which is a consumer-driven change.
The registry records both halves in one place so a candidate gate can be evaluated from repository state alone, without executing any product code.
Decision 3 — Candidate gate
Section titled “Decision 3 — Candidate gate”Suite release-candidate generation (scripts/suite_manifest.py generate)
evaluates the selected, tagged module versions and their shared-decoder pins
against the registry before emitting a candidate manifest, and refuses to
emit an incompatible one. The selected contract declaration — the resolved
producer/consumers, the decoder, the selected module and decoder versions, and
the evidence reference — is embedded into the immutable suite manifest.
scripts/suite_manifest.py verify --offline re-validates that embedded
declaration against the registry and cross-checks it against the manifest’s own
module metadata, so editing a candidate by hand (bumping a module version,
swapping a decoder pin, or rewriting the embedded declaration) cannot slip an
incompatible combination past the gate. The gate is fail-closed: any
malformed registry, missing declaration, below-minimum module version,
unsupported consumer version, or mismatched decoder pin blocks the candidate.
For a contract whose decoder is null, the embedded
selected_decoder_versions map must be empty and decoder checks are skipped;
module minima and supported-version checks remain mandatory.
Declarations explicitly marked implementation_status: "unreleased" are not
eligible for embedding and are omitted from candidate generation and verification.
The dependency-free validator/gate lives in scripts/suite_contracts.py
(stdlib only, per Rule 0) and is exercised by deterministic unit tests in the
existing cheap platform CI job.
Decision 4 — Additive vs breaking change policy
Section titled “Decision 4 — Additive vs breaking change policy”- Additive / backward-compatible change (a new optional field, a new
producer capability existing consumers ignore): keep the same contract
version. The gate’s SemVer minima move forward as new module versions ship the addition, but the contract version token on the wire does not change. - Breaking change (removing/renaming a field, changing its meaning, or
otherwise making an old consumer misread a new producer): bump the contract
version, add the new version to the producer’s and consumers’supported_versions, and set the new version’sminimum_module_versionsto the releases that first ship it. Old and new versions may coexist insupported_versionsduring a migration window; a version is retired only by removing it from every participant’s declaration.
Scope of this slice
Section titled “Scope of this slice”This ADR instantiates exactly one contract — suite.identity-rbac v2, the #59
identity/session/RBAC contract — as the first concrete gate. It intentionally
does not model the three operational seams; those are follow-up contracts
under #53 that will add their own registry entries, evidence, and consumer
supported-version declarations using this same mechanism.
Amendment 1 — first operational seam (2026-07-28, #164)
Section titled “Amendment 1 — first operational seam (2026-07-28, #164)”vectorflow.chad-ingestion v1 is the first operational contract added through
this mechanism. VectorFlow 0.11.3 produces the versioned envelope and CHAD
0.26.9 consumes it while retaining legacy-array rollout compatibility. The
contract has no shared decoder package, so its registry decoder is null and
its embedded selected-decoder map is empty. Immutable tagged-source evidence,
including byte-identical vector hashes, lives in
conformance/vectorflow-chad-ingestion-v1-readiness.json.
Amendment 2 — CHAD to Warden alert delivery (2026-07-28, #167)
Section titled “Amendment 2 — CHAD to Warden alert delivery (2026-07-28, #167)”chad.warden-alert-delivery v1 versions the shared CHAD alert body consumed by
Warden’s automatic event-sink and manual escalation lanes, together with each
lane’s existing acknowledgement. Warden 0.8.2 consumes explicit v1 bodies
while retaining absent-version legacy compatibility and rejecting every other
explicit version before intake side effects. CHAD 0.26.10 emits v1 only to
Warden; generic alert webhooks remain unversioned. The decoderless registry gate
requires both released versions and records the shared schema/vector hashes in
conformance/chad-warden-alert-delivery-v1-readiness.json.
Amendment 3 — Warden to CHAD case-reference writeback (2026-07-29, #171)
Section titled “Amendment 3 — Warden to CHAD case-reference writeback (2026-07-29, #171)”warden.chad-case-reference v1 versions Warden’s owning-Case reference written
back to CHAD after case creation. CHAD 0.26.12 consumes exact v1 requests while
retaining absent-version legacy rollout compatibility and rejecting malformed or
unsupported explicit versions before handler side effects. Warden 0.8.3 emits
v1 through one canonical payload builder used by both initial and retried outbox
delivery. The decoderless registry gate requires both consumer-first releases and
records their byte-identical Draft 2020-12 schema/vector hashes in
conformance/warden-chad-case-reference-v1-readiness.json.
Amendment 4 — released runtime diagnostics conformance (2026-07-29, #172)
Section titled “Amendment 4 — released runtime diagnostics conformance (2026-07-29, #172)”VectorFlow 0.11.5, CHAD 0.26.13, and Warden 0.8.4 expose secret-free,
dependency-free GET /api/contracts diagnostics using the shared
veltro-runtime-contracts/v1 response schema. The endpoint is diagnostic only:
it performs no negotiation, peer calls, tenant lookup, or health-policy change.
conformance/runtime-contracts-v1-readiness.json pins each immutable tag,
commit, source path and SHA-256, base-prefixed endpoint, and released declaration.
The protected gate checks out every GitHub tag with the scoped cross-repository
read token, verifies the resulting commit, and hashes the endpoint source before
checking declarations. The cheap
scripts/runtime_contracts_conformance.py gate computes the expected selected
producer versions and supported consumer sets from this registry’s roles; the
readiness file is release evidence, not a second expected-version registry. It
fails closed on response/module shape, schema, module identity/version, ordering,
duplicates, unknown or missing modules/contracts, and any producer/consumer
version-set divergence.
Amendment 5 — product-peer v1 foundation (2026-08-01, #249)
Section titled “Amendment 5 — product-peer v1 foundation (2026-08-01, #249)”vectorflow.veltro-product-peer, chad.veltro-product-peer, and
warden.veltro-product-peer v1 freeze the future product-owned receiver boundary
without claiming that any receiver exists yet. Each product will expose
POST /api/peer/v1/status, POST /api/peer/v1/audit, and
POST /api/peer/v1/mcp. Requests use short-lived veltro-suite-auth bearer
tokens with an exact product-prefixed scope and server-derived native actor/grant
claims. Caller-supplied actor identity is invalid. MCP execution additionally
requires the curated 77-tool authority matrix and a single realm-wide or
exact-team grant selected for the target product before dispatch.
The language-neutral schema, valid/invalid vectors, route deadlines, freshness
limits, stable error/HTTP vocabulary, and idempotency-conflict rule live in
conformance/product-peer-v1/. Dependency-free, unreleased consumers live in
@veltrosecurity/product-peer and veltro-product-peer; packed-artifact gates
prove their public exports and deny source, test, and private-key material. The
registry declarations remain explicitly unreleased with no invented module
minimum or readiness evidence. This amendment does not add product routes, wire
Compose, publish packages, release modules, deploy, or cut over any caller.
Amendment 6 — authority-envelope v2 decoder readiness (2026-08-10, #230)
Section titled “Amendment 6 — authority-envelope v2 decoder readiness (2026-08-10, #230)”suite.authority-envelope v2 freezes the Veltro-owned issuer-only ES256
suite-service authority envelope: its Draft 2020-12 schema and positive/negative
vector corpus live in conformance/suite-auth-service-v2/, its shared decoder is
veltro-suite-auth 2.0.x, and the CHAD and Warden adapters verify the exact
vectors but are staged disabled — there is no producer and no runtime
acceptance yet (issue #130 stays open).
This situation does not fit implementation_status: "unreleased", which
means “source contract still being frozen” and nulls decoder/minima/evidence —
here the source is frozen and the decoder, minima, and digest are exactly what a
candidate must bind to. So this amendment introduces a second status,
implementation_status: "decoder-readiness", for contracts that are
verification-only: the schema, vectors, and decoder are real and immutable,
but the producer does not exist and consumers are disabled.
A decoder-readiness contract narrows its gated participants to its consumers
(the producer is the veltro owner, not a deployable module and not available),
requires the decoder, the per-consumer minimum decoder-capable release, and a
contract_digest additive key recording the frozen schema/vector digests over
the Veltro repo’s own conformance/suite-auth-service-v2/ files. Its candidate
declaration embeds implementation_status: "decoder-readiness" and
producer_available: false, so the evidence can never be read as a claim of v2
producer availability.
The gate for a decoder-readiness contract is dormant until every consumer is
at or above its minimum decoder-capable release: below that nobody has staged
the decoder, so the current v1 release matrix is preserved unchanged and the
contract embeds nothing. Once a consumer crosses its minimum, complete decoder
declarations become mandatory and the ordinary fail-closed checks (module
minima, supported versions, exact shared-decoder pin) apply. verify --offline
additionally recomputes the recorded schema/vector digests against the repository
and fails closed on any missing, stale, or cross-repository material, and rejects
a declaration embedded below the decoder-capable minimum. These contracts remain
excluded from the released runtime /api/contracts declarations, because a
disabled consumer must never claim to produce or consume them at runtime.
Amendment 7 — product-peer v1 audit levels (2026-10-17, #1667)
Section titled “Amendment 7 — product-peer v1 audit levels (2026-10-17, #1667)”The product-peer v1 audit read gains an optional level, a Decision 4 additive
change, so the contract version stays 1. Levels rank important, normal,
verbose. A request may carry level (the POST body, or ?level= on GET), and
means that level and above: normal returns important and normal rows. Without
it a product returns every row. Each event may carry a level too; a row without
one, or with a value the reader doesn’t know, reads as normal.
Only VectorFlow’s receiver accepts the request field: CHAD and Warden answer 400
to any key outside contract_version, operation, cursor and limit. So
identity sends level to VectorFlow alone, and only when the reader picked a
filter (#1668). It filters every returned page itself before merging, so CHAD,
Warden and older VectorFlow rows are filtered too. VectorFlow is the first
producer to filter and emit it.
Consequences
Section titled “Consequences”- Promotion of a suite candidate now provably requires a compatible identity/RBAC matrix; a too-old module, a wrong decoder pin, or a consumer that no longer supports the version is a hard, actionable failure at candidate time rather than a latent runtime break.
- The suite manifest schema advances to
veltro-suite-manifest/v2to carry the embedded contract declarations. Preflight still accepts the currently releasedveltro-suite-manifest/v1manifest (suite 0.4.3) as the “current” side of a v1→v2 upgrade check, so no released suite is stranded. - Adding a new seam contract is now a documented, additive operation: a registry entry plus producer evidence and consumer supported-version declarations, with no new bespoke tooling.