Public: anyone can read this; the crew can change it.
Cost ledger: the chain is committed to git in signed batches, so another forge can verify it #12282
Check · Review · Land · Live done — Landed and live in production.
The cost ledger's chain now goes to a `ledger` branch in signed batches, and `bun scripts/ledger-verify.ts --git <repo>` checks it from a clone with no database. The new tests, `test/cost-ledger.test.ts` and `test/timers-named.test.ts` pass, and `bun scripts/gate.ts` and `tsc` are clean. `test/ssh-port-taken.test.ts` fails here, and it failed the same way with my changes stashed. I did not run the…
shipped production is serving it deploy d003798
2 earlier comments
The cost ledger's chain now goes to a ledger branch in signed batches, and bun scripts/ledger-verify.ts --git <repo> checks it from a clone with no database. The new tests, test/cost-ledger.test.ts and test/timers-named.test.ts pass, and bun scripts/gate.ts and tsc are clean. test/ssh-port-taken.test.ts fails here, and it failed the same way with my changes stashed. I did not run the full suite or scripts/fast.ts.
What changed
- Batching: a timer named
ledger-gitfires every hour. Only when a month file has lines the branch lacks, it appends them as<yyyy-mm>.jsonlat the branch root in one commit. No new lines means no commit. - Where it commits: the repository is
FORGE_LEDGER_REPO(owner/name), elseFORGE_REPO. With neither set it does nothing, so production will not commit until one is configured. That setting is the one thing you need to do. - Spool mismatch: if the branch holds lines the spool lacks, the batch commits nothing over it and logs the failure.
- Signing: commits are signed with the forge's SSH host key (
<data>/ssh/host_ed25519) as anSSHSIGin thegpgsigheader. The signing code is new, insrc/ssh/sshsig.ts, because the sandbox has nossh-keygen. I could not rungit verify-commithere, so the format is checked only by our own verifier. - No shelling out with entry text: it reuses
commitFiles, which gained an optionalsignhook. Entry text only reaches git as blob content, never as an argument, a path or a message. I added no dependencies. - Verifier:
--gitchecks every commit's signature and the whole hash chain. A bad entry is reported asbroken at <file>:<line>, and a bad signature asbroken at <commit>.--key "<ssh-ed25519 line>"pins the signer. Without it the commits must agree on one key, and the key is printed. - Doc:
docs/features/cost-ledger.mdnow says where the chain lives and how to clone and verify it.
Tests (test/ledger-git.test.ts):
- A batch makes one commit, and a second batch with nothing new makes none.
- A rewritten entry in the git file breaks verification at its line.
- A commit edited after signing fails the signature check.
- A wrong pinned key fails.
- The timer batches at most once an hour.
Left out
- The verifier does not check that each commit only appends to the files. It relies on the signatures plus the chain.
- The hourly gap is kept in memory, so a restart can batch earlier than an hour after the last one.
- The commit does not announce a
pushevent on the bus. - The signing key is not published on any page.
The cost ledger's hourly signed commits to the ledger branch are unchanged. This push only rewraps five comment lines that were over 100 columns, which is what the gate flagged.
The lines were in src/git/write.ts, src/main.ts, src/ssh/sshsig.ts (two lines) and test/ledger-git.test.ts. Two of the sshsig.ts comments were slightly reworded to fit. No code changed.
Locally, bun scripts/gate.ts and bun x tsc --noEmit are clean, and test/ledger-git.test.ts passes (4 tests). The gate only passed once the rewrap was committed, so it reads committed files. I did not run any other tests, since only comments changed. I did not look at the forge's check result on the new push.
To try it: bun scripts/gate.ts on this branch should print nothing.
The two failures this pull caused are fixed and pushed. I ran test/job-slices.test.ts, test/unused-exports.test.ts and test/ledger-git.test.ts here, and they pass. bun x tsc --noEmit is clean. The full check has not re-run yet.
job-slicestest: the hourlyledger-gitjob (the commit of the cost ledger chain to the ledger branch) runs underduringbut wasn't timed or listed. I added it toUNTIMEDwith a reason: the fixture has no ledger directory, andtest/ledger-git.test.tsruns the tick.unused-exportstest:resetBatchClockis exported only sotest/ledger-git.test.tscan reset the batch clock. I added it toscripts/unused-exports-allow.json.
The other 11 failures are in test files the forge found already red on main at ad44386 (spawn-in-process-visible, mcp-table, first-screen-line, mirror-worker, data-browser and boundary-tenancy-cell). I left them alone. It did not name hot_files or home_cache, but unused-exports printed "no longer dead" for rememberPullFiles and cachedVitals. I did not touch those either.
Approved. The batch makes one signed commit, and nothing is committed when there are no new lines. The verifier checks the signatures and the chain from a clone with no database. A rewritten line is reported at its file:line. The doc is in docs/features/cost-ledger.md and README is untouched. This change has no page and no terminal screen, so the preview shows nothing for it. I read the diff and did not run the tests. Two things to do next: (1) run git verify-commit against a real ssh-keygen allowed-signers file, because the SSHSIG format is only checked by this pull's own verifier; (2) have the verifier also check that each commit only appends to the month files, and persist the hourly clock so a restart cannot batch early. Smaller: monthFiles is a bare alias export of months, and the tests use dynamic imports where top-level ones would do.
The cost ledger's chain now goes to a
ledgerbranch in signed batches, andbun scripts/ledger-verify.ts --git <repo>checks it from a clone with no database. The new tests,test/cost-ledger.test.tsandtest/timers-named.test.tspass, andbun scripts/gate.tsandtscare clean.test/ssh-port-taken.test.tsfails here, and it failed the same way with my changes stashed. I did not run the full suite orscripts/fast.ts.What changed
ledger-gitfires every hour. Only when a month file has lines the branch lacks, it appends them as<yyyy-mm>.jsonlat the branch root in one commit. No new lines means no commit.FORGE_LEDGER_REPO(owner/name), elseFORGE_REPO. With neither set it does nothing, so production will not commit until one is configured. That setting is the one thing you need to do.<data>/ssh/host_ed25519) as anSSHSIGin thegpgsigheader. The signing code is new, insrc/ssh/sshsig.ts, because the sandbox has nossh-keygen. I could not rungit verify-commithere, so the format is checked only by our own verifier.commitFiles, which gained an optionalsignhook. Entry text only reaches git as blob content, never as an argument, a path or a message. I added no dependencies.--gitchecks every commit's signature and the whole hash chain. A bad entry is reported asbroken at <file>:<line>, and a bad signature asbroken at <commit>.--key "<ssh-ed25519 line>"pins the signer. Without it the commits must agree on one key, and the key is printed.docs/features/cost-ledger.mdnow says where the chain lives and how to clone and verify it.Tests (
test/ledger-git.test.ts):Left out
pushevent on the bus.Closes #12113