@junghanacs/entwurf 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-herdr-runtime-bootstrap — deterministic gate for the #116 M3-b1 runtime leaf
|
|
3
|
+
* (`plugins/herdr/lib/runtime-bootstrap.mjs`): acquiring, verifying and replacing the
|
|
4
|
+
* Entwurf-owned runtime the Herdr plugin installs against.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS GATE PROVES AND WHAT IT DOES NOT. `acquire` is a FUNCTION seam, so the transaction here
|
|
7
|
+
* is driven against a fixture package this gate materialises: a real `package.json`, a real
|
|
8
|
+
* compiled entry, and real executable bins. That makes the transaction hermetic — no network, no
|
|
9
|
+
* npm — but it means the `check-bridge` cell proves only that an installed tree's own bin is
|
|
10
|
+
* executed and its exit code decides the verdict. It does NOT prove that the real Entwurf package
|
|
11
|
+
* boots. THE ACTUAL PACKAGE PROOF LIVES IN `check-pack-install`, which packs this checkout, installs
|
|
12
|
+
* the tarball into a fresh temp project, and runs this module's own `verifyInstalledRuntime` against
|
|
13
|
+
* that installed tree. Neither gate stands in for the other, and this header is where the boundary
|
|
14
|
+
* is named rather than implied.
|
|
15
|
+
*
|
|
16
|
+
* WHY THE SANDBOX IS THE WHOLE POINT. Every root this module may touch comes from XDG, so the gate
|
|
17
|
+
* hands it a mkdtemp HOME with XDG_DATA_HOME/XDG_CACHE_HOME inside it and then asserts that nothing
|
|
18
|
+
* appeared anywhere else under that HOME. A module that owns a user's data root has to prove the
|
|
19
|
+
* boundary, not describe it.
|
|
20
|
+
*
|
|
21
|
+
* ORDER IS LOAD-BEARING. Each [QK:HRB-*] token appears exactly once, on the assertion that fails
|
|
22
|
+
* for that claim, and the cells are ordered so that a mutant of one claim cannot trip an earlier
|
|
23
|
+
* claim's assertion first.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { pathToFileURL } from "node:url";
|
|
32
|
+
import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
|
|
33
|
+
|
|
34
|
+
let passed = 0;
|
|
35
|
+
function ok(label: string, cond: boolean): void {
|
|
36
|
+
assert.ok(cond, label);
|
|
37
|
+
console.log(` ok ${label}`);
|
|
38
|
+
passed++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REPO = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
42
|
+
const PLUGIN_DIR = path.join(REPO, "plugins", "herdr");
|
|
43
|
+
/** The ONE owner — under `scripts/`, which the npm tarball ships. */
|
|
44
|
+
const LEAF = path.join(REPO, "scripts", "herdr-runtime.mjs");
|
|
45
|
+
/** The plugin-side file, which must be a thin re-export of exactly that. */
|
|
46
|
+
const PLUGIN_REEXPORT = path.join(PLUGIN_DIR, "lib", "runtime-bootstrap.mjs");
|
|
47
|
+
|
|
48
|
+
interface Spec {
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
integrity?: string;
|
|
52
|
+
}
|
|
53
|
+
interface ArtifactIdentity {
|
|
54
|
+
kind: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
version?: string;
|
|
57
|
+
expectedIntegrity?: string;
|
|
58
|
+
repository?: string;
|
|
59
|
+
commit?: string;
|
|
60
|
+
packageName?: string;
|
|
61
|
+
packageVersion?: string;
|
|
62
|
+
observedDigest?: string;
|
|
63
|
+
}
|
|
64
|
+
interface Lock {
|
|
65
|
+
source: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
version?: string;
|
|
68
|
+
integrity?: string;
|
|
69
|
+
repository?: string;
|
|
70
|
+
}
|
|
71
|
+
interface Journal {
|
|
72
|
+
schemaVersion: number;
|
|
73
|
+
phase: string;
|
|
74
|
+
runtimeRoot: string;
|
|
75
|
+
artifactIdentity: ArtifactIdentity;
|
|
76
|
+
previousRuntime: ArtifactIdentity | null;
|
|
77
|
+
}
|
|
78
|
+
interface BootstrapResult {
|
|
79
|
+
phase: string;
|
|
80
|
+
changed: boolean;
|
|
81
|
+
recovered: string | null;
|
|
82
|
+
journal: Journal;
|
|
83
|
+
}
|
|
84
|
+
interface Layout {
|
|
85
|
+
pluginRoot: string;
|
|
86
|
+
runtimeRoot: string;
|
|
87
|
+
activeDir: string;
|
|
88
|
+
stagingDir: string;
|
|
89
|
+
previousDir: string;
|
|
90
|
+
journalPath: string;
|
|
91
|
+
cacheDir: string;
|
|
92
|
+
}
|
|
93
|
+
interface AcquireArgs {
|
|
94
|
+
identity: ArtifactIdentity;
|
|
95
|
+
prefix: string;
|
|
96
|
+
cacheDir: string;
|
|
97
|
+
env: NodeJS.ProcessEnv;
|
|
98
|
+
}
|
|
99
|
+
interface AcquireResult {
|
|
100
|
+
observedDigest: string;
|
|
101
|
+
packageName?: string;
|
|
102
|
+
packageVersion?: string;
|
|
103
|
+
}
|
|
104
|
+
interface SpawnLike {
|
|
105
|
+
status: number | null;
|
|
106
|
+
stdout: string;
|
|
107
|
+
stderr: string;
|
|
108
|
+
error?: Error;
|
|
109
|
+
}
|
|
110
|
+
interface Assessed {
|
|
111
|
+
state: string;
|
|
112
|
+
refused?: string;
|
|
113
|
+
kinds: Record<string, string>;
|
|
114
|
+
active: boolean;
|
|
115
|
+
staging: boolean;
|
|
116
|
+
previous: boolean;
|
|
117
|
+
}
|
|
118
|
+
interface Leaf {
|
|
119
|
+
REQUIRED_BINS: string[];
|
|
120
|
+
COMPILED_ENTRY: string;
|
|
121
|
+
JOURNAL_PHASES: string[];
|
|
122
|
+
JOURNAL_KEYS: string[];
|
|
123
|
+
RUNTIME_SCHEMA_VERSION: number;
|
|
124
|
+
ARTIFACT_KINDS: string[];
|
|
125
|
+
ARTIFACT_KEYS: Record<string, { requested: string[]; ready: string[] }>;
|
|
126
|
+
CHECKOUT_REPOSITORY: string;
|
|
127
|
+
resolveRuntimeLayout: (env: NodeJS.ProcessEnv) => Layout;
|
|
128
|
+
readCheckoutPackageSpec: (root: string) => Spec;
|
|
129
|
+
readRuntimeLock: (pluginDir: string) => Lock;
|
|
130
|
+
certifyLockCoherence: (lock: Spec, spec: Spec) => Spec;
|
|
131
|
+
certifyArtifactIdentity: (where: string, value: unknown, stage?: string) => ArtifactIdentity;
|
|
132
|
+
artifactStage: (identity: ArtifactIdentity) => string;
|
|
133
|
+
artifactCompleteness: (identity: ArtifactIdentity) => Spec;
|
|
134
|
+
sameArtifactRequest: (ready: ArtifactIdentity, requested: ArtifactIdentity) => boolean;
|
|
135
|
+
requestedArtifactIdentity: (args: {
|
|
136
|
+
lock: Lock;
|
|
137
|
+
checkoutRoot: string;
|
|
138
|
+
resolveCommit?: (root: string) => string;
|
|
139
|
+
}) => ArtifactIdentity;
|
|
140
|
+
resolveCheckoutCommit: (
|
|
141
|
+
root: string,
|
|
142
|
+
deps?: { gitBin?: string; spawn?: (bin: string, argv: string[], opts: unknown) => SpawnLike },
|
|
143
|
+
) => string;
|
|
144
|
+
buildCheckoutRemote: (repository: string) => string;
|
|
145
|
+
buildCheckoutPackArgv: (identity: ArtifactIdentity, dest: string) => string[];
|
|
146
|
+
checkoutAcquire: (args: AcquireArgs & { npmBin?: string }) => AcquireResult;
|
|
147
|
+
assessRuntimeState: (layout: Layout, certified: Journal | null) => Assessed;
|
|
148
|
+
readCertifiedJournal: (layout: Layout) => Journal | null;
|
|
149
|
+
buildPackArgv: (spec: Spec, dest: string) => string[];
|
|
150
|
+
buildInstallArgv: (tarball: string, prefix: string) => string[];
|
|
151
|
+
npmEnvironment: (env: NodeJS.ProcessEnv, cacheDir: string) => NodeJS.ProcessEnv;
|
|
152
|
+
integrityOfFile: (file: string) => string;
|
|
153
|
+
classifyPath: (target: string) => string;
|
|
154
|
+
inspectInstalledRuntime: (prefix: string, packageName: string) => { ok: boolean; version?: string; code?: string };
|
|
155
|
+
writeJournal: (layout: Layout, entry: Record<string, unknown>) => void;
|
|
156
|
+
bootstrapRuntime: (args: {
|
|
157
|
+
env: NodeJS.ProcessEnv;
|
|
158
|
+
lock: Lock;
|
|
159
|
+
checkoutRoot?: string;
|
|
160
|
+
acquire?: unknown;
|
|
161
|
+
resolveCommit?: (root: string) => string;
|
|
162
|
+
}) => BootstrapResult;
|
|
163
|
+
removeOwnedRuntime: (args: { env: NodeJS.ProcessEnv }) => { removed: string[]; reason: string | null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const leaf = (await import(pathToFileURL(LEAF).href)) as Leaf;
|
|
167
|
+
const {
|
|
168
|
+
REQUIRED_BINS,
|
|
169
|
+
COMPILED_ENTRY,
|
|
170
|
+
JOURNAL_PHASES,
|
|
171
|
+
JOURNAL_KEYS,
|
|
172
|
+
ARTIFACT_KINDS,
|
|
173
|
+
ARTIFACT_KEYS,
|
|
174
|
+
CHECKOUT_REPOSITORY,
|
|
175
|
+
resolveRuntimeLayout,
|
|
176
|
+
readCheckoutPackageSpec,
|
|
177
|
+
assessRuntimeState,
|
|
178
|
+
readCertifiedJournal,
|
|
179
|
+
buildPackArgv,
|
|
180
|
+
buildInstallArgv,
|
|
181
|
+
buildCheckoutRemote,
|
|
182
|
+
buildCheckoutPackArgv,
|
|
183
|
+
checkoutAcquire,
|
|
184
|
+
certifyArtifactIdentity,
|
|
185
|
+
artifactStage,
|
|
186
|
+
artifactCompleteness,
|
|
187
|
+
sameArtifactRequest,
|
|
188
|
+
requestedArtifactIdentity,
|
|
189
|
+
resolveCheckoutCommit,
|
|
190
|
+
npmEnvironment,
|
|
191
|
+
readRuntimeLock,
|
|
192
|
+
classifyPath,
|
|
193
|
+
inspectInstalledRuntime,
|
|
194
|
+
writeJournal,
|
|
195
|
+
bootstrapRuntime,
|
|
196
|
+
removeOwnedRuntime,
|
|
197
|
+
} = leaf;
|
|
198
|
+
|
|
199
|
+
function world(tag: string): NodeJS.ProcessEnv {
|
|
200
|
+
const home = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), `entwurf-hrb-${tag}-`)));
|
|
201
|
+
return {
|
|
202
|
+
HOME: home,
|
|
203
|
+
XDG_DATA_HOME: path.join(home, "data"),
|
|
204
|
+
XDG_CACHE_HOME: path.join(home, "cache"),
|
|
205
|
+
PATH: process.env.PATH,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const PACKAGE = "@junghanacs/entwurf";
|
|
210
|
+
const LOCK_A: Lock = { source: "npm", name: PACKAGE, version: "0.21.0", integrity: "sha512-fixtureA" };
|
|
211
|
+
const LOCK_B: Lock = { source: "npm", name: PACKAGE, version: "0.22.0", integrity: "sha512-fixtureB" };
|
|
212
|
+
/** The committed candidate lock's shape: a source and the repo literal, and nothing else. */
|
|
213
|
+
const CHECKOUT_LOCK: Lock = { source: "herdr-checkout", repository: "junghan0611/entwurf" };
|
|
214
|
+
const COMMIT_A = "1".repeat(40);
|
|
215
|
+
const COMMIT_B = "2".repeat(40);
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A fixture CHECKOUT — the thing an npm lock must be coherent with, and the thing a checkout-sourced
|
|
219
|
+
* identity reads its commit from. Written per lock so the two sources can be driven side by side.
|
|
220
|
+
*/
|
|
221
|
+
function fixtureCheckout(lock: Lock): string {
|
|
222
|
+
const root = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hrb-checkout-")));
|
|
223
|
+
fs.writeFileSync(
|
|
224
|
+
path.join(root, "package.json"),
|
|
225
|
+
`${JSON.stringify({ name: lock.name ?? PACKAGE, version: lock.version ?? "0.21.0" }, null, 2)}\n`,
|
|
226
|
+
);
|
|
227
|
+
return root;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Materialise a fixture package into `prefix` the way npm would, and return an observed digest. */
|
|
231
|
+
function fixtureAcquire(
|
|
232
|
+
opts: {
|
|
233
|
+
version?: string;
|
|
234
|
+
name?: string;
|
|
235
|
+
dist?: boolean;
|
|
236
|
+
exitCode?: number;
|
|
237
|
+
omitBin?: string;
|
|
238
|
+
unexecutableBin?: string;
|
|
239
|
+
} = {},
|
|
240
|
+
) {
|
|
241
|
+
return ({ identity, prefix }: AcquireArgs): AcquireResult => {
|
|
242
|
+
const version = opts.version ?? identity.version ?? "0.21.0";
|
|
243
|
+
const name = opts.name ?? identity.name ?? PACKAGE;
|
|
244
|
+
const root = path.join(prefix, "node_modules", name);
|
|
245
|
+
fs.mkdirSync(root, { recursive: true });
|
|
246
|
+
fs.writeFileSync(path.join(root, "package.json"), `${JSON.stringify({ name, version }, null, 2)}\n`);
|
|
247
|
+
if (opts.dist !== false) {
|
|
248
|
+
fs.mkdirSync(path.join(root, path.dirname(COMPILED_ENTRY)), { recursive: true });
|
|
249
|
+
fs.writeFileSync(path.join(root, COMPILED_ENTRY), "// compiled entry fixture\n");
|
|
250
|
+
}
|
|
251
|
+
const binDir = path.join(prefix, "node_modules", ".bin");
|
|
252
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
253
|
+
for (const name of REQUIRED_BINS) {
|
|
254
|
+
if (name === opts.omitBin) continue;
|
|
255
|
+
const p = path.join(binDir, name);
|
|
256
|
+
fs.writeFileSync(p, `#!/bin/sh\nexit ${name === "entwurf" ? (opts.exitCode ?? 0) : 0}\n`);
|
|
257
|
+
fs.chmodSync(p, name === opts.unexecutableBin ? 0o644 : 0o755);
|
|
258
|
+
}
|
|
259
|
+
// A well-formed digest: the certified reader checks the SHAPE of an identity, not its truth.
|
|
260
|
+
// The completeness pair rides along because a checkout-sourced acquisition is the thing that
|
|
261
|
+
// OBSERVES what it packed — for npm the request already said it.
|
|
262
|
+
return {
|
|
263
|
+
observedDigest: `sha256-${createHash("sha256").update(`${name}@${version}`).digest("hex")}`,
|
|
264
|
+
packageName: name,
|
|
265
|
+
packageVersion: version,
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Every file under a root, relative and sorted — the byte-identity comparison this gate needs. */
|
|
271
|
+
function treeDigest(root: string): string {
|
|
272
|
+
if (!fs.existsSync(root)) return "<absent>";
|
|
273
|
+
const rows: string[] = [];
|
|
274
|
+
const walk = (dir: string): void => {
|
|
275
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
276
|
+
const abs = path.join(dir, entry.name);
|
|
277
|
+
if (entry.isDirectory()) walk(abs);
|
|
278
|
+
else rows.push(`${path.relative(root, abs)}:${fs.readFileSync(abs).length}`);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
walk(root);
|
|
282
|
+
return rows.join("|");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function refusal(fn: () => unknown): string | null {
|
|
286
|
+
try {
|
|
287
|
+
fn();
|
|
288
|
+
return null;
|
|
289
|
+
} catch (err) {
|
|
290
|
+
return (err as { code?: string }).code ?? `not-a-RuntimeBootstrapError:${String(err)}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* One bootstrap against a fixture checkout. The npm locks get a checkout whose `package.json`
|
|
296
|
+
* agrees with them (that coherence is a contract of its own); a checkout lock gets a fixed commit
|
|
297
|
+
* seam, because this gate proves the transaction, and `git` itself is proven in its own cell.
|
|
298
|
+
*/
|
|
299
|
+
function install(
|
|
300
|
+
env: NodeJS.ProcessEnv,
|
|
301
|
+
lock: Lock = LOCK_A,
|
|
302
|
+
acquire = fixtureAcquire(),
|
|
303
|
+
commit: string = COMMIT_A,
|
|
304
|
+
): BootstrapResult {
|
|
305
|
+
return bootstrapRuntime({ env, lock, checkoutRoot: fixtureCheckout(lock), acquire, resolveCommit: () => commit });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── 1. the shipped owner is the only implementation ────────────────────────────
|
|
309
|
+
{
|
|
310
|
+
const shipped = (JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as { files: string[] }).files;
|
|
311
|
+
const reexport = fs.readFileSync(PLUGIN_REEXPORT, "utf8");
|
|
312
|
+
const code = reexport
|
|
313
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
314
|
+
.split("\n")
|
|
315
|
+
.map((l) => l.trim())
|
|
316
|
+
.filter((l) => l.length > 0);
|
|
317
|
+
const owner = (await import(pathToFileURL(LEAF).href)) as Record<string, unknown>;
|
|
318
|
+
const thin = (await import(pathToFileURL(PLUGIN_REEXPORT).href)) as Record<string, unknown>;
|
|
319
|
+
const surfaces = JSON.stringify(Object.keys(owner).sort()) === JSON.stringify(Object.keys(thin).sort());
|
|
320
|
+
ok(
|
|
321
|
+
"[QK:HRB-ONE-SHIPPED-OWNER] the implementation lives under `scripts/` — which `package.json.files` ships — and " +
|
|
322
|
+
"the plugin-side file is a re-export and NOTHING else: Herdr's `plugin uninstall` deletes the checkout this " +
|
|
323
|
+
"plugin sits in and calls no cleanup hook, so the code that retires the runtime and undoes an activation has " +
|
|
324
|
+
"to outlive that deletion, and a second copy would let an uninstall read back a schema a different " +
|
|
325
|
+
`implementation wrote (scripts-shipped=${shipped.includes("scripts/")} plugins-shipped=${shipped.some((f) => f.startsWith("plugins"))} reexport-lines=${code.length} surfaces-identical=${surfaces})`,
|
|
326
|
+
shipped.includes("scripts/") &&
|
|
327
|
+
!shipped.some((f) => f.startsWith("plugins")) &&
|
|
328
|
+
code.length === 1 &&
|
|
329
|
+
code[0] === 'export * from "../../../scripts/herdr-runtime.mjs";' &&
|
|
330
|
+
surfaces &&
|
|
331
|
+
Object.keys(owner).length > 0,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── 2. the address the wiring will name is a real directory and it does not move ─
|
|
336
|
+
{
|
|
337
|
+
const env = world("address");
|
|
338
|
+
const layout = resolveRuntimeLayout(env);
|
|
339
|
+
// The installs are CAUGHT. An active address that stopped being a real directory does not fail
|
|
340
|
+
// here as a wrong lstat — the NEXT install refuses the path kind first, and an uncaught refusal
|
|
341
|
+
// would end this cell before the claim it violates is judged, red with nobody's name on it.
|
|
342
|
+
const refusalOf = (run: () => void): string | null => {
|
|
343
|
+
try {
|
|
344
|
+
run();
|
|
345
|
+
return null;
|
|
346
|
+
} catch (err) {
|
|
347
|
+
return `${(err as { code?: string }).code ?? "unknown"}: ${(err as Error).message}`;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const firstRefusal = refusalOf(() => install(env));
|
|
351
|
+
const first = fs.lstatSync(layout.activeDir, { throwIfNoEntry: false }) ?? null;
|
|
352
|
+
const secondRefusal = refusalOf(() => install(env, LOCK_B));
|
|
353
|
+
const second = fs.lstatSync(layout.activeDir, { throwIfNoEntry: false }) ?? null;
|
|
354
|
+
ok(
|
|
355
|
+
"[QK:HRB-STABLE-ADDRESS] the runtime address is a REAL DIRECTORY at $XDG_DATA_HOME/entwurf/herdr-plugin/" +
|
|
356
|
+
"runtime/active and it is the same path after a version change — the scoped wiring one slice above records " +
|
|
357
|
+
"ABSOLUTE commands under this root, and Pi records the owner root it was wired with, so a symlink whose " +
|
|
358
|
+
"canonicalisation resolves to a per-version directory makes every upgrade look like a different owner " +
|
|
359
|
+
`taking that root over (active=${path.relative(env.HOME as string, layout.activeDir)} ` +
|
|
360
|
+
`symlink=${first?.isSymbolicLink()}/${second?.isSymbolicLink()} ` +
|
|
361
|
+
`refusals=${JSON.stringify([firstRefusal, secondRefusal])})`,
|
|
362
|
+
layout.activeDir === path.join(env.XDG_DATA_HOME as string, "entwurf", "herdr-plugin", "runtime", "active") &&
|
|
363
|
+
firstRefusal === null &&
|
|
364
|
+
secondRefusal === null &&
|
|
365
|
+
first !== null &&
|
|
366
|
+
second !== null &&
|
|
367
|
+
first.isDirectory() &&
|
|
368
|
+
!first.isSymbolicLink() &&
|
|
369
|
+
second.isDirectory() &&
|
|
370
|
+
!second.isSymbolicLink(),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── 3. nothing is created before the judgement ─────────────────────────────────
|
|
375
|
+
{
|
|
376
|
+
const env = world("premutation");
|
|
377
|
+
const layout = resolveRuntimeLayout(env);
|
|
378
|
+
// A host whose journal cannot be certified: the refusal must happen with the runtime root still
|
|
379
|
+
// absent, because a module that mkdirs its own root first has answered "is this ours?" with its
|
|
380
|
+
// own footprint.
|
|
381
|
+
fs.mkdirSync(layout.pluginRoot, { recursive: true });
|
|
382
|
+
fs.writeFileSync(layout.journalPath, "{ this is not json\n");
|
|
383
|
+
const code = refusal(() => install(env));
|
|
384
|
+
ok(
|
|
385
|
+
"[QK:HRB-PRE-MUTATION-ORDER] ownership is decided BEFORE the first mkdir — the runtime root is still absent " +
|
|
386
|
+
"after a refusal, because a module that creates its own root and then asks whether the root was its own has " +
|
|
387
|
+
"already made the answer yes; every later ownership question on that host would then be answered by this " +
|
|
388
|
+
`module's own footprint (refusal=${code} runtimeRoot-after=${classifyPath(layout.runtimeRoot)})`,
|
|
389
|
+
code === "runtime-journal-uncertified" && classifyPath(layout.runtimeRoot) === "absent",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── 4. disk facts come from lstat, and a link is never a directory ─────────────
|
|
394
|
+
{
|
|
395
|
+
const dangling = world("dangling");
|
|
396
|
+
const danglingLayout = resolveRuntimeLayout(dangling);
|
|
397
|
+
fs.mkdirSync(danglingLayout.runtimeRoot, { recursive: true });
|
|
398
|
+
fs.symlinkSync(path.join(dangling.HOME as string, "nothing-here"), danglingLayout.activeDir);
|
|
399
|
+
const existsSaysAbsent = !fs.existsSync(danglingLayout.activeDir);
|
|
400
|
+
const danglingCode = refusal(() => install(dangling));
|
|
401
|
+
|
|
402
|
+
const linkedRoot = world("linked-root");
|
|
403
|
+
const linkedLayout = resolveRuntimeLayout(linkedRoot);
|
|
404
|
+
fs.mkdirSync(path.dirname(linkedLayout.runtimeRoot), { recursive: true });
|
|
405
|
+
fs.symlinkSync(path.join(linkedRoot.HOME as string, "elsewhere"), linkedLayout.runtimeRoot);
|
|
406
|
+
const linkedCode = refusal(() => install(linkedRoot));
|
|
407
|
+
|
|
408
|
+
const linkedActive = world("linked-active");
|
|
409
|
+
const linkedActiveLayout = resolveRuntimeLayout(linkedActive);
|
|
410
|
+
fs.mkdirSync(path.join(linkedActive.HOME as string, "someone-elses-tree"), { recursive: true });
|
|
411
|
+
fs.mkdirSync(linkedActiveLayout.runtimeRoot, { recursive: true });
|
|
412
|
+
fs.symlinkSync(path.join(linkedActive.HOME as string, "someone-elses-tree"), linkedActiveLayout.activeDir);
|
|
413
|
+
const linkedActiveCode = refusal(() => install(linkedActive));
|
|
414
|
+
|
|
415
|
+
ok(
|
|
416
|
+
"[QK:HRB-PATH-KIND-LSTAT] every runtime path is classified with lstat, and only `absent` or `real-dir` is " +
|
|
417
|
+
"allowed to stand there — `existsSync` answers 'absent' for a dangling symlink and 'there' for a link into " +
|
|
418
|
+
"somebody else's tree, so a module that trusts it will rename away a stranger's directory while believing it " +
|
|
419
|
+
"found nothing, and will mkdir through a symlinked root and fail with a bare errno instead of a refusal " +
|
|
420
|
+
`(dangling=${danglingCode}/existsSync-said-absent=${existsSaysAbsent} linked-root=${linkedCode} linked-active=${linkedActiveCode})`,
|
|
421
|
+
existsSaysAbsent &&
|
|
422
|
+
danglingCode === "runtime-path-kind-refused" &&
|
|
423
|
+
linkedCode === "runtime-path-kind-refused" &&
|
|
424
|
+
linkedActiveCode === "runtime-path-kind-refused",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── 5. the exact package, or a named refusal ───────────────────────────────────
|
|
429
|
+
{
|
|
430
|
+
const wrongVersion = refusal(() => install(world("wrong-version"), LOCK_A, fixtureAcquire({ version: "0.0.1" })));
|
|
431
|
+
const noDist = refusal(() => install(world("no-dist"), LOCK_A, fixtureAcquire({ dist: false })));
|
|
432
|
+
const badBridge = refusal(() => install(world("bad-bridge"), LOCK_A, fixtureAcquire({ exitCode: 3 })));
|
|
433
|
+
ok(
|
|
434
|
+
"[QK:HRB-EXACT-ARTIFACT] the tree that becomes the runtime must BE the exact name@version asked for, carry the " +
|
|
435
|
+
"compiled entry, and answer `check-bridge` — each failure named separately. A runtime whose version is " +
|
|
436
|
+
"whatever the registry felt like serving is an unpinned supply chain, and a directory npm created is not " +
|
|
437
|
+
`yet a package that runs (${wrongVersion} / ${noDist} / ${badBridge})`,
|
|
438
|
+
wrongVersion === "runtime-package-spec-mismatch" &&
|
|
439
|
+
noDist === "runtime-compiled-dist-missing" &&
|
|
440
|
+
badBridge === "runtime-check-bridge-failed",
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── 6. an incomplete runtime is never `runtime-ready` ──────────────────────────
|
|
445
|
+
{
|
|
446
|
+
const missing = refusal(() =>
|
|
447
|
+
install(world("bin-missing"), LOCK_A, fixtureAcquire({ omitBin: "entwurf-statusline" })),
|
|
448
|
+
);
|
|
449
|
+
const unexecutable = refusal(() =>
|
|
450
|
+
install(world("bin-unexecutable"), LOCK_A, fixtureAcquire({ unexecutableBin: "entwurf-bridge" })),
|
|
451
|
+
);
|
|
452
|
+
ok(
|
|
453
|
+
"[QK:HRB-RUNTIME-COMPLETE] every required bin must be present AND executable before a runtime is called ready — " +
|
|
454
|
+
"the install runs with `--ignore-scripts`, so the chmod a package's own postinstall would have done never " +
|
|
455
|
+
"happened, and the scoped wiring above names these as absolute commands: one that is missing or not " +
|
|
456
|
+
`executable is a harness whose configured command does not run (missing=${missing} unexecutable=${unexecutable} required=${JSON.stringify(REQUIRED_BINS)})`,
|
|
457
|
+
missing === "runtime-required-bin-missing" && unexecutable === "runtime-required-bin-missing",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── 7. the lock's SHAPE is named before any field is read ──────────────────────
|
|
462
|
+
{
|
|
463
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hrb-lock-")));
|
|
464
|
+
const read = (body: string): string | null => {
|
|
465
|
+
fs.writeFileSync(path.join(dir, "runtime-lock.json"), body);
|
|
466
|
+
return refusal(() => readRuntimeLock(dir));
|
|
467
|
+
};
|
|
468
|
+
const cells: Record<string, string | null> = {
|
|
469
|
+
"json-null": read("null"),
|
|
470
|
+
array: read("[]"),
|
|
471
|
+
string: read('"0.21.0"'),
|
|
472
|
+
number: read("42"),
|
|
473
|
+
truncated: read("{ not json"),
|
|
474
|
+
};
|
|
475
|
+
const verdicts = Object.entries(cells).map(([k, v]) => `${k}=${v}`);
|
|
476
|
+
ok(
|
|
477
|
+
"[QK:HRB-LOCK-SHAPE-NAMED] the lock's shape is checked before any field is read — `JSON.parse` returns null, an " +
|
|
478
|
+
"array and a scalar just as happily as an object, and reaching for `.schemaVersion` on one of those leaks a " +
|
|
479
|
+
"native TypeError with no code. This is the one file that says WHICH package gets installed, so an unnamed " +
|
|
480
|
+
`crash there is the worst place in this module to lose a refusal's name (${verdicts.join(" ")})`,
|
|
481
|
+
Object.values(cells).every((v) => v === "runtime-lock-unreadable"),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ── 8. the npm artifact is locked, and that lock agrees with the checkout ──────
|
|
486
|
+
{
|
|
487
|
+
const drifted = fixtureCheckout({ ...LOCK_A, version: "9.9.9" });
|
|
488
|
+
const incoherent = refusal(() =>
|
|
489
|
+
bootstrapRuntime({ env: world("incoherent"), lock: LOCK_A, checkoutRoot: drifted, acquire: fixtureAcquire() }),
|
|
490
|
+
);
|
|
491
|
+
const env = world("provenance");
|
|
492
|
+
const result = install(env);
|
|
493
|
+
const identity = result.journal.artifactIdentity;
|
|
494
|
+
ok(
|
|
495
|
+
"[QK:HRB-ARTIFACT-LOCK] an npm-sourced lock names the exact package and the integrity npm published for it, and " +
|
|
496
|
+
"it must agree with the checkout's own package.json — a lock that drifts from the checkout installs a version " +
|
|
497
|
+
"nobody here reviewed. The ready identity keeps `expectedIntegrity` and `observedDigest` as SEPARATE fields: a " +
|
|
498
|
+
"digest computed from whatever arrived records what happened, and calling it a pin would claim a check only " +
|
|
499
|
+
`the comparison performs (incoherent=${incoherent} identity=${JSON.stringify(identity)})`,
|
|
500
|
+
incoherent === "runtime-lock-incoherent" &&
|
|
501
|
+
identity.kind === "npm" &&
|
|
502
|
+
identity.expectedIntegrity === LOCK_A.integrity &&
|
|
503
|
+
identity.observedDigest !== identity.expectedIntegrity &&
|
|
504
|
+
typeof identity.observedDigest === "string",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── 8a. the COMMITTED lock is coherent with this checkout, whichever source it names ──
|
|
509
|
+
{
|
|
510
|
+
const lock = readRuntimeLock(PLUGIN_DIR);
|
|
511
|
+
const checkout = readCheckoutPackageSpec(REPO);
|
|
512
|
+
const requested = requestedArtifactIdentity({
|
|
513
|
+
lock,
|
|
514
|
+
checkoutRoot: REPO,
|
|
515
|
+
resolveCommit: () => COMMIT_A,
|
|
516
|
+
});
|
|
517
|
+
const npmCoherent = lock.source !== "npm" || (lock.name === checkout.name && lock.version === checkout.version);
|
|
518
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hrb-source-")));
|
|
519
|
+
const read = (body: unknown): string | null => {
|
|
520
|
+
fs.writeFileSync(path.join(dir, "runtime-lock.json"), `${JSON.stringify(body)}\n`);
|
|
521
|
+
return refusal(() => readRuntimeLock(dir));
|
|
522
|
+
};
|
|
523
|
+
const cells: Record<string, string | null> = {
|
|
524
|
+
"no-source": read({ schemaVersion: 2, name: PACKAGE, version: "0.21.0", integrity: "sha512-x" }),
|
|
525
|
+
"unknown-source": read({ schemaVersion: 2, source: "github-release", repository: CHECKOUT_REPOSITORY }),
|
|
526
|
+
"npm-keys-on-checkout": read({
|
|
527
|
+
schemaVersion: 2,
|
|
528
|
+
source: "herdr-checkout",
|
|
529
|
+
repository: CHECKOUT_REPOSITORY,
|
|
530
|
+
integrity: "sha512-x",
|
|
531
|
+
}),
|
|
532
|
+
"v1-lock": read({ schemaVersion: 1, name: PACKAGE, version: "0.21.0", integrity: "sha512-x" }),
|
|
533
|
+
};
|
|
534
|
+
const verdicts = Object.entries(cells).map(([k, v]) => `${k}=${v}`);
|
|
535
|
+
ok(
|
|
536
|
+
"[QK:HRB-LOCK-SOURCE-CLOSED] the lock's `source` is a CLOSED discriminant, the committed lock resolves to a " +
|
|
537
|
+
"requested identity of exactly that kind, and an absent source, an unknown source, one source wearing the " +
|
|
538
|
+
"other's keys, and a previous schema version are each refused by name — which source a host acquires from may " +
|
|
539
|
+
"not be decided by an environment variable, a caller flag or a fallback, because all three let whoever is " +
|
|
540
|
+
"running choose the acquisition authority, and a fallback in particular would turn an unreachable source into " +
|
|
541
|
+
`'install something else instead' (source=${lock.source} kinds=${JSON.stringify(ARTIFACT_KINDS)} requested=${JSON.stringify(requested)} npm-coherent=${npmCoherent} ${verdicts.join(" ")})`,
|
|
542
|
+
ARTIFACT_KINDS.includes(lock.source) &&
|
|
543
|
+
requested.kind === lock.source &&
|
|
544
|
+
artifactStage(requested) === "requested" &&
|
|
545
|
+
npmCoherent &&
|
|
546
|
+
cells["no-source"] === "runtime-lock-source-unknown" &&
|
|
547
|
+
cells["unknown-source"] === "runtime-lock-source-unknown" &&
|
|
548
|
+
cells["npm-keys-on-checkout"] === "runtime-lock-unreadable" &&
|
|
549
|
+
cells["v1-lock"] === "runtime-lock-unreadable",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ── 8b. the repository is a literal on both sides of the wire ──────────────────
|
|
554
|
+
{
|
|
555
|
+
const dir = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hrb-repo-")));
|
|
556
|
+
fs.writeFileSync(
|
|
557
|
+
path.join(dir, "runtime-lock.json"),
|
|
558
|
+
`${JSON.stringify({ schemaVersion: 2, source: "herdr-checkout", repository: "someone/else" })}\n`,
|
|
559
|
+
);
|
|
560
|
+
const lockRepo = refusal(() => readRuntimeLock(dir));
|
|
561
|
+
const identityRepo = refusal(() =>
|
|
562
|
+
certifyArtifactIdentity("borrowed", { kind: "herdr-checkout", repository: "someone/else", commit: COMMIT_A }),
|
|
563
|
+
);
|
|
564
|
+
ok(
|
|
565
|
+
"[QK:HRB-CHECKOUT-REPO-LITERAL] a repository that is not this plugin's is refused where the LOCK is read AND " +
|
|
566
|
+
"wherever an identity is read back — journal, carried previous runtime, activation ledger — so a strange tree " +
|
|
567
|
+
"cannot become ours by being transcribed into a record. Herdr's own remote is a hardcoded " +
|
|
568
|
+
"`https://github.com/{owner}/{repo}.git` with no override (measured, `src/cli/plugin.rs:763`), and binding " +
|
|
569
|
+
`this side to the same literal is what keeps the pair closed (lock=${lockRepo} identity=${identityRepo})`,
|
|
570
|
+
lockRepo === "runtime-checkout-repository-foreign" && identityRepo === "runtime-checkout-repository-foreign",
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── 9. npm fills OUR cache, never the operator's ───────────────────────────────
|
|
575
|
+
{
|
|
576
|
+
const env = world("cache");
|
|
577
|
+
const layout = resolveRuntimeLayout(env);
|
|
578
|
+
const hostile = { ...env, npm_config_cache: path.join(env.HOME as string, ".npm") };
|
|
579
|
+
const pinned = npmEnvironment(hostile, layout.cacheDir);
|
|
580
|
+
install(env);
|
|
581
|
+
const strayRoots = fs
|
|
582
|
+
.readdirSync(env.HOME as string)
|
|
583
|
+
.filter((e) => e !== "data" && e !== "cache")
|
|
584
|
+
.sort();
|
|
585
|
+
ok(
|
|
586
|
+
"[QK:HRB-OWNED-NPM-CACHE] the acquisition environment pins `npm_config_cache` to the Entwurf-owned XDG cache " +
|
|
587
|
+
"and an inherited operator cache setting cannot win — a plugin install that fills the user's default npm " +
|
|
588
|
+
"cache has written megabytes into a root it was never given, and an inverse cannot tell those bytes from " +
|
|
589
|
+
"the ones the operator's own installs put there. The transaction itself also creates NOTHING under HOME " +
|
|
590
|
+
`outside the two XDG roots (pinned=${pinned.npm_config_cache === layout.cacheDir} stray=${JSON.stringify(strayRoots)})`,
|
|
591
|
+
pinned.npm_config_cache === layout.cacheDir &&
|
|
592
|
+
layout.cacheDir === path.join(env.XDG_CACHE_HOME as string, "entwurf", "herdr-plugin", "npm") &&
|
|
593
|
+
strayRoots.length === 0,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── 10. nothing this plugin installs may run install scripts ───────────────────
|
|
598
|
+
{
|
|
599
|
+
const installArgv = buildInstallArgv("/tmp/pkg.tgz", "/tmp/prefix");
|
|
600
|
+
const packArgv = buildPackArgv({ name: PACKAGE, version: "0.21.0" }, "/tmp/cache");
|
|
601
|
+
ok(
|
|
602
|
+
"[QK:HRB-INSTALL-SCRIPTS-OFF] the install argv carries `--ignore-scripts`, and the artifact is a LOCAL tarball " +
|
|
603
|
+
"packed at an exact spec with `--json` so its filename and bytes can be checked before use — a Herdr user " +
|
|
604
|
+
"who typed one plugin command did not ask to run a package's arbitrary postinstall hook in their own HOME, " +
|
|
605
|
+
`and never saw an npm prompt to decline (install=${JSON.stringify(installArgv)} pack=${JSON.stringify(packArgv)})`,
|
|
606
|
+
installArgv.includes("--ignore-scripts") &&
|
|
607
|
+
installArgv[1] === "/tmp/pkg.tgz" &&
|
|
608
|
+
installArgv.includes("--no-save") &&
|
|
609
|
+
packArgv[1] === `${PACKAGE}@0.21.0` &&
|
|
610
|
+
packArgv.includes("--json"),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── 11. nothing at our address is moved without proof that it is ours ──────────
|
|
615
|
+
{
|
|
616
|
+
const env = world("unowned");
|
|
617
|
+
const layout = resolveRuntimeLayout(env);
|
|
618
|
+
fs.mkdirSync(layout.activeDir, { recursive: true });
|
|
619
|
+
fs.writeFileSync(path.join(layout.activeDir, "SOMEBODY-ELSES-FILE"), "not ours\n");
|
|
620
|
+
const before = treeDigest(layout.activeDir);
|
|
621
|
+
const unowned = refusal(() => install(env));
|
|
622
|
+
const after = treeDigest(layout.activeDir);
|
|
623
|
+
|
|
624
|
+
const foreign = world("foreign-root");
|
|
625
|
+
const foreignLayout = resolveRuntimeLayout(foreign);
|
|
626
|
+
install(foreign);
|
|
627
|
+
const stolen = { ...(readCertifiedJournal(foreignLayout) as Journal), runtimeRoot: "/somewhere/else/active" };
|
|
628
|
+
fs.writeFileSync(foreignLayout.journalPath, `${JSON.stringify(stolen)}\n`);
|
|
629
|
+
const foreignRoot = refusal(() => readCertifiedJournal(foreignLayout));
|
|
630
|
+
ok(
|
|
631
|
+
"[QK:HRB-OWNER-STATE-CERTIFIED] a directory at our address with no certified journal behind it is somebody " +
|
|
632
|
+
"else's until proven otherwise, and a journal naming a DIFFERENT root proves nothing about this host — the " +
|
|
633
|
+
"journal is the only thing that makes these directories ours to rename away, and a refusal must leave the " +
|
|
634
|
+
`bytes it refused to touch exactly as it found them (unowned=${unowned} preserved=${before === after} foreign-root=${foreignRoot})`,
|
|
635
|
+
unowned === "runtime-owner-state-missing" && before === after && foreignRoot === "runtime-journal-foreign-root",
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ── 12. the journal's SHAPE is certified, phase by phase ───────────────────────
|
|
640
|
+
{
|
|
641
|
+
const env = world("journal-shape");
|
|
642
|
+
const layout = resolveRuntimeLayout(env);
|
|
643
|
+
install(env);
|
|
644
|
+
const good = readCertifiedJournal(layout) as Journal;
|
|
645
|
+
const write = (body: unknown): string | null => {
|
|
646
|
+
fs.writeFileSync(layout.journalPath, `${JSON.stringify(body)}\n`);
|
|
647
|
+
return refusal(() => readCertifiedJournal(layout));
|
|
648
|
+
};
|
|
649
|
+
const cells: Record<string, string | null> = {
|
|
650
|
+
"json-null": write(null),
|
|
651
|
+
array: write([good]),
|
|
652
|
+
scalar: write("runtime-ready"),
|
|
653
|
+
"missing-key": write({ schemaVersion: 2, phase: "runtime-ready" }),
|
|
654
|
+
"extra-key": write({ ...good, source: "npm" }),
|
|
655
|
+
"wrong-schema": write({ ...good, schemaVersion: 1 }),
|
|
656
|
+
};
|
|
657
|
+
const verdicts = Object.entries(cells).map(([k, v]) => `${k}=${v}`);
|
|
658
|
+
ok(
|
|
659
|
+
"[QK:HRB-JOURNAL-SHAPE-CERTIFIED] a bare `null`, an array, a scalar, a missing key, an EXTRA key beside the one " +
|
|
660
|
+
"identity field, and a previous schema version are ALL refused by one name — `JSON.parse` returning something " +
|
|
661
|
+
"is not the same as a journal, an entry that is accepted becomes authority to delete a user's directories, " +
|
|
662
|
+
"and a second identity-ish field beside `artifactIdentity` would be a second authority that could disagree " +
|
|
663
|
+
`with the first (${verdicts.join(" ")} keys=${JSON.stringify(JOURNAL_KEYS)})`,
|
|
664
|
+
Object.values(cells).every((v) => v === "runtime-journal-uncertified"),
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── 13. a failing candidate leaves the previous runtime exactly as it was ──────
|
|
669
|
+
{
|
|
670
|
+
const env = world("rollback");
|
|
671
|
+
const layout = resolveRuntimeLayout(env);
|
|
672
|
+
install(env);
|
|
673
|
+
const before = treeDigest(layout.activeDir);
|
|
674
|
+
const code = refusal(() => install(env, LOCK_B, fixtureAcquire({ exitCode: 9 })));
|
|
675
|
+
const after = treeDigest(layout.activeDir);
|
|
676
|
+
ok(
|
|
677
|
+
"[QK:HRB-FAILED-CANDIDATE-PRESERVES-ACTIVE] a candidate that fails verification leaves the running runtime " +
|
|
678
|
+
"byte-identical and leaves no staging or backup directory behind — the failure mode this prevents is the " +
|
|
679
|
+
"worst one available here: a host whose wiring names an address that was emptied on behalf of an upgrade " +
|
|
680
|
+
`that never arrived (refusal=${code} identical=${before === after} staging=${fs.existsSync(layout.stagingDir)} previous=${fs.existsSync(layout.previousDir)})`,
|
|
681
|
+
code === "runtime-check-bridge-failed" &&
|
|
682
|
+
before === after &&
|
|
683
|
+
before !== "<absent>" &&
|
|
684
|
+
!fs.existsSync(layout.stagingDir) &&
|
|
685
|
+
!fs.existsSync(layout.previousDir),
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── 14. the last good runtime is never lost ────────────────────────────────────
|
|
690
|
+
{
|
|
691
|
+
// (a) the crash window itself: active renamed away, staging not yet renamed in.
|
|
692
|
+
const torn = world("torn");
|
|
693
|
+
const tornLayout = resolveRuntimeLayout(torn);
|
|
694
|
+
install(torn);
|
|
695
|
+
const tornGood = treeDigest(tornLayout.activeDir);
|
|
696
|
+
fs.renameSync(tornLayout.activeDir, tornLayout.previousDir);
|
|
697
|
+
const tornState = assessRuntimeState(tornLayout, readCertifiedJournal(tornLayout)).state;
|
|
698
|
+
const tornCode = refusal(() => install(torn, LOCK_B, fixtureAcquire({ exitCode: 9 })));
|
|
699
|
+
const tornAfter = treeDigest(tornLayout.activeDir);
|
|
700
|
+
|
|
701
|
+
// (b) the one the coordinator measured: active is CORRUPT and previous is the last good copy.
|
|
702
|
+
const corrupt = world("corrupt-active");
|
|
703
|
+
const corruptLayout = resolveRuntimeLayout(corrupt);
|
|
704
|
+
install(corrupt);
|
|
705
|
+
fs.cpSync(corruptLayout.activeDir, corruptLayout.previousDir, { recursive: true });
|
|
706
|
+
fs.rmSync(path.join(corruptLayout.activeDir, "node_modules", PACKAGE, "package.json"));
|
|
707
|
+
const corruptState = assessRuntimeState(corruptLayout, readCertifiedJournal(corruptLayout)).state;
|
|
708
|
+
const corruptCode = refusal(() => install(corrupt, LOCK_B, fixtureAcquire({ exitCode: 9 })));
|
|
709
|
+
const corruptRescued = inspectInstalledRuntime(corruptLayout.activeDir, PACKAGE).ok;
|
|
710
|
+
|
|
711
|
+
// (c) all three present, active corrupt: the candidate is dropped, the backup still wins.
|
|
712
|
+
const three = world("three-corrupt");
|
|
713
|
+
const threeLayout = resolveRuntimeLayout(three);
|
|
714
|
+
install(three);
|
|
715
|
+
fs.cpSync(threeLayout.activeDir, threeLayout.previousDir, { recursive: true });
|
|
716
|
+
fs.mkdirSync(path.join(threeLayout.stagingDir, "half"), { recursive: true });
|
|
717
|
+
fs.rmSync(path.join(threeLayout.activeDir, "node_modules", PACKAGE, "package.json"));
|
|
718
|
+
const threeState = assessRuntimeState(threeLayout, readCertifiedJournal(threeLayout)).state;
|
|
719
|
+
refusal(() => install(three, LOCK_B, fixtureAcquire({ exitCode: 9 })));
|
|
720
|
+
const threeRescued = inspectInstalledRuntime(threeLayout.activeDir, PACKAGE).ok;
|
|
721
|
+
|
|
722
|
+
ok(
|
|
723
|
+
"[QK:HRB-LAST-GOOD-NEVER-LOST] a backup is the last good runtime until inspection says otherwise, so it is " +
|
|
724
|
+
"restored when the active tree is missing OR corrupt and it is never dropped merely because a sibling " +
|
|
725
|
+
"directory exists — measured on the first cut of this module: with a corrupt active beside a good previous " +
|
|
726
|
+
"it deleted the backup first, then failed to acquire a candidate, and the host ended the run with zero " +
|
|
727
|
+
`usable runtimes at the address its wiring names (torn=${tornState}/${tornCode}/restored=${tornAfter === tornGood} corrupt=${corruptState}/${corruptCode}/rescued=${corruptRescued} three=${threeState}/rescued=${threeRescued})`,
|
|
728
|
+
tornState === "torn-swap" &&
|
|
729
|
+
tornCode === "runtime-check-bridge-failed" &&
|
|
730
|
+
tornAfter === tornGood &&
|
|
731
|
+
tornGood !== "<absent>" &&
|
|
732
|
+
corruptState === "backup-pending" &&
|
|
733
|
+
corruptCode === "runtime-check-bridge-failed" &&
|
|
734
|
+
corruptRescued &&
|
|
735
|
+
threeState === "backup-pending-with-candidate" &&
|
|
736
|
+
threeRescued,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── 15. all eight presence combinations have a name, and none leaves residue ───
|
|
741
|
+
{
|
|
742
|
+
const cases: { tag: string; arrange: (l: Layout) => void; expect: string }[] = [
|
|
743
|
+
{ tag: "000", arrange: (l) => fs.rmSync(l.activeDir, { recursive: true }), expect: "owned-empty" },
|
|
744
|
+
{ tag: "001", arrange: (l) => fs.renameSync(l.activeDir, l.previousDir), expect: "torn-swap" },
|
|
745
|
+
{
|
|
746
|
+
tag: "010",
|
|
747
|
+
arrange: (l) => {
|
|
748
|
+
fs.mkdirSync(path.join(l.stagingDir, "half"), { recursive: true });
|
|
749
|
+
fs.rmSync(l.activeDir, { recursive: true });
|
|
750
|
+
},
|
|
751
|
+
expect: "abandoned-candidate",
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
tag: "011",
|
|
755
|
+
arrange: (l) => {
|
|
756
|
+
fs.renameSync(l.activeDir, l.previousDir);
|
|
757
|
+
fs.mkdirSync(path.join(l.stagingDir, "half"), { recursive: true });
|
|
758
|
+
},
|
|
759
|
+
expect: "torn-swap-with-candidate",
|
|
760
|
+
},
|
|
761
|
+
{ tag: "100", arrange: () => {}, expect: "settled" },
|
|
762
|
+
{
|
|
763
|
+
tag: "101",
|
|
764
|
+
arrange: (l) => fs.cpSync(l.activeDir, l.previousDir, { recursive: true }),
|
|
765
|
+
expect: "backup-pending",
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
tag: "110",
|
|
769
|
+
arrange: (l) => fs.mkdirSync(path.join(l.stagingDir, "half"), { recursive: true }),
|
|
770
|
+
expect: "stale-candidate",
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
tag: "111",
|
|
774
|
+
arrange: (l) => {
|
|
775
|
+
fs.cpSync(l.activeDir, l.previousDir, { recursive: true });
|
|
776
|
+
fs.mkdirSync(path.join(l.stagingDir, "half"), { recursive: true });
|
|
777
|
+
},
|
|
778
|
+
expect: "backup-pending-with-candidate",
|
|
779
|
+
},
|
|
780
|
+
];
|
|
781
|
+
const rows: string[] = [];
|
|
782
|
+
let allClean = true;
|
|
783
|
+
for (const c of cases) {
|
|
784
|
+
const env = world(`combo-${c.tag}`);
|
|
785
|
+
const layout = resolveRuntimeLayout(env);
|
|
786
|
+
install(env);
|
|
787
|
+
c.arrange(layout);
|
|
788
|
+
const assessed = assessRuntimeState(layout, readCertifiedJournal(layout));
|
|
789
|
+
install(env);
|
|
790
|
+
const residue = fs.existsSync(layout.stagingDir) || fs.existsSync(layout.previousDir);
|
|
791
|
+
rows.push(`${c.tag}=${assessed.state}${residue ? "/RESIDUE" : ""}`);
|
|
792
|
+
if (assessed.state !== c.expect || residue) allClean = false;
|
|
793
|
+
}
|
|
794
|
+
ok(
|
|
795
|
+
"[QK:HRB-ALL-STATES-NAMED] every one of the eight active/staging/previous combinations has its own name and " +
|
|
796
|
+
"none of them survives the next install — an unnamed combination is a branch nobody decided, which is how a " +
|
|
797
|
+
"half-written candidate silently becomes the next runtime and how a host that repaired itself becomes " +
|
|
798
|
+
`indistinguishable from one that never broke (${rows.join(" ")})`,
|
|
799
|
+
allClean && rows.length === 8,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ── 16. the provenance of the runtime being replaced is carried, not overwritten ─
|
|
804
|
+
{
|
|
805
|
+
const env = world("provenance-carry");
|
|
806
|
+
const layout = resolveRuntimeLayout(env);
|
|
807
|
+
const first = install(env);
|
|
808
|
+
const acquireDies = () => {
|
|
809
|
+
throw new Error("the process died during acquisition");
|
|
810
|
+
};
|
|
811
|
+
refusal(() => install(env, LOCK_B, acquireDies));
|
|
812
|
+
const torn = readCertifiedJournal(layout);
|
|
813
|
+
// `artifactStage` reads the KEYS off what it is handed, so asking it about a provenance that is
|
|
814
|
+
// missing throws a TypeError from inside the condition — the gate would die on the exact input
|
|
815
|
+
// this claim exists to refuse, and the red would carry someone else's name. Absence is a stage
|
|
816
|
+
// answer here, and the assertion judges it.
|
|
817
|
+
const stageOf = (identity: unknown): string | null =>
|
|
818
|
+
identity !== null && typeof identity === "object" ? artifactStage(identity as ArtifactIdentity) : null;
|
|
819
|
+
ok(
|
|
820
|
+
"[QK:HRB-PROVENANCE-CARRIED] writing the `installing` entry carries the finished runtime's provenance into " +
|
|
821
|
+
"`previousRuntime` instead of overwriting it — a host that dies mid-install otherwise holds a backup " +
|
|
822
|
+
"directory and no statement of what is in it, which is recoverable bytes with unrecoverable provenance; a " +
|
|
823
|
+
`finished install sets it back to null because there is then no previous runtime to describe (ready.previous=${JSON.stringify(first.journal.previousRuntime)} torn.phase=${torn?.phase} torn.identity=${JSON.stringify(torn?.artifactIdentity)} torn.previous=${JSON.stringify(torn?.previousRuntime)})`,
|
|
824
|
+
first.journal.previousRuntime === null &&
|
|
825
|
+
torn?.phase === "installing" &&
|
|
826
|
+
stageOf(torn.artifactIdentity) === "requested" &&
|
|
827
|
+
torn.artifactIdentity.version === LOCK_B.version &&
|
|
828
|
+
stageOf(torn.previousRuntime) === "ready" &&
|
|
829
|
+
torn.previousRuntime?.version === LOCK_A.version &&
|
|
830
|
+
torn.previousRuntime?.observedDigest === first.journal.artifactIdentity.observedDigest,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── 17. the same exact runtime is not acquired twice ───────────────────────────
|
|
835
|
+
{
|
|
836
|
+
const env = world("idempotent");
|
|
837
|
+
let calls = 0;
|
|
838
|
+
const counted = (args: AcquireArgs) => {
|
|
839
|
+
calls++;
|
|
840
|
+
return fixtureAcquire()(args);
|
|
841
|
+
};
|
|
842
|
+
const first = install(env, LOCK_A, counted);
|
|
843
|
+
const second = install(env, LOCK_A, counted);
|
|
844
|
+
ok(
|
|
845
|
+
"[QK:HRB-IDEMPOTENT-REINSTALL] a reinstall of the SAME exact spec over a journal-and-disk-backed runtime " +
|
|
846
|
+
"acquires nothing and replaces nothing — Herdr's own reinstall is the explicit refresh trigger this product " +
|
|
847
|
+
"depends on, so the common case is a plugin reinstall on a host that already has the right runtime, and a " +
|
|
848
|
+
`transaction that re-downloads and re-swaps there makes the safe operation the expensive one (calls=${calls} changed=${first.changed}/${second.changed})`,
|
|
849
|
+
first.changed === true && second.changed === false && calls === 1,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ── 18. a journal the disk does not back is not believed ───────────────────────
|
|
854
|
+
{
|
|
855
|
+
const env = world("reconcile");
|
|
856
|
+
const layout = resolveRuntimeLayout(env);
|
|
857
|
+
install(env);
|
|
858
|
+
fs.rmSync(path.join(layout.activeDir, "node_modules", PACKAGE, "package.json"));
|
|
859
|
+
const journalBefore = readCertifiedJournal(layout);
|
|
860
|
+
const repaired = install(env);
|
|
861
|
+
ok(
|
|
862
|
+
"[QK:HRB-JOURNAL-MUST-MATCH-DISK] a `runtime-ready` journal is believed only while the active tree still " +
|
|
863
|
+
"verifies — the journal is a record of an intention that completed, not evidence that the bytes are still " +
|
|
864
|
+
"there, and the reconciliation that makes an externally-damaged install recoverable is exactly this " +
|
|
865
|
+
`re-verification; trusting the record alone turns one bad state into a permanent one (before=${journalBefore?.phase} changed=${repaired.changed})`,
|
|
866
|
+
journalBefore?.phase === "runtime-ready" &&
|
|
867
|
+
repaired.changed === true &&
|
|
868
|
+
fs.existsSync(path.join(layout.activeDir, "node_modules", PACKAGE, "package.json")),
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ── 19. the inverse removes only what it can prove, runtime last ───────────────
|
|
873
|
+
{
|
|
874
|
+
const foreign = world("inverse-foreign");
|
|
875
|
+
const foreignLayout = resolveRuntimeLayout(foreign);
|
|
876
|
+
fs.mkdirSync(foreignLayout.activeDir, { recursive: true });
|
|
877
|
+
fs.writeFileSync(path.join(foreignLayout.activeDir, "SOMEBODY-ELSES-FILE"), "not ours\n");
|
|
878
|
+
const beforeForeign = treeDigest(foreignLayout.activeDir);
|
|
879
|
+
const refused = refusal(() => removeOwnedRuntime({ env: foreign }));
|
|
880
|
+
const afterForeign = treeDigest(foreignLayout.activeDir);
|
|
881
|
+
|
|
882
|
+
const clean = world("inverse-clean");
|
|
883
|
+
const cleanLayout = resolveRuntimeLayout(clean);
|
|
884
|
+
install(clean);
|
|
885
|
+
fs.mkdirSync(cleanLayout.cacheDir, { recursive: true });
|
|
886
|
+
fs.writeFileSync(path.join(cleanLayout.cacheDir, "tarball.tgz"), "cached\n");
|
|
887
|
+
const result = removeOwnedRuntime({ env: clean });
|
|
888
|
+
ok(
|
|
889
|
+
"[QK:HRB-INVERSE-STATE-BACKED] the inverse establishes every deletion authority BEFORE the first deletion, " +
|
|
890
|
+
"refuses an address it cannot prove is ours without touching a byte, and removes cache → runtime → journal " +
|
|
891
|
+
"so the ledger outlives what it authorised — Herdr has no cleanup hook (measured: the plugin manifest has " +
|
|
892
|
+
"six sections and none of them is an uninstall), so this is the only inverse that will ever run, and one " +
|
|
893
|
+
`that deletes as it checks leaves a host half-removed with nothing recording where it stopped (refusal=${refused} untouched=${beforeForeign === afterForeign} removed=${JSON.stringify(result.removed)})`,
|
|
894
|
+
refused === "runtime-inverse-foreign-refused" &&
|
|
895
|
+
beforeForeign === afterForeign &&
|
|
896
|
+
JSON.stringify(result.removed) === JSON.stringify(["cache", "runtime", "journal"]) &&
|
|
897
|
+
!fs.existsSync(cleanLayout.runtimeRoot) &&
|
|
898
|
+
!fs.existsSync(cleanLayout.journalPath) &&
|
|
899
|
+
!fs.existsSync(cleanLayout.cacheDir),
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ── 20. this slice never claims Herdr's commit ─────────────────────────────────
|
|
904
|
+
{
|
|
905
|
+
const env = world("no-claim");
|
|
906
|
+
const layout = resolveRuntimeLayout(env);
|
|
907
|
+
const result = install(env);
|
|
908
|
+
const rejected = refusal(() => writeJournal(layout, { phase: "herdr-installed" }));
|
|
909
|
+
ok(
|
|
910
|
+
"[QK:HRB-NO-HERDR-CLAIM] success is recorded as `runtime-ready` and `herdr-installed` is not a phase this " +
|
|
911
|
+
"module can write — `[[build]]` finishes BEFORE Herdr re-reads the manifest, swaps its checkout and " +
|
|
912
|
+
"registers the plugin, so at the moment this transaction succeeds nobody knows whether Herdr will commit; " +
|
|
913
|
+
"a journal that says the plugin is installed would be a claim about a step that had not run, and the next " +
|
|
914
|
+
`install would reconcile against a lie (phase=${result.phase} phases=${JSON.stringify(JOURNAL_PHASES)} rejected=${rejected})`,
|
|
915
|
+
result.phase === "runtime-ready" &&
|
|
916
|
+
!JOURNAL_PHASES.includes("herdr-installed") &&
|
|
917
|
+
rejected === "runtime-journal-phase-unknown",
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// ── 21. one identity union, exact per kind AND per stage ──────────────────────
|
|
922
|
+
{
|
|
923
|
+
const readyNpm = {
|
|
924
|
+
kind: "npm",
|
|
925
|
+
name: PACKAGE,
|
|
926
|
+
version: "0.21.0",
|
|
927
|
+
expectedIntegrity: "sha512-x",
|
|
928
|
+
observedDigest: `sha256-${"a".repeat(64)}`,
|
|
929
|
+
};
|
|
930
|
+
const readyCheckout = {
|
|
931
|
+
kind: "herdr-checkout",
|
|
932
|
+
repository: CHECKOUT_REPOSITORY,
|
|
933
|
+
commit: COMMIT_A,
|
|
934
|
+
packageName: PACKAGE,
|
|
935
|
+
packageVersion: "0.21.0",
|
|
936
|
+
observedDigest: `sha256-${"b".repeat(64)}`,
|
|
937
|
+
};
|
|
938
|
+
const cells: Record<string, string | null> = {
|
|
939
|
+
"npm-ready-as-requested": refusal(() => certifyArtifactIdentity("x", readyNpm, "requested")),
|
|
940
|
+
"npm-requested-as-ready": refusal(() =>
|
|
941
|
+
certifyArtifactIdentity(
|
|
942
|
+
"x",
|
|
943
|
+
{ kind: "npm", name: PACKAGE, version: "0.21.0", expectedIntegrity: "sha512-x" },
|
|
944
|
+
"ready",
|
|
945
|
+
),
|
|
946
|
+
),
|
|
947
|
+
"checkout-keys-on-npm": refusal(() =>
|
|
948
|
+
certifyArtifactIdentity("x", { ...readyNpm, repository: CHECKOUT_REPOSITORY }, "ready"),
|
|
949
|
+
),
|
|
950
|
+
"npm-keys-on-checkout": refusal(() => certifyArtifactIdentity("x", { ...readyCheckout, name: PACKAGE }, "ready")),
|
|
951
|
+
"unknown-kind": refusal(() => certifyArtifactIdentity("x", { ...readyNpm, kind: "github-release" }, "ready")),
|
|
952
|
+
"short-commit": refusal(() => certifyArtifactIdentity("x", { ...readyCheckout, commit: "abc" }, "ready")),
|
|
953
|
+
"uppercase-commit": refusal(() =>
|
|
954
|
+
certifyArtifactIdentity("x", { ...readyCheckout, commit: COMMIT_A.toUpperCase().replace(/1/g, "A") }, "ready"),
|
|
955
|
+
),
|
|
956
|
+
"blank-completeness": refusal(() =>
|
|
957
|
+
certifyArtifactIdentity("x", { ...readyCheckout, packageVersion: " " }, "ready"),
|
|
958
|
+
),
|
|
959
|
+
"bad-digest": refusal(() =>
|
|
960
|
+
certifyArtifactIdentity("x", { ...readyCheckout, observedDigest: "sha256-nope" }, "ready"),
|
|
961
|
+
),
|
|
962
|
+
nullish: refusal(() => certifyArtifactIdentity("x", null, "ready")),
|
|
963
|
+
};
|
|
964
|
+
const accepted =
|
|
965
|
+
JSON.stringify(ARTIFACT_KEYS.npm) ===
|
|
966
|
+
JSON.stringify({
|
|
967
|
+
requested: ["kind", "name", "version", "expectedIntegrity"],
|
|
968
|
+
ready: ["kind", "name", "version", "expectedIntegrity", "observedDigest"],
|
|
969
|
+
}) &&
|
|
970
|
+
JSON.stringify(ARTIFACT_KEYS["herdr-checkout"]) ===
|
|
971
|
+
JSON.stringify({
|
|
972
|
+
requested: ["kind", "repository", "commit"],
|
|
973
|
+
ready: ["kind", "repository", "commit", "packageName", "packageVersion", "observedDigest"],
|
|
974
|
+
}) &&
|
|
975
|
+
certifyArtifactIdentity("x", readyNpm, "ready").kind === "npm" &&
|
|
976
|
+
certifyArtifactIdentity("x", readyCheckout, "ready").kind === "herdr-checkout" &&
|
|
977
|
+
artifactStage(readyCheckout) === "ready" &&
|
|
978
|
+
artifactCompleteness(readyCheckout).version === "0.21.0" &&
|
|
979
|
+
artifactCompleteness(readyNpm).name === PACKAGE;
|
|
980
|
+
const completenessOfRequested = refusal(() =>
|
|
981
|
+
artifactCompleteness({ kind: "herdr-checkout", repository: CHECKOUT_REPOSITORY, commit: COMMIT_A }),
|
|
982
|
+
);
|
|
983
|
+
const verdicts = Object.entries(cells).map(([k, v]) => `${k}=${v}`);
|
|
984
|
+
ok(
|
|
985
|
+
"[QK:HRB-ARTIFACT-IDENTITY-EXACT] one identity union, with an EXACT key set per kind and per stage: a ready " +
|
|
986
|
+
"shape where a request belongs, a request where a ready belongs, either kind wearing the other's keys, an " +
|
|
987
|
+
"unknown kind, a short or upper-case commit, a blank completeness pair and a malformed digest are each " +
|
|
988
|
+
"refused — a union that merged the two sources would have to accept a null integrity or a null commit, in " +
|
|
989
|
+
"which 'not observed yet' and 'this source has no such fact' become the same value, and a REQUESTED identity " +
|
|
990
|
+
`must not be able to answer what is on disk at all (${verdicts.join(" ")} accepted=${accepted} completeness-of-requested=${completenessOfRequested})`,
|
|
991
|
+
Object.values(cells).every((v) => v === "runtime-artifact-identity-uncertified") &&
|
|
992
|
+
accepted &&
|
|
993
|
+
completenessOfRequested === "runtime-artifact-identity-uncertified",
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ── 22. for a checkout, the COMMIT decides — never the version ────────────────
|
|
998
|
+
{
|
|
999
|
+
const env = world("commit-decides");
|
|
1000
|
+
let calls = 0;
|
|
1001
|
+
const counted = (args: AcquireArgs) => {
|
|
1002
|
+
calls++;
|
|
1003
|
+
return fixtureAcquire()(args);
|
|
1004
|
+
};
|
|
1005
|
+
const first = install(env, CHECKOUT_LOCK, counted, COMMIT_A);
|
|
1006
|
+
const again = install(env, CHECKOUT_LOCK, counted, COMMIT_A);
|
|
1007
|
+
// SAME package version, DIFFERENT commit: the fixture package is 0.21.0 in both runs.
|
|
1008
|
+
const rebuilt = install(env, CHECKOUT_LOCK, counted, COMMIT_B);
|
|
1009
|
+
const identity = rebuilt.journal.artifactIdentity;
|
|
1010
|
+
const sameVersionDifferentCommit = !sameArtifactRequest(first.journal.artifactIdentity, {
|
|
1011
|
+
kind: "herdr-checkout",
|
|
1012
|
+
repository: CHECKOUT_REPOSITORY,
|
|
1013
|
+
commit: COMMIT_B,
|
|
1014
|
+
});
|
|
1015
|
+
const crossSource = !sameArtifactRequest(rebuilt.journal.artifactIdentity, {
|
|
1016
|
+
kind: "npm",
|
|
1017
|
+
name: PACKAGE,
|
|
1018
|
+
version: "0.21.0",
|
|
1019
|
+
expectedIntegrity: "sha512-fixtureA",
|
|
1020
|
+
});
|
|
1021
|
+
ok(
|
|
1022
|
+
"[QK:HRB-IDENTITY-COMMIT-DECIDES] a checkout-sourced runtime is identified by its COMMIT: the same commit twice " +
|
|
1023
|
+
"acquires nothing, and a NEW commit carrying the same package version is reinstalled rather than skipped — " +
|
|
1024
|
+
"two commits can both call themselves 0.21.0, so a version comparison would let the previous candidate keep " +
|
|
1025
|
+
"serving while every receipt above it named the new one, which is the silence this whole lane exists to " +
|
|
1026
|
+
`prevent; a different SOURCE is never the same artifact either (calls=${calls} changed=${first.changed}/${again.changed}/${rebuilt.changed} commit=${identity.commit?.slice(0, 8)} version=${identity.packageVersion} distinct=${sameVersionDifferentCommit} cross-source=${crossSource})`,
|
|
1027
|
+
calls === 2 &&
|
|
1028
|
+
first.changed === true &&
|
|
1029
|
+
again.changed === false &&
|
|
1030
|
+
rebuilt.changed === true &&
|
|
1031
|
+
identity.kind === "herdr-checkout" &&
|
|
1032
|
+
identity.commit === COMMIT_B &&
|
|
1033
|
+
identity.packageVersion === "0.21.0" &&
|
|
1034
|
+
identity.repository === CHECKOUT_REPOSITORY &&
|
|
1035
|
+
sameVersionDifferentCommit &&
|
|
1036
|
+
crossSource,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// ── 23. the commit is read from the checkout, canonically ─────────────────────
|
|
1041
|
+
{
|
|
1042
|
+
let asked: string[] = [];
|
|
1043
|
+
const git = (result: Partial<SpawnLike>) => ({
|
|
1044
|
+
spawn: (_bin: string, argv: string[]): SpawnLike => {
|
|
1045
|
+
asked = argv;
|
|
1046
|
+
return { status: 0, stdout: "", stderr: "", ...result } as SpawnLike;
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
const resolved = resolveCheckoutCommit("/checkout", git({ stdout: `${COMMIT_A}\n` }));
|
|
1050
|
+
const short = refusal(() => resolveCheckoutCommit("/checkout", git({ stdout: "abc1234\n" })));
|
|
1051
|
+
const empty = refusal(() => resolveCheckoutCommit("/checkout", git({ stdout: "\n" })));
|
|
1052
|
+
const failed = refusal(() =>
|
|
1053
|
+
resolveCheckoutCommit("/checkout", git({ status: 128, stderr: "not a git repository" })),
|
|
1054
|
+
);
|
|
1055
|
+
const missing = refusal(() =>
|
|
1056
|
+
resolveCheckoutCommit("/checkout", {
|
|
1057
|
+
spawn: () => ({ status: null, stdout: "", stderr: "", error: new Error("spawn git ENOENT") }),
|
|
1058
|
+
}),
|
|
1059
|
+
);
|
|
1060
|
+
ok(
|
|
1061
|
+
"[QK:HRB-CHECKOUT-COMMIT-CANONICAL] the candidate's commit comes from the checkout itself via " +
|
|
1062
|
+
"`rev-parse --verify HEAD^{commit}` — `--verify` refuses an ambiguous or missing revision instead of echoing " +
|
|
1063
|
+
"the argument back, and `^{commit}` resolves a tag down to the commit, so what lands in the identity is a " +
|
|
1064
|
+
"full 40-hex commit or a named refusal. A shallow clone answers this exactly as a full one does, which is " +
|
|
1065
|
+
`what makes Herdr's own managed checkout usable (argv=${JSON.stringify(asked)} resolved=${resolved.slice(0, 8)} short=${short} empty=${empty} failed=${failed} missing=${missing})`,
|
|
1066
|
+
JSON.stringify(asked) === JSON.stringify(["-C", "/checkout", "rev-parse", "--verify", "HEAD^{commit}"]) &&
|
|
1067
|
+
resolved === COMMIT_A &&
|
|
1068
|
+
short === "runtime-checkout-commit-unresolvable" &&
|
|
1069
|
+
empty === "runtime-checkout-commit-unresolvable" &&
|
|
1070
|
+
failed === "runtime-checkout-commit-unresolvable" &&
|
|
1071
|
+
missing === "runtime-checkout-commit-unresolvable",
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// ── 24. the product pack argv is the fixed remote at that commit, and no other form ──
|
|
1076
|
+
{
|
|
1077
|
+
const identity = { kind: "herdr-checkout", repository: CHECKOUT_REPOSITORY, commit: COMMIT_A };
|
|
1078
|
+
const argv = buildCheckoutPackArgv(identity, "/tmp/cache");
|
|
1079
|
+
const source = fs.readFileSync(LEAF, "utf8");
|
|
1080
|
+
const packLiterals = (source.match(/"pack",/g) ?? []).length;
|
|
1081
|
+
ok(
|
|
1082
|
+
"[QK:HRB-CHECKOUT-ARGV-FIXED] the only checkout acquisition is `npm pack git+https://github.com/" +
|
|
1083
|
+
"junghan0611/entwurf.git#<full sha>` — a fixed remote, a full commit, `--json`, our own pack destination, and " +
|
|
1084
|
+
"NO `--ignore-scripts`, because on a git spec npm runs `prepare` (which compiles the bridge) and not " +
|
|
1085
|
+
"`prepack`; measured 2026-09-16 on npm 11.16.0, where the same command produced a byte-identical tarball " +
|
|
1086
|
+
"across three sandboxes including a shallow clone. The DIRECTORY form is structurally absent: it runs " +
|
|
1087
|
+
"`prepack`, which calls pnpm and exits 127 on a clean host, so this module builds exactly two pack argvs and " +
|
|
1088
|
+
`neither of them is a path (argv=${JSON.stringify(argv)} pack-literals=${packLiterals})`,
|
|
1089
|
+
JSON.stringify(argv) ===
|
|
1090
|
+
JSON.stringify([
|
|
1091
|
+
"pack",
|
|
1092
|
+
`git+https://github.com/${CHECKOUT_REPOSITORY}.git#${COMMIT_A}`,
|
|
1093
|
+
"--json",
|
|
1094
|
+
"--pack-destination",
|
|
1095
|
+
"/tmp/cache",
|
|
1096
|
+
"--no-audit",
|
|
1097
|
+
"--no-fund",
|
|
1098
|
+
]) &&
|
|
1099
|
+
buildCheckoutRemote(CHECKOUT_REPOSITORY) === `git+https://github.com/${CHECKOUT_REPOSITORY}.git` &&
|
|
1100
|
+
!argv.includes("--ignore-scripts") &&
|
|
1101
|
+
packLiterals === 2,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// ── 25. a source that cannot serve the commit is named, never substituted ─────
|
|
1106
|
+
{
|
|
1107
|
+
const bin = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-hrb-npm-")));
|
|
1108
|
+
const fakeNpm = (body: string): string => {
|
|
1109
|
+
const p = path.join(bin, `npm-${createHash("sha256").update(body).digest("hex").slice(0, 8)}`);
|
|
1110
|
+
fs.writeFileSync(p, `#!/bin/sh\n${body}\n`);
|
|
1111
|
+
fs.chmodSync(p, 0o755);
|
|
1112
|
+
return p;
|
|
1113
|
+
};
|
|
1114
|
+
const env = world("source-unavailable");
|
|
1115
|
+
const layout = resolveRuntimeLayout(env);
|
|
1116
|
+
const identity = certifyArtifactIdentity(
|
|
1117
|
+
"x",
|
|
1118
|
+
{ kind: "herdr-checkout", repository: CHECKOUT_REPOSITORY, commit: COMMIT_A },
|
|
1119
|
+
"requested",
|
|
1120
|
+
);
|
|
1121
|
+
const args = { identity, prefix: path.join(env.HOME as string, "prefix"), cacheDir: layout.cacheDir, env };
|
|
1122
|
+
const remoteRefused = refusal(() =>
|
|
1123
|
+
checkoutAcquire({ ...args, npmBin: fakeNpm('echo "Could not read from remote repository" >&2; exit 128') }),
|
|
1124
|
+
);
|
|
1125
|
+
const noJson = refusal(() => checkoutAcquire({ ...args, npmBin: fakeNpm('echo "[]"; exit 0') }));
|
|
1126
|
+
const prefixUntouched = classifyPath(args.prefix);
|
|
1127
|
+
ok(
|
|
1128
|
+
"[QK:HRB-SOURCE-UNAVAILABLE-NAMED] a remote that cannot serve the commit, and a pack whose `--json` carries no " +
|
|
1129
|
+
"artifact, are ONE named refusal — `runtime-checkout-source-unavailable` — and nothing is installed in their " +
|
|
1130
|
+
"place. This is the branch where a fallback would be most tempting and most wrong: 'the commit is not there, " +
|
|
1131
|
+
"so install a registry version instead' would silently substitute an artifact nobody asked for, on the one " +
|
|
1132
|
+
`path whose entire purpose is verifying a specific commit (remote=${remoteRefused} no-json=${noJson} prefix=${prefixUntouched})`,
|
|
1133
|
+
remoteRefused === "runtime-checkout-source-unavailable" &&
|
|
1134
|
+
noJson === "runtime-checkout-source-unavailable" &&
|
|
1135
|
+
prefixUntouched === "absent",
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ── 26. a cache we cannot prove is ours is not adopted ────────────────────────
|
|
1140
|
+
{
|
|
1141
|
+
const env = world("cache-unowned");
|
|
1142
|
+
const layout = resolveRuntimeLayout(env);
|
|
1143
|
+
fs.mkdirSync(layout.cacheDir, { recursive: true });
|
|
1144
|
+
fs.writeFileSync(path.join(layout.cacheDir, "someone-elses.tgz"), "not ours\n");
|
|
1145
|
+
const before = treeDigest(layout.cacheDir);
|
|
1146
|
+
const refused = refusal(() => install(env));
|
|
1147
|
+
const after = treeDigest(layout.cacheDir);
|
|
1148
|
+
const assessed = assessRuntimeState(layout, null);
|
|
1149
|
+
ok(
|
|
1150
|
+
"[QK:HRB-CACHE-UNOWNED-REFUSED] a cache directory at our address with NO certified journal behind it is refused " +
|
|
1151
|
+
"by name and left byte-identical — the cache is the fourth thing this module deletes, and a transaction whose " +
|
|
1152
|
+
"first act on a strange host is reclaiming a tree it cannot prove it wrote has adopted somebody else's bytes " +
|
|
1153
|
+
`in order to tidy them away (state=${assessed.state} refusal=${refused} untouched=${before === after})`,
|
|
1154
|
+
assessed.state === "unowned-cache-residue" &&
|
|
1155
|
+
refused === "runtime-cache-unowned-residue" &&
|
|
1156
|
+
before === after &&
|
|
1157
|
+
before !== "<absent>",
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// ── 26a. the INVERSE reads that same ownership fact the same way ──────────────
|
|
1162
|
+
{
|
|
1163
|
+
const env = world("inverse-cache-unowned");
|
|
1164
|
+
const layout = resolveRuntimeLayout(env);
|
|
1165
|
+
fs.mkdirSync(layout.cacheDir, { recursive: true });
|
|
1166
|
+
fs.writeFileSync(path.join(layout.cacheDir, "someone-elses.tgz"), "not ours\n");
|
|
1167
|
+
const before = treeDigest(layout.cacheDir);
|
|
1168
|
+
const refused = refusal(() => removeOwnedRuntime({ env }));
|
|
1169
|
+
const after = treeDigest(layout.cacheDir);
|
|
1170
|
+
ok(
|
|
1171
|
+
"[QK:HRB-INVERSE-CACHE-UNOWNED] the inverse refuses a journal-less cache by name, with its bytes untouched — the " +
|
|
1172
|
+
"forward transaction already refuses that exact state, and one ownership fact read two different ways is how " +
|
|
1173
|
+
"the careless side eventually deletes what the careful side would not: the inverse's plan BEGINS with this " +
|
|
1174
|
+
`directory, so 'nothing of ours, report success' is the wrong answer to give about it (refusal=${refused} untouched=${before === after})`,
|
|
1175
|
+
refused === "runtime-inverse-foreign-refused" && before === after && before !== "<absent>",
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// ── 27. a failed acquisition reclaims what IT wrote, and a retry is green ─────
|
|
1180
|
+
{
|
|
1181
|
+
const env = world("cache-reclaim");
|
|
1182
|
+
const layout = resolveRuntimeLayout(env);
|
|
1183
|
+
install(env, CHECKOUT_LOCK, fixtureAcquire(), COMMIT_A);
|
|
1184
|
+
const goodTree = treeDigest(layout.activeDir);
|
|
1185
|
+
const goodJournal = readCertifiedJournal(layout) as Journal;
|
|
1186
|
+
const halfFetched = (args: AcquireArgs): AcquireResult => {
|
|
1187
|
+
fs.mkdirSync(args.cacheDir, { recursive: true });
|
|
1188
|
+
fs.writeFileSync(path.join(args.cacheDir, "half.tgz"), "truncated\n");
|
|
1189
|
+
throw new Error("the remote hung up mid-pack");
|
|
1190
|
+
};
|
|
1191
|
+
const code = refusal(() => install(env, CHECKOUT_LOCK, halfFetched, COMMIT_B));
|
|
1192
|
+
const afterFailure = {
|
|
1193
|
+
staging: classifyPath(layout.stagingDir),
|
|
1194
|
+
cache: classifyPath(layout.cacheDir),
|
|
1195
|
+
previous: classifyPath(layout.previousDir),
|
|
1196
|
+
active: treeDigest(layout.activeDir),
|
|
1197
|
+
journal: readCertifiedJournal(layout) as Journal,
|
|
1198
|
+
};
|
|
1199
|
+
const retry = install(env, CHECKOUT_LOCK, fixtureAcquire(), COMMIT_B);
|
|
1200
|
+
ok(
|
|
1201
|
+
"[QK:HRB-CACHE-FAILED-ACQUIRE-RECLAIMED] a failed acquisition reclaims the staging tree AND the cache this " +
|
|
1202
|
+
"transaction filled — its own `installing` journal is what proves both are ours — while the journal itself, " +
|
|
1203
|
+
"the backup and the last good runtime are preserved, so the retry that follows is green rather than tripping " +
|
|
1204
|
+
"over a truncated tarball nobody owns. The authority order is the point: the cache may be reclaimed only " +
|
|
1205
|
+
`AFTER the write that claims it, never before (refusal=${code} staging=${afterFailure.staging} cache=${afterFailure.cache} previous=${afterFailure.previous} active-preserved=${afterFailure.active === goodTree} journal=${afterFailure.journal.phase} retry=${retry.changed}/${retry.journal.artifactIdentity.commit?.slice(0, 8)})`,
|
|
1206
|
+
code !== null &&
|
|
1207
|
+
afterFailure.staging === "absent" &&
|
|
1208
|
+
afterFailure.cache === "absent" &&
|
|
1209
|
+
afterFailure.previous === "absent" &&
|
|
1210
|
+
afterFailure.active === goodTree &&
|
|
1211
|
+
goodTree !== "<absent>" &&
|
|
1212
|
+
afterFailure.journal.phase === "installing" &&
|
|
1213
|
+
afterFailure.journal.previousRuntime?.commit === COMMIT_A &&
|
|
1214
|
+
afterFailure.journal.previousRuntime?.observedDigest === goodJournal.artifactIdentity.observedDigest &&
|
|
1215
|
+
retry.changed === true &&
|
|
1216
|
+
retry.journal.phase === "runtime-ready" &&
|
|
1217
|
+
retry.journal.artifactIdentity.commit === COMMIT_B,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// ── 28. OUR OWN lifecycle scripts may not write to the stdout this rail parses ─
|
|
1222
|
+
{
|
|
1223
|
+
// `[측정 2026-09-17, oracle, npm 11.16.0 / node 24.18.1]` the checkout branch reads
|
|
1224
|
+
// `npm pack --json`'s STDOUT, and npm forwards a lifecycle script's stdout into that same
|
|
1225
|
+
// stream. Packing the product's own GitHub git spec runs `prepare` in a tree that has NO
|
|
1226
|
+
// `.git` — GitHub serves a codeload tarball rather than a clone — where husky v9 writes
|
|
1227
|
+
// `.git can't be found` with `p.stdout.write` (`husky/bin.js`), so the JSON arrives with
|
|
1228
|
+
// that sentence in front of it. Measured against the real remote at `ee535a17`: exit 0, a
|
|
1229
|
+
// perfectly good 13,017,411-byte tarball, and stdout beginning `.git can't be found`.
|
|
1230
|
+
// On the operator's clean host that became
|
|
1231
|
+
// `runtime-checkout-source-unavailable: npm pack --json was unreadable: Unexpected token '.'`.
|
|
1232
|
+
//
|
|
1233
|
+
// THE REFUSAL WAS RIGHT AND THE DEFECT WAS OURS, which is why this cell reads OUR
|
|
1234
|
+
// `package.json` instead of loosening the parser. Neither existing axis could see it: the
|
|
1235
|
+
// deterministic cells above drive a fake npm that emits clean JSON, and the LIVE gate
|
|
1236
|
+
// redirects the product remote to a local bare mirror through git's `insteadOf`
|
|
1237
|
+
// (`smoke-herdr-plugin-build-live.ts`) — a `file://` spec IS cloned, `.git` therefore
|
|
1238
|
+
// exists, and husky stays silent. The substitution removed the condition under test.
|
|
1239
|
+
const pkgScripts = (
|
|
1240
|
+
JSON.parse(fs.readFileSync(path.join(REPO, "package.json"), "utf8")) as { scripts: Record<string, string> }
|
|
1241
|
+
).scripts;
|
|
1242
|
+
const prepare = pkgScripts.prepare ?? "";
|
|
1243
|
+
const commands = prepare
|
|
1244
|
+
.split(/;|&&/)
|
|
1245
|
+
.map((part) =>
|
|
1246
|
+
part
|
|
1247
|
+
.trim()
|
|
1248
|
+
.replace(/\|\|\s*true$/, "")
|
|
1249
|
+
.trim(),
|
|
1250
|
+
)
|
|
1251
|
+
.filter((part) => part.length > 0);
|
|
1252
|
+
const polluting = commands.filter((command) => !/1>&2$/.test(command));
|
|
1253
|
+
ok(
|
|
1254
|
+
"[QK:HRB-PREPARE-STDOUT-CLEAN] every command in this package's `prepare` routes its STDOUT to stderr — npm " +
|
|
1255
|
+
"merges lifecycle stdout into the `npm pack --json` stream this rail parses, so one chatty script in our own " +
|
|
1256
|
+
"package turns a perfectly good artifact into `runtime-checkout-source-unavailable` on every clean host. " +
|
|
1257
|
+
"`2>/dev/null` is NOT this check and never was: husky writes its diagnostics to stdout, so silencing stderr " +
|
|
1258
|
+
"left the one stream that mattered wide open (measured 2026-09-17 against the real GitHub remote). Diagnostics " +
|
|
1259
|
+
`still reach the operator — they go to stderr, which npm does not parse (prepare=${JSON.stringify(prepare)} polluting=${JSON.stringify(polluting)})`,
|
|
1260
|
+
commands.length > 0 && polluting.length === 0,
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
console.log(`\ncheck-herdr-runtime-bootstrap: ${passed} assertions passed`);
|