Governance Contracts

The index bot and its workflows enforce twenty governance contracts, originally defined in design_spec_registry_indirection.md against the pointer-only index and carried forward — several reinterpreted — under the locked wire format. Design authority: adr_index_bot_and_workflow_security.md and its Amendment A1, adr_fork_pr_announce.md.

IDContractStatus
G-01Schema-shape validation against the JSON SchemasKept, reinterpreted — three schema files now, run by check-jsonschema in the unprivileged schema-validate job, never imported into the bot
G-02name equals the path-derived logical name (p/<ns>/<pkg>.json<ns>/<pkg>)Kept — hand-rolled check, not schema-expressible
G-03repository host allowlist, checked before any network callKept — SSRF-ordering guard; the allowlist itself is per-deployment policy, committed at .github/index-policy.json (see below)
G-04New entry file → new-package label, mandatory human review, never auto-mergeKept — namespace-fit judgment is Namespace Policy's contract; this gate is the mechanical enforcement
G-05Green refresh → auto-merge eligible; yank/deprecate/transfer/owners/pointer change → human review alwaysKept, key set expanded — human-review-required keys are repository, owners, status, deprecated_message, superseded_by, and any mutation of an existing tag row's yanked field
G-06Render: source tree → deploy treeReinterpreted — no longer an identity copy; reachability-filtered CAS copy, config.json emission, /c/index.json emission, /data/catalog/** emission. Per-package detail pages are VitePress dynamic routes that glob the committed p/*/*.json tree directly at build time — not a bot-emitted wrapper-page tree
G-07Deploy is idempotent; no-op on an unchanged treeKept
G-08repository_dispatch payload validated via env-var indirection, regex-checked before useKept, regex reinterpreted — exact 2-segment package-id form (see Namespace Policy)
G-09Field provenance partition: registry-derived vs human-governed fields never cross-contaminateKept, field set updated — see Entry Schema
G-10Bounded backoff retry on manifest fetch before giving upKept — exhaustion exits 75
G-11Idempotent, cascade-safe convergence; diff routes to G-04/G-05 merge policyKept — idempotency is now an explicit required test ("run twice, second diff empty")
G-12Nightly reconcile regenerates every entry, diffs, opens one PR with all driftKept — see Run a Reconcile Dry Run
G-13Separate reconcile-maintained state file for anomaly detectionEliminated as a separate file — the committed root is the observation ledger; the anomaly check reads it directly
G-14Sibling-repo CI hardening: permissions: default-deny + SHA-pinned actions everywhereKept
G-15Ownership proof: fetch the physical manifest, verify the embedded canonical identifier equals the entry's logical nameReinterpreted as a pluggable loud-skip seam — the identifier-embedding convention is unconfirmed against actual publishing behaviour, so the probe returns confirmed, mismatch (blocking), or unconfirmed (warns, surfaced on the PR, never a silent pass)
G-16Privileged/unprivileged workflow splitKept in full — schema-validate runs unprivileged against PR-head content; governance-gate is the privileged, API-only job that never checks out PR-head code
G-17Announce abuse bounds: namespace-scoped PAT, per-package concurrency groups, schema-validated payloadRetired — no namespace-scoped PAT under the fork-PR lane; abuse bounds are the fork-PR spam posture (label failed-check PRs, stale-close)
G-18Reconcile disabled/dry-run until the seed republish batch is parity-verifiedReinterpreted — a repo Actions variable, RECONCILE_DRY_RUN, gates mutation; flip documented at M-1 Flip
G-19Owners-membership gate for the machine laneNew — a fork PR qualifies for auto-merge only if BOTH hold: its author's github_id is in the target root's committed owners[], AND every changed path stays inside those roots' refresh scope (the roots themselves plus their own packages' p/<ns>/<pkg>/o/sha256/<64-hex>.{json,md,svg,png} CAS objects). Both are evaluated by the privileged governance job from PR metadata + base-branch root, never PR-head content. The path condition fails closed: any path outside that scope — a workflow file, bot/** source, another package's files, an unrelated deletion — routes the PR to the human lane, so an owner of one package cannot attach arbitrary repository content to a refresh-classified PR (ADR-6 FP-5)
G-20Maintainers-YAML reviewer assignmentNew — human-lane PRs get reviewers assigned from a committed maintainers.yml (list of {github, github_id}) by the privileged governance job, plus an idempotent bot review-request comment

Registry-Host Policy (G-03)

repository is the pointer every ocx client follows to fetch bytes, so the set of registry hosts an index will accept is a supply-chain trust decision. That set is per deployment, not compiled into the bot: OCX's index format is designed as one format, many copies, and an organization running its own index points at its own registry, not at ghcr.io.

Each index repo commits its own policy at .github/index-policy.json:

json
{
  "registry_hosts": ["ghcr.io"]
}

For this index the effective policy is exactly ghcr.io — a root naming any other host is rejected before the bot makes a single network call, and a PR that widens the shipped file fails a named test in the bot's security suite.

Two properties are deliberate:

  • A committed file, never a repository or Actions variable. "Extend only via reviewed PR" is the control. A settings-page variable could widen the set of trusted registries with no diff and no reviewer; a file cannot.
  • A host with no registry adapter is refused up front. The bot implements one registry client (ghcr.io). Allowlisting a host it cannot fetch from would produce entries that pass every validation check and then fail every download, so the bot refuses that policy at startup instead — with an error naming what is missing. Running an index on another registry needs an adapter for it, not just an allowlist entry.

Auto-Merge Decision

PR opened (announce or reconcile)
  ├─ schema-validate green?
  ├─ classified refresh (no G-05 key touched)?
  ├─ every changed path inside the touched roots' refresh scope?
  └─ governance/review-required green?
       all yes → gh pr merge --auto (branch protection completes the merge)
       any no  → blocked until a human approves

New-package PRs (G-04) and any PR touching a G-05 human-review-required key are never auto-merge eligible, regardless of how green the automated checks are. Nor is a PR that changes anything outside the refresh scope of the roots it touches — the classifier selects which files are classified, never which files are allowed.