OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 commits / 5d2eea8

0.1.253: refuse to boot on an unpatched autobase, instead of finding out later
This daemon depends on two local autobase patches. `scripts/patch-autobase-warmup.cjs` re-applies both on every npm install, so a REINSTALL cannot revert them — verified against a pristine upstream copy. An autobase BUMP can: the anchors move, the script prints "anchor not found … SKIPPING", and the install SUCCEEDS with both guards gone. What they prevent is not subtle — a SIGABRT crash-loop on boot (pear-git #3) and a daemon wedged in a microtask chain that accepts nothing and reaps nothing (stortron: 800,000 _advance calls, no banner). A console line at install time defends against neither: nobody reads install output, and the symptom arrives days later wearing a different face. `overrides: {"autobase":"7.28.1"}` is the only thing holding it shut today; this makes the pin's failure loud. ⛔ FAIL CLOSED ON A POSITIVE ANSWER ONLY. A readable file missing its marker is a fact and we refuse. A file we cannot read is not knowledge — the layout could have changed, the check itself could be wrong — and refusing to boot a production forge over our own uncertainty is worse than what we are guarding against. Unknown proceeds, loudly. Same posture as chooseBundle's addon guard. ⚠ `PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE=1` overrides and SAYS so. An override that must be typed is a decision; a silent one is indistinguishable from a check that does not run. ⚠ CAUGHT IN REVIEW: my first version used `await import('node:fs')`. This daemon runs under BOTH node and bare, and bare has no `node:` builtins — it would have loaded fine under node and failed at boot under the runtime the fleet ships. Every other import in server.js is `#node:…` for exactly that reason. It now reuses the symbols already imported at the top. 5/5, 11/11. Boots under bare with the guard in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5d2eea887846805a48673c4b3bd7e0bb6a4aff56
by plankatron <fleeky@gmail.com> · 2026-09-15T19:53:20.000Z

Changed files (4) +179 -2

lib/autobase-guards.js +86 -0
……@@ -0,0 +1,86 @@
1// Are the two autobase patches this daemon depends on actually present?
2//
3// ⛔ WHY A BOOT CHECK AND NOT TRUST. `scripts/patch-autobase-warmup.cjs` re-applies both patches on
4// every `npm install`, so a reinstall cannot revert them — proven 2026-09-15 against a pristine
5// upstream copy. What it does NOT survive is an autobase BUMP: the anchors move, the script prints
6//
7// anchor not found (autobase version changed?) — SKIPPING; re-verify manually
8//
9// and the install SUCCEEDS. Both guards vanish, and what they prevent is a SIGABRT crash-loop on boot
10// (pear-git #3) and a daemon wedged in a microtask chain that accepts nothing and reaps nothing
11// (measured on stortron: 800,000 `_advance` calls, no banner). A console line at install time is not
12// a defence against either — nobody reads install output, and the symptom arrives days later wearing
13// a different face.
14//
15// `overrides: { "autobase": "7.28.1" }` pins the version and is the only thing holding this shut
16// today. This makes the pin's failure LOUD instead of silent.
17//
18// ⛔ FAIL CLOSED ON A POSITIVE ANSWER ONLY. A file we can read whose marker is missing is a fact, and
19// we refuse. A file we cannot read at all is NOT knowledge — the layout could have changed, this
20// could be a bundled tree, the check itself could be wrong — and refusing to boot a production forge
21// over our own uncertainty is worse than the thing we are guarding against. Unknown proceeds, loudly.
22// Same posture as chooseBundle's addon guard, for the same reason.
23//
24// PURE + INJECTED: `read` is a parameter, so every branch is testable with no node_modules.
25
26export const GUARDS = [
27 {
28 file: 'node_modules/autobase/lib/apply-state.js',
29 marker: 'GUARD (pgv3 patch)',
30 protects: 'null view.core during _drain warmup — without it the daemon SIGABRT-crash-loops on boot (pear-git #3)'
31 },
32 {
33 file: 'node_modules/autobase/index.js',
34 marker: 'BOUND (pgv3 patch)',
35 protects: 'the _appendBatch flush loop — without it a mirror whose writer the origin has not enrolled spins forever and the daemon never serves (stortron, 800k _advance calls)'
36 }
37]
38
39/**
40 * @param {object} o
41 * read (relativePath) => string | null — source text, or null when it cannot be read
42 * @returns {{ ok: boolean, missing: object[], unreadable: object[] }}
43 */
44export function checkAutobaseGuards ({ read } = {}) {
45 if (typeof read !== 'function') throw new Error('checkAutobaseGuards requires read(path)')
46 const missing = []
47 const unreadable = []
48 for (const g of GUARDS) {
49 let src = null
50 try { src = read(g.file) } catch { src = null }
51 if (src == null) { unreadable.push(g); continue }
52 if (!src.includes(g.marker)) missing.push(g)
53 }
54 return { ok: missing.length === 0, missing, unreadable }
55}
56
57/**
58 * Boot gate. Throws when a guard is positively absent; warns when it cannot tell.
59 *
60 * ⚠ `PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE=1` overrides, for the operator who has upstreamed the patches
61 * or is deliberately testing without them. An override that must be typed is a decision; a check that
62 * can be skipped by accident is not a check.
63 */
64export function assertAutobaseGuards ({ read, env = {}, log = console.error } = {}) {
65 const { ok, missing, unreadable } = checkAutobaseGuards({ read })
66
67 for (const g of unreadable) {
68 log('[autobase-guards] cannot read ' + g.file + ' — NOT verifying: ' + g.protects)
69 }
70 if (ok) return { ok: true, missing, unreadable }
71
72 const detail = missing.map((g) => ' ' + g.file + ' is missing "' + g.marker + '"\n guards: ' + g.protects).join('\n')
73 const msg = 'autobase is not patched for this daemon.\n' + detail +
74 '\n\nRun `npm install` (postinstall re-applies both), or if autobase was bumped the anchors have' +
75 '\nmoved and scripts/patch-autobase-warmup.cjs needs re-pointing — it warns and succeeds in that' +
76 '\ncase, which is why this check exists.' +
77 '\n\nTo boot anyway: PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE=1'
78
79 if (env.PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE === '1') {
80 log('[autobase-guards] OVERRIDDEN — booting unpatched.\n' + msg)
81 return { ok: false, overridden: true, missing, unreadable }
82 }
83 throw new Error('[autobase-guards] ' + msg)
84}
85
86export default { GUARDS, checkAutobaseGuards, assertAutobaseGuards }
package.json +2 -2
……@@ -1,7 +1,7 @@
11{
22 "name": "pear-git-v3",
3 "version": "0.1.252",
4 "description": "P2P git forge — multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
3 "version": "0.1.253",
4 "description": "P2P git forge \u2014 multi-writer issue/org/release/peer-mirror daemon. Built bottom-up after the failed v2.",
55 "type": "module",
66 "main": "index.js",
77 "scripts": {
server.js +17 -0
……@@ -184,6 +184,23 @@ let shuttingDown = false
184184let otaRuntime = null
185185
186186async function main () {
187 // ⛔ BEFORE startPearGit, because that is where the unpatched failures happen: a SIGABRT
188 // crash-loop on boot, or a wedge in a microtask chain that never hands the event loop back.
189 // Refusing here is a sentence an operator can act on; both alternatives are a daemon that looks
190 // broken for reasons nothing names. See lib/autobase-guards.js.
191 {
192 // ⚠ `#node:fs`, NOT `node:fs`. This daemon runs under BOTH node and bare (`start` / `start:bare`),
193 // and the bare build has no `node:` builtins — the import map at the top of package.json is the
194 // whole reason every other import in this file is `#node:…`. A plain `node:fs` here would load
195 // fine under node and fail at boot under bare, which is the runtime the fleet actually ships.
196 const { assertAutobaseGuards } = await import('./lib/autobase-guards.js')
197 const root = dirname(fileURLToPath(import.meta.url))
198 assertAutobaseGuards({
199 read: (rel) => { try { return readFileSync(join(root, rel), 'utf8') } catch { return null } },
200 env: process.env
201 })
202 }
203
187204 node = await startPearGit(opts)
188205
189206 console.log('pear-git-v3 daemon')
test/autobase-guards.test.js +74 -0
……@@ -0,0 +1,74 @@
1// The two autobase patches this daemon cannot boot safely without.
2//
3// ⛔ THE HOLE THIS CLOSES. `scripts/patch-autobase-warmup.cjs` re-applies both on every npm install —
4// proven against a pristine upstream copy — so a REINSTALL cannot revert them. An autobase BUMP can:
5// the anchors move, the script prints "anchor not found … SKIPPING" and the install SUCCEEDS. Both
6// guards vanish silently, and what they prevent is a SIGABRT crash-loop on boot and a daemon wedged
7// in a microtask chain that accepts nothing.
8//
9// ⚠ A console line at install time is not a defence. Nobody reads install output, and the symptom
10// arrives days later wearing a different face — which is how both of these were found the first time.
11
12import test from 'brittle'
13import { readFileSync } from '#node:fs'
14import { join } from '#node:path'
15import { checkAutobaseGuards, assertAutobaseGuards, GUARDS } from '../lib/autobase-guards.js'
16
17const ROOT = new URL('..', import.meta.url).pathname
18
19const patched = () => ({
20 'node_modules/autobase/lib/apply-state.js': 'x\n// GUARD (pgv3 patch): …\ny',
21 'node_modules/autobase/index.js': 'a\n// BOUND (pgv3 patch): …\nb'
22})
23const reader = (files) => (p) => (p in files ? files[p] : null)
24
25test('both guards present — boots', (t) => {
26 const r = checkAutobaseGuards({ read: reader(patched()) })
27 t.ok(r.ok)
28 t.is(r.missing.length, 0)
29 t.is(r.unreadable.length, 0)
30})
31
32test('★★★ a guard positively ABSENT refuses the boot, and names what it protected', (t) => {
33 const files = patched()
34 files['node_modules/autobase/index.js'] = 'a\n// upstream, unpatched\nb'
35 t.exception(
36 () => assertAutobaseGuards({ read: reader(files), env: {}, log: () => {} }),
37 /_appendBatch flush loop/,
38 'the error says what breaks, not merely that a string is missing'
39 )
40})
41
42test('⛔ a file we cannot READ does not refuse — unknown is not a fact', (t) => {
43 const said = []
44 // The layout could have changed, this could be a bundled tree, the check itself could be wrong.
45 // Refusing to boot a production forge over our own uncertainty is worse than what we guard against.
46 const r = assertAutobaseGuards({ read: () => null, env: {}, log: (m) => said.push(m) })
47 t.ok(r.ok, 'unreadable is not missing')
48 t.is(said.length, GUARDS.length, 'but every one it could not verify is said out loud')
49 t.ok(said.every((m) => /NOT verifying/.test(m)))
50})
51
52test('⚠ the override boots, and says so', (t) => {
53 const files = patched()
54 delete files['node_modules/autobase/index.js']
55 files['node_modules/autobase/index.js'] = 'unpatched'
56 const said = []
57 const r = assertAutobaseGuards({
58 read: reader(files),
59 env: { PEAR_GIT_ALLOW_UNPATCHED_AUTOBASE: '1' },
60 log: (m) => said.push(m)
61 })
62 t.absent(r.ok)
63 t.ok(r.overridden)
64 t.ok(said.some((m) => /OVERRIDDEN/.test(m)), 'an override that is silent is indistinguishable from a check that does not run')
65})
66
67test('★ the real tree passes — this is the check that would have caught a bump', async (t) => {
68 const r = checkAutobaseGuards({
69 read: (p) => { try { return readFileSync(join(ROOT, p), 'utf8') } catch { return null } }
70 })
71 // ⚠ If autobase is ever unreadable here this goes to `unreadable` and passes — deliberately. The
72 // assertion is about THIS tree being patched, not about the test environment having node_modules.
73 t.is(r.missing.length, 0, 'no guard is positively missing in the installed autobase')
74})