Drift-proof release & versioning¶
Design principle (operator): "This repository prefer to design systems that cannot drift at all." Prevention over detection, detection over repair.
Every value that appears in two places will eventually appear differently in those two places. This page records, for each release value that has actually drifted, which mechanism now prevents it and what class that mechanism reaches.
| Class | Guarantee | Where the failure surfaces |
|---|---|---|
| A — cannot drift | The value is derived from one authority, not duplicated | Nowhere; there is no second copy to disagree |
| B — cannot be committed | A gate fails closed at generation/commit time | Before the wrong value enters the tree |
| C — caught at promotion | Detected during release assembly | Before publication |
| D — detected after the fact | A report | After the damage |
1. Attestation compatibility band — Class A¶
connector_source_attestation.SOURCE_COMPATIBILITY used to be a literal that
.bumpversion.cfg rewrote on every bump, compared for exact string equality by the
admission gate. On 2026-07-28 the commit "Bump 2.1.0 → 2.1.1" rewrote the floor to
>=2.1.1,<3 and invalidated all 68 provider attestations — 22 minutes after the only
key that can re-sign them was believed lost. Every patch release did this.
Now:
source_compatibility()derives a minor-floor band (>=MAJOR.MINOR.0,<MAJOR+1) fromagent_utilities._version.__version__and from the engine version pinned indeploy/release/compatibility-matrix.yml. A patch bump recomputes the identical string — there is nothing to rewrite.- Validation is containment, not equality: a declared band is admissible when it is bounded on both sides and still contains the released version. An attestation signed under 2.1.0 stays valid at 2.1.1 and 2.9.9, and is rejected only at the major boundary it already declared.
.bumpversion.cfgno longer targets the module, andscripts/check_version_consistency.pyemitsbumpversion-attestation-coupling:<path>if any bump section reaches an attestation input again (the Class-B backstop).
Test: tests/unit/knowledge_graph/integrations/test_connector_source_attestation_drift.py
— simulates a patch bump against a freshly signed attestation and asserts the bytes and
the signature both survive, plus the structural assertion that no bump section can reach
the band.
2. Connector MCP server alias — Class A (+ Class B for the source tree)¶
deploy/mcp-fleet.registry.yml is the one authority for what a fleet server is called.
Every provider restated that name in mcp_source_presets.json, and
generate_connector_manifests.py copied it verbatim: 27 providers named their
distribution (github-agent) where the fleet runs the service (github-mcp), and 9
more named services the committed registry did not contain at all.
Now:
- The generator derives
serverfrom the registry by distribution name (registry_server_alias) and normalises the preservedrawpreset with it, so a wrong alias cannot enter a signed manifest. A provider absent from the registry has no derivable alias and fails closed before any artifact is projected. - A preset that restates a different alias is a hard error, not a warning.
scripts/check_connector_source_presets.pyreports (and with--fixrepairs) the same disagreement across the fleet, so the source tree cannot hold it silently.
Why the registry had rotted¶
Host ports were assigned by list index, so adding one provider renumbered the published
port of every service after it — regeneration was a deployment-breaking change, so the
registry was left stale instead. gen_mcp_fleet_registry.py now preserves already
allocated ports and gives only genuinely new services the next free port, which makes
regeneration a no-op for unchanged services and lets the freshness expectation be
enforced at all.
Test: tests/unit/scripts/test_connector_server_alias_drift.py.
3. Cross-project version cascade — Class A¶
scripts/release/version_cascade.py models the release train as three tiers:
epistemic-graph X.Y.Z
-> agent-utilities pyproject: epistemic-graph[full]>=X.Y.Z,<{X+1}.0.0 (+ the matrix)
-> ~68 providers: agent-utilities[extras]>={A}.0.0,<{A+1}.0.0
Provider pins are a major band derived from the agent-utilities major, so a patch or
minor bump produces zero provider edits by construction. Only a major bump moves them —
and it moves all of them, which is exactly the fastmcp-4 case (au 2.x → 3.0.0
invalidates every agent-utilities>=2.0.0,<3.0.0 ceiling). The planner reports that as
severity: major with an explicit breaking_notes entry.
Dry-run is the default; --apply writes files and checks each replacement target still
matches before writing (fail closed on concurrent modification). It never runs git — each
provider is its own repository and committing/pushing is a separate, deliberate act.
Test: tests/unit/release/test_version_cascade.py.
4. Release-signing key custody — Class A (custody) + Class B (agreement)¶
The 2026-07-28 incident had two distinct causes.
Custody. The signing seed's source of truth was env:// backed by a local
~/.config/agent-utilities/runtime-secrets.json. That file was overwritten in place with
a different seed; it has no version history and no backup, so the original looked
destroyed. The same seed had been written to OpenBao KV v2 at
apps/agent-utilities minutes earlier and survived untouched — because KV v2 keeps every
version. A store that can be silently overwritten beyond recovery is not custody.
release_signer_for_publication() is now the only signer a publication path may use, and
it refuses any reference that is not vault:// or secret://. The required
configuration is:
SECRETS_BACKEND=vault
SECRETS_VAULT_URL=<openbao url>
SECRETS_VAULT_MOUNT=apps
ONTOLOGY_RELEASE_SIGNING_PRIVATE_KEY_REF=vault://agent-utilities#ONTOLOGY_RELEASE_SIGNING_PRIVATE_KEY@2
(the vault extra — hvac — must be installed on the release host).
Which token authenticates this read (D-AR-4). VaultBackend (security/secrets_client.py)
authenticates with whatever static token is on VAULT_TOKEN (or token= passed explicitly) when
no OIDC/AppRole/Kubernetes auth is configured. That token must carry the agent-apps-rw
policy — the same policy every service's own OPENBAO_TOKEN carries, scoped to create/read/
update/delete on apps/data/* and list/read/delete on apps/metadata/*. Two credentials look
plausible here and only one is correct:
- ✅
OPENBAO_TOKEN(apps/<service>, e.g.apps/openbao-mcp, policyagent-apps-rw) — readsapps/data/agent-utilitiesandapps/metadata/agent-utilitiesdirectly. This is whatVAULT_TOKENshould resolve to for release signing. A freshly-minted short-TTLagent-apps-rwtoken (see below) also works and is the more auditable choice for a one-shot release run. - ❌
OPENBAO_ADMIN_TOKEN(apps/openbao-mcp, policyagent-apps-token-minter) — do not setVAULT_TOKENto this value. Despite the name, this token is deliberately scoped tocreate/update/sudoonauth/token/createonly (minting freshagent-apps-rwtokens for others to use) and holds zero capability onapps/data/*orapps/metadata/*— it 403s on bothapps/data/agent-utilitiesandapps/metadata/agent-utilitiesby design, not by bug (seeservices/openbao/k8s/bootstrap-policies.shand the openbao-mcpsecret-vault-managerskill'srotation-operational-facts.mdfact 8). If the operator need a fresh scoped credential instead of the sharedOPENBAO_TOKEN, mint one FROMOPENBAO_ADMIN_TOKENfirst (POST auth/token/create {"policies":"agent-apps-rw","ttl":"..."}—policiesmust be a plain string, not a JSON list) and use that minted token asVAULT_TOKEN; never grantOPENBAO_ADMIN_TOKENitself direct data-plane access.
Pin the KV version (D-AR-5). The bare form above (no @<version>) resolves whatever KV
v2 version is current at signing time — reproducible only as long as nobody else writes to
apps/agent-utilities in the meantime. resolve_ref() (security/secrets_client.py) supports
an explicit #field@<version> pin (CONCEPT:AU-KG.ontology.release-key-rotation, D-OC-1) so the
reference names an immutable version instead of "latest"; the release host's actual
ONTOLOGY_RELEASE_SIGNING_PRIVATE_KEY_REF should always carry an explicit @<version> suffix
(bump it deliberately, as a recorded step, whenever deploy/release/signing-key-rotation.yml
gains a new entry) rather than track "latest" implicitly.
Agreement. The mismatch was only discovered at admission time, long after artifacts
had been produced. assert_signing_key_matches_locks() performs the same comparison at
signing time, before a single byte is written, and refuses when the seed that will
sign is not a seed ontology.lock pins. scripts/update_ontology_lock.py no longer adds
the ambient runtime key to its trusted set — trust comes from declared sources only.
Rotation is a recorded transaction. deploy/release/signing-key-rotation.yml is an
ordered ledger: each entry names the key rotated from, the key rotated to, the
authorisation, and the artifacts that moved. release_trusted_public_keys() includes the
key the newest entry designates, so a new trust anchor becomes trusted by being
recorded, never by merely being present.
Test: tests/unit/knowledge_graph/ontology/test_release_key_custody.py.
The controlled release job (GOC-16/BUG-234). This section documents the read path
mechanism; it does not by itself give any real pipeline a way to reach OpenBao with the
right identity. docs/release/connector-manifest-signing-custody.md is the operator
runbook that closes that gap for connector-manifest signing specifically: the exact
OpenBao policy/Vault-Kubernetes-auth-role commands, the Kubernetes Job
(deploy/release/connector-manifest-signing-job.yaml) that holds the key via workload
identity only (never a materialized k8s Secret, never a GitHub Actions secret), and the
keyless diff/freeze report (connector-manifest-diff in .github/workflows/advisory.yml)
that gives the operator the exact frozen commit + dependency-lock digest to sign against.