Skip to content

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 shared veltro-suite-auth contract 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.

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 (here veltro-suite-auth), or null when 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 in backend/requirements.txt (DECODER_MODULES in scripts/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-auth package, so its manifest module records decoder: null and no embedded selected_decoder_versions map 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.

  • 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.

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’s minimum_module_versions to the releases that first ship it. Old and new versions may coexist in supported_versions during a migration window; a version is retired only by removing it from every participant’s declaration.

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.

  • 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/v2 to carry the embedded contract declarations. Preflight still accepts the currently released veltro-suite-manifest/v1 manifest (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.