Skip to content

ADR-0009 — This repository becomes public, and stays canonical

  • Status: Accepted (2026-07-26)
  • Related: ADR-0008 (open core), VERIFYING_RELEASES.md, and three documents this decision rests on that step 1 withholds from the published tree — the v1.0 public-repository scope analysis, the v1.0 audit and hardening plan §2.1, and the v0.9→v1.0 release spec §4. All three are readable in history at the commit that last touched them.
  • Supersedes: the audit plan’s §2.1 decision that “the repository is not made public before v1.0.0”. Its four consequences are not overruled — three of them simply stop applying, and this record says which.
  • Amends: ADR-0008’s distribution consequences (see the addendum there).

The audit of 2026-07-26 deferred publication to after v1.0 and accepted the costs: no build provenance, and an install path to be invented. Working through that install path is what reversed the decision.

Two facts, both verified rather than assumed:

Rivet currently has no install path for anyone but its author. All four fetches in install.sh resolve against a private repository and 404 or fail authentication anonymously — the release API, the release download, cargo install --git, and the source-tarball fallback. rivetsnap-cli is not on crates.io. The Phase 2 gate is explicit that this is not waivable: “a private repository is not an excuse for an install path that only works for the author.”

Attestation is not lost to privacy — it is lost to the building repository being private. release.yml guards its provenance step on if: ${{ !github.event.repository.private }}, and GitHub refuses to persist an attestation for a user-owned private repo. Provenance binds the repository that ran the build. So a public repository that runs release.yml gets the attestation back; a public repository that only holds assets does not.

That reframes the question. Publication is not a cost paid for openness with no return — it is the cheapest way to buy back a guarantee §2.1 had written off, and it removes the install problem instead of solving it.

The third consequence in §2.1 already conceded the rest: cargo publish uploads full crate source to crates.io permanently, so v1.0 publishes Rivet’s source whatever this record decides. Privacy was never protecting the code.

1. This repository — fobiat/Rivet — is made public, and remains the single canonical tree.

Not a new repository, not a mirror, not an assets bucket. The repo that holds the code is the repo that builds, signs, attests, publishes, and that install.sh points at. One tree, so the source a reader can read is provably the source that built the binary.

2. install.sh is not repointed. REPO="fobiat/Rivet" is already correct and all four fetch paths start working the moment the switch is flipped. The scope document’s “only REPO repointed” assumed a differently-named public repo; publishing this one is cheaper still. This is the entire implementation cost of §2.4 on the code side.

3. Build provenance returns for v1.0. The guard activates by itself — no workflow edit, only the comment beneath it, which currently explains a state that will no longer be true. v1.0 ships signed and attested. The reproducibility workflow becomes a claim a stranger can execute rather than one they are asked to believe, and deny.toml becomes evidence rather than an assertion.

4. History is published as-is — every commit, not a squash. It was scanned on 2026-07-26 at 201 commits, and the scan is repeated as checklist step 0 for whatever has landed since: no private key, token, credential, or .env was ever committed or later deleted. Every match for AGE-SECRET-KEY-1, -----BEGIN … PRIVATE KEY-----, ghp_… and AKIA… is either a test fixture or a literal in the credential scanner’s own detection rules (crates/rivet-core/src/policy.rs). Two author identities appear, both the maintainer’s.

Squashing to a single v1.0 commit stays possible and is declined: development history is itself a trust signal for a young security tool, and rewriting it would invalidate every SHA and tag the handovers, ADRs and CHANGELOG cite.

5. Planning and process leave the tracked tree at HEAD — and this buys less than it looks like, which is stated here rather than discovered later.

Six files are untracked at publication:

File Why it is withheld
docs/v0.9-to-v1.0-release-spec.md Unshipped roadmap and acceptance gates
docs/v1.0-audit-and-hardening-plan.md Audit findings still being worked
docs/v1.0-public-repository-scope.md Superseded by this record
docs/v0.5-security-review-brief.md Scopes a review that has not happened
docs/v0.9-security-review-brief.md As above
Rivet Project Charter.md Internal vision document

They join CLAUDE.md, AGENTS.md, tasks.md and handoverdaily.md, which .gitignore already untracked at v0.8 for exactly this reason.

Consequence 4 applies to all of them: because history is published, every one of these files remains readable via git log at the commit that last touched it. Untracking stops future revisions from being published; it does not retract the existing ones. .gitignore:12-14 already recorded that trade in its own words — “it does not erase the old ones, which would mean rewriting every commit SHA and tag” — and this record extends the same trade rather than inventing a new one. Nothing in the six is harmful public; the boundary exists to keep docs/ readable for users, not to conceal.

