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).
Context
Section titled “Context”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.
Decision
Section titled “Decision”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.shis public already becauserelease.ymlcalls 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.
Alternatives considered
Section titled “Alternatives considered”| 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. |
Consequences
Section titled “Consequences”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.
Pre-publication checklist
Section titled “Pre-publication checklist”Ordered. Nothing here is reversible after step 5.
- 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.
Untrack the files above; add them to✅ done 2026-07-26, at six files rather than eighteen — see the amendment under consequence 5..gitignorebeside the existing block, with the same explanation.- Land the documentation sweep listed under Consequences.
- Set the GitHub description, topics, and license metadata.
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.- Flip the repository to public.
- Verify from a machine with no GitHub credentials:
install.shcompletes, reports the verification level it achieved, andcargo install --gitworks. 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. - 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:
v0.9.0-rc.2is a published GitHub Release with zero assets.gh release view v0.9.0-rc.2returns"assets": []. The release workflow never ran, so no binary, no.sha256and no.cosign.bundlewas 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.- 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:
- Third-party actions are pinned by tag or branch, not by digest. The
sharpest instance is
taiki-e/install-action@crossatrelease.yml:76— a moving ref inside the job that now holds the signing identity;dtolnay/rust-toolchain@masteratci.yml:123is the same shape with far less reach. Not fixed here, and the reason matters: fordtolnay/rust-toolchain@stableandtaiki-e/install-action@crossthe ref is the argument — pinning to a digest means converting them to explicitwith:inputs, which changes behaviour.release.ymlonly runs on a published release, so that change has to be proved by an actual release rather than landed blind. - 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.
What this does not decide
Section titled “What this does not decide”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.