The forward-looking value is real and is the point: unshipped roadmaps, in-flight audit findings, and review briefs written before their fixes land do not belong in a tree strangers read.

6. web/, .github/workflows/web.yml, and all of scripts/ are public — a deliberate departure from the scope document, which put those 36 files on the private side on the grounds that nothing requires them to be public. Nothing requires it, but splitting them costs more than it saves:

  • The site syncs from docs/ at build time and fails its build when a page it expects is missing. That drift check only works while both live in one tree; splitting them turns a build-time guarantee into a cross-repo one.
  • It reintroduces the two-places problem this record rejects for code, for a static site with nothing sensitive in it.
  • scripts/package-release.sh is public already because release.yml calls it. Publishing five sibling development scripts and withholding the rest of the directory is fiddly for no gain.

236 of the 242 tracked files stay public — 235 if ADR-0008 joins the six, which is the one item left open below.

Option Why not
Assets-only public mirror (private canonical, public tarballs + .sha256 + .cosign.bundle) No attestation — provenance binds the building repo. Needs object storage or a second repo plus a cross-repo write token, which is a credential worth more than the repo it lives in. Adds a fetch path to maintain. Keeps history private, which consequence 4 shows is the only thing it actually buys.
Public mirror holding code The worst square: drift, plus a public tree a reader cannot verify against the crates.io tarball (sync lag and substitution look identical), plus still no attestation. Rejected in the scope document and rejected here.
Cargo-first, no binaries (cargo install rivetsnap-cli only) Contradicts release spec §3, which ships prebuilt binaries for three targets and a Homebrew tap. Publishes the source to crates.io anyway, so it withholds the history and the workflows while still giving away the code — and asks every user to have a Rust toolchain.
Defer publication past v1.0 Fails the Phase 2 gate as written. Ships a v1.0 nobody outside can install, which is not a v1.0.
New public repo with fresh or filtered history The only option that actually withholds the withheld six. Costs: 201 commits of visible development, every tag re-created, install.sh repointed, and every SHA cited in the CHANGELOG, ADRs and handovers left dangling. Paid to hide documents that are not harmful public.

Immediately, and by itself: issues, pull requests, CI logs, branches, and the entire commit history become world-readable. This is mostly the point — it is what makes the supply-chain and reproducibility work checkable by someone who is not the author.

Publication is a one-way door. Forks, mirrors, and archives make it unrevocable. It is taken here as its own decision, on its own reasoning, and not as a launch checkbox — which is what the audit plan’s escalation trigger required.

ADR-0008’s seam must hold from day one. Commercial code never lands in this tree. That was already the rule; it stops being enforceable by obscurity and starts being enforceable only by discipline.

The external security review (Phase 4.3) no longer needs a collaborator invitation, and its brief must drop the line about the reviewer looking at a tree that will still be private at v1.0 — it changes what they can assume about who else has looked.

§4.6’s repository metadata stops being invisible. The GitHub description, topics, and the dual MIT OR Apache-2.0 license need to be right before the switch is flipped, not after.

What must be swept, because each is honest today and becomes wrong on publication: release.yml’s guard comment, VERIFYING_RELEASES.md check 3, SECURITY.md’s “not currently checkable” row, ADR-0008’s distribution consequences, audit plan §2.1, release spec §4 and the v1.0 gate.

Ordered. Nothing here is reversible after step 5.

  1. Re-run the secret scan over the commits added since 2026-07-26. The clean result in consequence 4 is a snapshot, not a standing property.
  2. Untrack the files above; add them to .gitignore beside the existing block, with the same explanation.done 2026-07-26, at six files rather than eighteen — see the amendment under consequence 5.
  3. Land the documentation sweep listed under Consequences.
  4. Set the GitHub description, topics, and license metadata.
  5. Confirm no workflow secret is referenced by a workflow that a fork could run — public forks change who can trigger what.done 2026-07-26. Result below.
  6. Flip the repository to public.
  7. Verify from a machine with no GitHub credentials: install.sh completes, reports the verification level it achieved, and cargo install --git works. This is the Phase 2 gate; it is not met until someone unauthenticated has actually run it. Blocked on the billing state above — there are no release assets to verify.
  8. Cut a release and confirm the attestation step ran rather than skipped. Also blocked on billing: the release workflow currently does not start.

Blocker found while executing this checklist (2026-07-26)

Section titled “Blocker found while executing this checklist (2026-07-26)”

GitHub Actions has not run for this repository since at least 2026-07-25. Every one of the twelve most recent workflow runs failed in under seven seconds, with the same annotation on every job:

The job was not started because recent account payments have failed or your spending limit needs to be increased.

The jobs are not failing — they are not starting. Two consequences that nothing else in the plan had recorded:

  1. v0.9.0-rc.2 is a published GitHub Release with zero assets. gh release view v0.9.0-rc.2 returns "assets": []. The release workflow never ran, so no binary, no .sha256 and no .cosign.bundle was ever attached. The signing and packaging pipeline has therefore never executed end to end against a real release — install.sh’s verification logic was exercised against fake releases over local HTTP, which tests the script but not the workflow that is supposed to feed it.
  2. Checklist steps 6 and 7 below cannot be met until this is resolved, independently of publication. Step 6 needs release assets to download and verify; step 7 needs the release workflow to run at all.

Publication is likely to fix it by itself, which is a benefit this record did not claim and should: GitHub Actions is not metered for public repositories on standard runners, and this project uses only ubuntu-latest and macos-14. That is a convenience, not a reason — it must not become the argument for publishing, and the billing state should be understood either way rather than routed around.

Resolving the account billing is the maintainer’s, and is not a step this record can automate.

Checklist step 4 — the fork and secrets audit (2026-07-26)

Section titled “Checklist step 4 — the fork and secrets audit (2026-07-26)”

Headline: no fork-triggerable workflow can reach a secret or a write-scoped token, because there are no secrets at all. grep -n "secrets\." across all four workflows returns nothing. Signing is keyless Sigstore over OIDC and uploads use the auto-provided github.token, so there is no long-lived credential in the repository for a fork to try to reach.

Workflow Fork-triggerable Token scope
ci.yml yes (pull_request) contents: read, all 7 jobs
web.yml yes (pull_request) contents: read
reproducibility.yml yes (pull_request, 4 paths) contents: read
release.yml no (release: published, workflow_dispatch) see below

No pull_request_target, no workflow_run, no issue_comment, no self-hosted runners — the four triggers that make fork PRs dangerous are all absent. Interpolation inside run: blocks is confined to matrix.* (workflow -authored) and github.event.release.tag_name/inputs.tag, both of which require write access to set.

One real finding, fixed here. A GitHub OIDC subject identifies the workflow, not the job: it is …/.github/workflows/release.yml@<ref>, which is exactly the string install.sh pins in cosign_identity_regexp. id-token: write was granted at workflow level, so the sbom job could mint a certificate every Rivet installer accepts — a job that signs nothing, and that installs cargo-cyclonedx through a third-party action. Permissions are now default-deny at workflow level and opted into per job; only the two jobs that call cosign sign-blob hold id-token: write. ci.yml gained a workflow-level contents: read for the same reason: so a job added later inherits read-only rather than the repository’s UI-configured default.

Two items deliberately left open, because neither can be closed from the tree:

  1. Third-party actions are pinned by tag or branch, not by digest. The sharpest instance is taiki-e/install-action@cross at release.yml:76 — a moving ref inside the job that now holds the signing identity; dtolnay/rust-toolchain@master at ci.yml:123 is the same shape with far less reach. Not fixed here, and the reason matters: for dtolnay/rust-toolchain@stable and taiki-e/install-action@cross the ref is the argument — pinning to a digest means converting them to explicit with: inputs, which changes behaviour. release.yml only runs on a published release, so that change has to be proved by an actual release rather than landed blind.
  2. Repository settings are not in this tree and must be set before step 5: default workflow permissions → read-only; “Allow GitHub Actions to create and approve pull requests” → off; require approval for fork PRs from all outside collaborators, not just first-time contributors. A read-only permissions: block in every workflow is what makes the first of these belt-and-braces rather than load-bearing — but it should still be right.

Whether ADR-0008 itself is published. It is deliberately not in the six — it is the one file where “untracked at HEAD, readable in history” is an arguably incoherent posture: a commercial strategy nobody can find at HEAD but anyone can git log to. Under this record it stays tracked and therefore public by default; the alternative is to untrack it as a seventh and accept that the distinction is cosmetic. The honest options are to publish it deliberately (ADR-0008’s own second obligation is “state it publicly”, and the load-bearing rule is already verbatim in CONTRIBUTING.md and on the site) or to accept that history makes the distinction cosmetic. Left open, on its own, as a communications decision rather than an architectural one.