bareloop 0.20.0 → 0.22.0
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/CHANGELOG.md +234 -0
- package/README.md +14 -0
- package/bareloop.context.md +331 -10
- package/bin/bareloop.mjs +24 -0
- package/package.json +7 -3
- package/src/authorjob.js +15 -1
- package/src/bundle.js +713 -0
- package/src/cli.js +472 -0
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/index.js +31 -1
- package/src/job.js +48 -4
- package/src/plan.js +5 -1
- package/src/planrun.js +212 -7
- package/src/ralph.js +40 -7
- package/src/reuse.js +4 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +24 -3
- package/src/validate.js +59 -0
- package/types/bundle.d.ts +199 -0
- package/types/cli.d.ts +19 -0
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/index.d.ts +4 -1
- package/types/planrun.d.ts +17 -2
- package/types/ralph.d.ts +19 -5
- package/types/reuse.d.ts +16 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +25 -2
- package/types/validate.d.ts +48 -0
package/src/validate.js
CHANGED
|
@@ -73,6 +73,65 @@ export function isNonEmptyString(v) {
|
|
|
73
73
|
return typeof v === 'string' && v.length > 0;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* PRD item 27/M2 close-bytes signature, orchestrator-flagged gap
|
|
78
|
+
* (2026-09-06): a close stage's `cmd` can name an addressable script TWO
|
|
79
|
+
* ways — `node <path> …` (M1's original scope) or a BARE directly-executable
|
|
80
|
+
* absolute path (a `.sh` wrapper, e.g. `jobs/aurora-testgen-cold.json`'s
|
|
81
|
+
* close, which `readCloseScripts` used to skip entirely, leaving both
|
|
82
|
+
* `close-absolute-path` and the sha256 signature blind to it — a "blind
|
|
83
|
+
* instrument" the audit named directly). This list is the shared, STATIC
|
|
84
|
+
* (no fs) shape test both `src/job.js` (which only needs to know WHETHER a
|
|
85
|
+
* stage names an addressable script, to decide whether `sha256` is
|
|
86
|
+
* demanded) and `src/close-integrity.js` (which resolves the token against
|
|
87
|
+
* a real cwd and reads it off disk) key off — one spelling, so the demand
|
|
88
|
+
* and the detector can never drift apart about which stages are in scope.
|
|
89
|
+
* A leading-interpreter cmd (`sh`/`bash`/`python`/`python3`/`npx` besides
|
|
90
|
+
* `node`) is included for the same reason `node` is: a close author may use
|
|
91
|
+
* any of them, and a scope that only recognized `node` would just move the
|
|
92
|
+
* blind spot rather than close it.
|
|
93
|
+
* @type {readonly string[]}
|
|
94
|
+
*/
|
|
95
|
+
export const CLOSE_SCRIPT_INTERPRETERS = Object.freeze(['node', 'sh', 'bash', 'python', 'python3', 'npx']);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The candidate script path TOKEN a close stage's `cmd` names, by SHAPE
|
|
99
|
+
* alone — no fs, no cwd resolution, so this is safe for `validateJob` (pure)
|
|
100
|
+
* to call directly. Two recognized forms, first match wins:
|
|
101
|
+
* - an INTERPRETER cmd: the token is the first argv[1..] element that is
|
|
102
|
+
* PATH-SHAPED (not a `-`-prefixed flag, and either contains `/` or ends
|
|
103
|
+
* in one of `.mjs .cjs .js .ts .sh .py`) — never just argv[1] outright.
|
|
104
|
+
* `node --enable-source-maps ./close/x.mjs stage` names `./close/x.mjs`,
|
|
105
|
+
* not `--enable-source-maps`; `python -m pytest` and `npx tsc --noEmit`
|
|
106
|
+
* name NOTHING (`-m`/`--noEmit` are flags, `pytest`/`tsc` are bare
|
|
107
|
+
* command names with no path shape) — fixed 2026-09-06 (orchestrator
|
|
108
|
+
* audit) after the earlier bare-argv[1] version demanded a sha256 for a
|
|
109
|
+
* token that named no file, making such a spec unsignable forever (the
|
|
110
|
+
* sign helper reads nothing, the validator reds permanently);
|
|
111
|
+
* - a bare cmd whose argv[0] ITSELF is an absolute path (no interpreter
|
|
112
|
+
* prefix — a `.sh`/other directly-executable close script) → the token
|
|
113
|
+
* is argv[0].
|
|
114
|
+
* Anything else — a relative bare executable (`true`, `pytest`), an
|
|
115
|
+
* interpreter with no path-shaped argument (`python -m pytest`, `npx tsc
|
|
116
|
+
* --noEmit`), a non-string/empty cmd — names no addressable file under this
|
|
117
|
+
* scheme and returns null; those stages carry no sha256 demand and are
|
|
118
|
+
* invisible to the byte/path detectors, exactly as they always were (most
|
|
119
|
+
* test-fixture closes use exactly this shape on purpose, to exercise
|
|
120
|
+
* runtime behaviour with no real script file at all).
|
|
121
|
+
* @param {unknown} cmd
|
|
122
|
+
* @returns {string|null}
|
|
123
|
+
*/
|
|
124
|
+
export function closeScriptCandidateToken(cmd) {
|
|
125
|
+
if (typeof cmd !== 'string') return null;
|
|
126
|
+
const parts = cmd.trim().split(/\s+/);
|
|
127
|
+
if (!parts[0]) return null;
|
|
128
|
+
if (CLOSE_SCRIPT_INTERPRETERS.includes(parts[0])) {
|
|
129
|
+
const pathShaped = (/** @type {string} */ tok) => !tok.startsWith('-') && (tok.includes('/') || /\.(?:mjs|cjs|js|ts|sh|py)$/.test(tok));
|
|
130
|
+
return parts.slice(1).find(pathShaped) ?? null;
|
|
131
|
+
}
|
|
132
|
+
return parts[0].startsWith('/') ? parts[0] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
76
135
|
// Secrets never enter the tree/spine/configs (hard line) — BOTH config
|
|
77
136
|
// documents get the same sweep: the agent-authored workflow config is the
|
|
78
137
|
// riskier entry point (machine-written), the operator's job spec the other.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recompute the bundle hash from what is ACTUALLY on disk right now (never
|
|
3
|
+
* trusts the stored manifest). Throws only if `dir` itself does not exist —
|
|
4
|
+
* every other absence (a missing spec.json, an empty close/) folds into the
|
|
5
|
+
* hash as an omitted file, which is exactly what makes a swapped/deleted
|
|
6
|
+
* script change the hash.
|
|
7
|
+
* @param {string} dir bundle directory
|
|
8
|
+
* @returns {string} sha256 hex
|
|
9
|
+
*/
|
|
10
|
+
export function bundleHash(dir: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* EXPORT — mints a bundle directory from a signed job spec (`docs/product/
|
|
13
|
+
* EXPORT-BUILD.md`, "The bundle"). Every check below runs BEFORE anything
|
|
14
|
+
* touches disk (typed reds, nothing written): a partially-written bundle
|
|
15
|
+
* would be worse than no bundle, since `readBundle`'s tamper check has
|
|
16
|
+
* nothing trustworthy to compare against yet.
|
|
17
|
+
*
|
|
18
|
+
* Refusals, each a distinct red code:
|
|
19
|
+
* - `no-bridge-at-hash` — neither of the job's two possible bridge names —
|
|
20
|
+
* `spec.job` itself, or its SHAPE FORK `shapeForkName(spec.job,
|
|
21
|
+
* closeStagesOf(spec))` (the name `writeGreenRow`/`writeRunGreenRow` in
|
|
22
|
+
* `src/reuse.js` mint whenever a run's close-stage set differs from the
|
|
23
|
+
* base bridge's `closeStageNames` — same derivation, imported rather than
|
|
24
|
+
* re-spelled, so the two names cannot drift) — carries a recorded specHash
|
|
25
|
+
* (bridge-level or on some version) equal to `jobSpecHash(spec)`: nothing
|
|
26
|
+
* has ever greened THIS exact spec, so there is nothing proven to ship.
|
|
27
|
+
* Every candidate bridge that DOES exist (base and/or fork) still ships
|
|
28
|
+
* into `bridges/` — the spec says "the job's WHOLE registry history
|
|
29
|
+
* (greens and reds)", not just the one that matched.
|
|
30
|
+
* - `close-cmd-unrelocatable` — a `close[].cmd` is not `node <abs .mjs> …`.
|
|
31
|
+
* - `close-script-missing` — a cmd names a script `closeScripts` does not
|
|
32
|
+
* carry the source for.
|
|
33
|
+
* - `close-script-collision` — two different script paths share a basename;
|
|
34
|
+
* copying both into one flat `close/` directory would silently clobber one.
|
|
35
|
+
* - `close-import-unexported` — a close script imports a name from
|
|
36
|
+
* bareloop's own src that `src/index.js` does not export.
|
|
37
|
+
* - `close-import-unparsed` — a close script's import is a shape this
|
|
38
|
+
* module cannot verify at all (default import, `* as ns`, mixed
|
|
39
|
+
* default+named/namespace, a bare side-effect import, or a dynamic
|
|
40
|
+
* `import()`), or is a relative import that does not point into
|
|
41
|
+
* `src/` (a sibling file `close/` never ships, so it would be missing
|
|
42
|
+
* from the bundle). Fails safe rather than silently accepting what it
|
|
43
|
+
* cannot read.
|
|
44
|
+
* - `close-absolute-path` — a close script's source bakes in a string
|
|
45
|
+
* literal that is an absolute POSIX path which exists on the exporting
|
|
46
|
+
* machine and is outside the system-prefix allow-list (see
|
|
47
|
+
* `absolutePathLiteralsOf`): a close judges the cwd the runner gives it,
|
|
48
|
+
* never a path baked into the script — F8/F129, the live defect this
|
|
49
|
+
* guard would have refused at export time before any run.
|
|
50
|
+
* - `outdir-not-empty` — `outDir` exists and already holds files.
|
|
51
|
+
* - `registry-unreadable` — `registryDir` itself could not be read
|
|
52
|
+
* (bubbled from `loadRegistry`).
|
|
53
|
+
* @param {{ spec: any, closeScripts: Record<string, string>, registryDir: string, outDir: string, bareloopVersion: string }} o
|
|
54
|
+
* @returns {{ ok: boolean, reds: Red[], dir: string|null, bundleHash: string|null, manifest: any|null }}
|
|
55
|
+
*/
|
|
56
|
+
export function exportBundle({ spec, closeScripts, registryDir, outDir, bareloopVersion }: {
|
|
57
|
+
spec: any;
|
|
58
|
+
closeScripts: Record<string, string>;
|
|
59
|
+
registryDir: string;
|
|
60
|
+
outDir: string;
|
|
61
|
+
bareloopVersion: string;
|
|
62
|
+
}): {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
reds: Red[];
|
|
65
|
+
dir: string | null;
|
|
66
|
+
bundleHash: string | null;
|
|
67
|
+
manifest: any | null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* READ a bundle directory back: the manifest/spec, tamper-checked against
|
|
71
|
+
* what is actually on disk, plus the record-only bridges/blessing/history a
|
|
72
|
+
* runner needs beside them. Never throws — every failure is a typed red.
|
|
73
|
+
*
|
|
74
|
+
* `bundle-tampered` (N4) is a RED, never a warning, and runs whenever both a
|
|
75
|
+
* manifest and a spec could be read at all — a swapped close script must be
|
|
76
|
+
* caught here, before any caller reaches `runJob`.
|
|
77
|
+
* @param {string} dir bundle directory
|
|
78
|
+
* @returns {{ ok: boolean, reds: Red[], spec: any, manifest: any, bridges: any[], blessing: any, history: any[] }}
|
|
79
|
+
*/
|
|
80
|
+
export function readBundle(dir: string): {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
reds: Red[];
|
|
83
|
+
spec: any;
|
|
84
|
+
manifest: any;
|
|
85
|
+
bridges: any[];
|
|
86
|
+
blessing: any;
|
|
87
|
+
history: any[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The IN-MEMORY `$BARELOOP_BUNDLE` substitution (the frozen spec's "two
|
|
91
|
+
* facts": the runner never touches the spec on disk, it substitutes the
|
|
92
|
+
* absolute bundle directory into a copy before `runJob`, and signs THAT
|
|
93
|
+
* substituted spec's hash — not the on-disk one). `approveHash` is the value
|
|
94
|
+
* an `--approve` flag / a `history.jsonl` row must record.
|
|
95
|
+
* @param {{ spec: any }} bundle a `readBundle` result (or anything carrying `.spec`)
|
|
96
|
+
* @param {string} bundleDir absolute or relative bundle directory
|
|
97
|
+
* @returns {{ spec: any, approveHash: string }}
|
|
98
|
+
*/
|
|
99
|
+
export function resolveBundleSpec(bundle: {
|
|
100
|
+
spec: any;
|
|
101
|
+
}, bundleDir: string): {
|
|
102
|
+
spec: any;
|
|
103
|
+
approveHash: string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* TIGHTEN-ONLY envelope check (hamr Q8: raising the ceiling is a re-export,
|
|
107
|
+
* never a runner flag). Each GIVEN number must be ≤ the bundle spec's own —
|
|
108
|
+
* an absent field in the envelope trivially passes (nothing was asked to
|
|
109
|
+
* tighten), and a spec with no ceiling of its own for that field (only
|
|
110
|
+
* `maxWallMs` is ever absent — `budgetUsd` is required) has nothing to
|
|
111
|
+
* widen, so a given number passes there too.
|
|
112
|
+
* @param {any} spec the (already `$BARELOOP_BUNDLE`-resolved) bundle spec
|
|
113
|
+
* @param {{ budgetUsd?: number, maxWallMs?: number }} envelope
|
|
114
|
+
* @returns {{ ok: boolean, reds: Red[] }}
|
|
115
|
+
*/
|
|
116
|
+
export function checkEnvelope(spec: any, envelope?: {
|
|
117
|
+
budgetUsd?: number;
|
|
118
|
+
maxWallMs?: number;
|
|
119
|
+
}): {
|
|
120
|
+
ok: boolean;
|
|
121
|
+
reds: Red[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Write `blessing.json`: the FIRST GREEN RUN on the importer's own machine
|
|
125
|
+
* signs the bundle as trustworthy (hamr Q2), so later runs need no
|
|
126
|
+
* `--approve` (hamr: "no-resign"). Overwrites any prior blessing — there is
|
|
127
|
+
* only ever one, for the bundle's current `bundleHash`.
|
|
128
|
+
* @param {string} dir bundle directory
|
|
129
|
+
* @param {{ bundleHash: string, runid: string, outcome: string, host?: string, blessedAt?: string }} record
|
|
130
|
+
* @returns {any} the blessing object written
|
|
131
|
+
*/
|
|
132
|
+
export function bless(dir: string, record: {
|
|
133
|
+
bundleHash: string;
|
|
134
|
+
runid: string;
|
|
135
|
+
outcome: string;
|
|
136
|
+
host?: string;
|
|
137
|
+
blessedAt?: string;
|
|
138
|
+
}): any;
|
|
139
|
+
/**
|
|
140
|
+
* Read `blessing.json` back and judge it against the bundle's CURRENT
|
|
141
|
+
* manifest hash. Three outcomes, never conflated: no blessing at all
|
|
142
|
+
* (`unblessed: true` — the bundle has simply never greened here, not a red);
|
|
143
|
+
* a blessing whose hash no longer matches the manifest (`blessing-stale` —
|
|
144
|
+
* the bundle was re-exported/edited since the run that blessed it); a match.
|
|
145
|
+
* @param {{ manifest: any, blessing: any }} bundle a `readBundle` result
|
|
146
|
+
* @returns {{ ok: boolean, unblessed: boolean, reds: Red[] }}
|
|
147
|
+
*/
|
|
148
|
+
export function verifyBlessing(bundle: {
|
|
149
|
+
manifest: any;
|
|
150
|
+
blessing: any;
|
|
151
|
+
}): {
|
|
152
|
+
ok: boolean;
|
|
153
|
+
unblessed: boolean;
|
|
154
|
+
reds: Red[];
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* F128 preflight — can this bundle's close scripts actually resolve `import
|
|
158
|
+
* ... from 'bareloop'` at runtime? The bundle's own `package.json` declares
|
|
159
|
+
* `bareloop` as ITS dependency (`npm install` must be run INSIDE the bundle
|
|
160
|
+
* directory — validation step 2 was corrected 2026-09-06, see the dated
|
|
161
|
+
* addendum in `docs/product/EXPORT-BUILD.md`); nothing installs it for the
|
|
162
|
+
* bundle as a side effect of the bundle being installed as someone else's
|
|
163
|
+
* dependency. Left unchecked, a missing `node_modules` crashes every close
|
|
164
|
+
* stage deep inside the close-first precheck with a bare
|
|
165
|
+
* `ERR_MODULE_NOT_FOUND`, which surfaces as a generic `close-red` with no
|
|
166
|
+
* cure line (the live defect this fixes) — this check runs first and fails
|
|
167
|
+
* fast with the exact command to run.
|
|
168
|
+
*
|
|
169
|
+
* Resolves the bare specifier `'bareloop'` exactly as a close script would,
|
|
170
|
+
* from inside the bundle's own `close/` directory (the probe path itself
|
|
171
|
+
* need not exist — `createRequire`/`resolve` only use it to anchor the
|
|
172
|
+
* `node_modules` walk). Going through `require.resolve` also walks
|
|
173
|
+
* `bareloop`'s own `exports` map (the package root, `"."` -> `./src/
|
|
174
|
+
* index.js`), so a bare successful resolve already proves a real,
|
|
175
|
+
* exports-map-shaped `bareloop` package is reachable from THIS bundle's own
|
|
176
|
+
* dependency chain — not some unrelated global install. Any resolution
|
|
177
|
+
* counts (a direct `node_modules/bareloop`, a symlink into it, or a parent
|
|
178
|
+
* `node_modules/bareloop` up the directory chain) — it only has to resolve.
|
|
179
|
+
* @param {string} bundleDir
|
|
180
|
+
* @returns {{ ok: boolean, reds: Red[] }}
|
|
181
|
+
*/
|
|
182
|
+
export function checkBundleDeps(bundleDir: string): {
|
|
183
|
+
ok: boolean;
|
|
184
|
+
reds: Red[];
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Append one row to `history.jsonl` — one `bareloop run` on this machine per
|
|
188
|
+
* line. Same convention as `spine.js`/`ledger.js`: `appendFileSync` opens
|
|
189
|
+
* with O_APPEND, so one line under PIPE_BUF is written atomically with
|
|
190
|
+
* respect to other appenders; there is no read-modify-write here to race.
|
|
191
|
+
* @param {string} dir bundle directory
|
|
192
|
+
* @param {any} row
|
|
193
|
+
*/
|
|
194
|
+
export function appendHistory(dir: string, row: any): void;
|
|
195
|
+
export type Red = {
|
|
196
|
+
code: string;
|
|
197
|
+
path: string;
|
|
198
|
+
detail?: string;
|
|
199
|
+
};
|
package/types/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one entry point. Never calls `process.exit` — always returns an exit
|
|
3
|
+
* code, so `bin/bareloop.mjs` can set `process.exitCode` and let node flush
|
|
4
|
+
* queued stdout on its own (F-something: `process.exit()` can discard it).
|
|
5
|
+
* @param {string[]} argv
|
|
6
|
+
* @param {{ env?: Record<string,string|undefined>, stdout?: any, stderr?: any, cwd?: string, provider?: any, providerFor?: any, judgeProvider?: any, now?: () => number, stdin?: any }} deps
|
|
7
|
+
* @returns {Promise<number>}
|
|
8
|
+
*/
|
|
9
|
+
export function main(argv: string[], deps?: {
|
|
10
|
+
env?: Record<string, string | undefined>;
|
|
11
|
+
stdout?: any;
|
|
12
|
+
stderr?: any;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
provider?: any;
|
|
15
|
+
providerFor?: any;
|
|
16
|
+
judgeProvider?: any;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
stdin?: any;
|
|
19
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex sha256 of a close script's TEXT, exactly the way `src/bundle.js`'s own
|
|
3
|
+
* `sha256(source)` hashes a close script's source before bundling (a JS
|
|
4
|
+
* source file is always valid UTF-8, so hashing the decoded string and
|
|
5
|
+
* hashing the raw file bytes agree) — ONE formula, so a signature minted here
|
|
6
|
+
* and a mismatch read at export time can never disagree about what "the
|
|
7
|
+
* bytes" means.
|
|
8
|
+
* @param {string} text
|
|
9
|
+
* @returns {string} sha256 hex
|
|
10
|
+
*/
|
|
11
|
+
export function hashCloseScriptBytes(text: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* F8/F129 — a close judges the cwd the runner gives it, never a path baked
|
|
14
|
+
* into the script itself. This scans every quoted string literal in a close
|
|
15
|
+
* script's source for an absolute POSIX path (`/…`) that EXISTS on this
|
|
16
|
+
* machine right now and is not under an allow-listed system prefix.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately monotone and simple, not clever, with named limits: a
|
|
19
|
+
* nonexistent absolute-looking string is treated as a NAME, not a path —
|
|
20
|
+
* `existsSync` is the only oracle this has for "load-bearing", so it is not
|
|
21
|
+
* flagged; a path built by string concatenation (never one whole literal)
|
|
22
|
+
* is invisible to this scan; and a literal that only exists inside a `//` or
|
|
23
|
+
* `/* *\/` comment is naturally excluded because a comment carries no quote
|
|
24
|
+
* characters of its own around the path text.
|
|
25
|
+
* @param {string} source close script text
|
|
26
|
+
* @returns {string[]} the offending literal path values, in source order
|
|
27
|
+
*/
|
|
28
|
+
export function absolutePathLiteralsOf(source: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Resolve every `close[].cmd` (or `closeDecl`'s equivalent, via
|
|
31
|
+
* `closeStagesOf`) that names an addressable script (`closeScriptCandidateToken`'s
|
|
32
|
+
* scope: an interpreter cmd or a bare absolute executable) to its absolute
|
|
33
|
+
* path and bytes off disk. The spec's OWN `cmd` path is the address and is never
|
|
34
|
+
* itself judged — only the named script's CONTENT is read here. A relative
|
|
35
|
+
* path resolves against `cwd` (the caller passes the run's own workdir or
|
|
36
|
+
* the spec file's directory, matching how each caller already resolves
|
|
37
|
+
* paths elsewhere). Never throws: a missing/unreadable script reports
|
|
38
|
+
* `bytes: null` so a caller can red it distinctly from a tampered one.
|
|
39
|
+
* `sha256` (M2, additive): the stage's OWN signed field, verbatim, or `null`
|
|
40
|
+
* when the stage carries none — the field this rung's checker compares
|
|
41
|
+
* against `hashCloseScriptBytes(bytes)`. M1's callers read `.stage`/`.path`/
|
|
42
|
+
* `.bytes` only, so this addition is behaviour-preserving for them.
|
|
43
|
+
* @param {any} spec resolved job spec (or anything `closeStagesOf` accepts)
|
|
44
|
+
* @param {string} cwd
|
|
45
|
+
* @returns {{stage: string, path: string, bytes: string|null, sha256: string|null}[]}
|
|
46
|
+
*/
|
|
47
|
+
export function readCloseScripts(spec: any, cwd: string): {
|
|
48
|
+
stage: string;
|
|
49
|
+
path: string;
|
|
50
|
+
bytes: string | null;
|
|
51
|
+
sha256: string | null;
|
|
52
|
+
}[];
|
|
53
|
+
/**
|
|
54
|
+
* The $0 run-start integrity check (PRD item 27(c)): red `close-absolute-path`
|
|
55
|
+
* for EVERY job whose close script bakes in an absolute path literal that
|
|
56
|
+
* exists on disk — the same rule `src/bundle.js`'s `exportBundle` already
|
|
57
|
+
* applies at export time, now applied before the close-first precheck and
|
|
58
|
+
* before any provider call, for every run, not only exported bundles. A
|
|
59
|
+
* script this reads couldn't read (missing/unreadable) is not reported here
|
|
60
|
+
* — that is `broken-close`'s job once the close actually tries to run it.
|
|
61
|
+
* @param {any} spec resolved job spec
|
|
62
|
+
* @param {string} cwd
|
|
63
|
+
* @returns {{ ok: true } | { ok: false, reds: {stage: string, path: string, literal: string}[] }}
|
|
64
|
+
*/
|
|
65
|
+
export function checkCloseAbsolutePaths(spec: any, cwd: string): {
|
|
66
|
+
ok: true;
|
|
67
|
+
} | {
|
|
68
|
+
ok: false;
|
|
69
|
+
reds: {
|
|
70
|
+
stage: string;
|
|
71
|
+
path: string;
|
|
72
|
+
literal: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* M2's run-start integrity check (PRD item 27, `close-tampered`): every
|
|
77
|
+
* signed `close[].sha256` must match the script's bytes off disk, RIGHT NOW —
|
|
78
|
+
* checked before the close-first precheck and before any provider call,
|
|
79
|
+
* exactly where `checkCloseAbsolutePaths` already runs (the two are always
|
|
80
|
+
* called together at that seam). A script this cannot read reads `actual:
|
|
81
|
+
* null` — distinct from a byte mismatch, but still a red: an unreadable
|
|
82
|
+
* signed script is exactly as untrustworthy as a tampered one, and silently
|
|
83
|
+
* treating "gone" as "fine" would be the M1 F129 hazard in a new coat.
|
|
84
|
+
* @param {any} spec resolved job spec
|
|
85
|
+
* @param {string} cwd
|
|
86
|
+
* @returns {{ ok: true } | { ok: false, reds: ByteRed[] }}
|
|
87
|
+
*/
|
|
88
|
+
export function checkCloseByteSignature(spec: any, cwd: string): {
|
|
89
|
+
ok: true;
|
|
90
|
+
} | {
|
|
91
|
+
ok: false;
|
|
92
|
+
reds: ByteRed[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* PRD item 27/M3, Part B — "the library `runJob` takes `closeDir` and refuses
|
|
96
|
+
* at $0 if a spec's close needs it and none is given." A close script's
|
|
97
|
+
* NEED is read the same way its bytes already are (`readCloseScripts`): a
|
|
98
|
+
* script whose source mentions `BARELOOP_CLOSE_DIR` reads that variable at
|
|
99
|
+
* run time, so a run with no `closeDir` handed to it would run that script
|
|
100
|
+
* against `undefined` — silently, since `env.BARELOOP_CLOSE_DIR` would just
|
|
101
|
+
* be absent from the child's environment rather than throwing. Refusing here,
|
|
102
|
+
* at the same $0 run-start seam as the absolute-path/byte-signature checks,
|
|
103
|
+
* turns that silent gap into a named red before any tokens spend, rather than
|
|
104
|
+
* leaving it to whatever the script's own author remembered to check for.
|
|
105
|
+
*
|
|
106
|
+
* A script that never mentions the variable carries no demand — most
|
|
107
|
+
* predicate-only fixture closes (`true`, `pytest`) have no books of their own
|
|
108
|
+
* to keep, and requiring a `closeDir` for them would be a needless refusal.
|
|
109
|
+
* @param {any} spec resolved job spec
|
|
110
|
+
* @param {string} cwd
|
|
111
|
+
* @param {string|null|undefined} closeDir the value the runner is about to hand
|
|
112
|
+
* the close as `BARELOOP_CLOSE_DIR` (or none)
|
|
113
|
+
* @returns {{ ok: true } | { ok: false, reds: {stage: string, path: string}[] }}
|
|
114
|
+
*/
|
|
115
|
+
export function checkCloseDirRequired(spec: any, cwd: string, closeDir: string | null | undefined): {
|
|
116
|
+
ok: true;
|
|
117
|
+
} | {
|
|
118
|
+
ok: false;
|
|
119
|
+
reds: {
|
|
120
|
+
stage: string;
|
|
121
|
+
path: string;
|
|
122
|
+
}[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The re-verify that runs before EVERY close run, precheck included (PRD
|
|
126
|
+
* item 27, M2 §2) — scoped to the STAGES about to actually run rather than
|
|
127
|
+
* the whole spec, because that is the true blast radius of "was this close
|
|
128
|
+
* script's content tampered with since it was signed" for this particular
|
|
129
|
+
* invocation. Same comparison, same red shape, as `checkCloseByteSignature`;
|
|
130
|
+
* kept as a separate export because its caller (`runCloseStages` in
|
|
131
|
+
* `src/planrun.js`) already holds a stage array, not a spec.
|
|
132
|
+
* @param {any[]} stages a close stage chain (the shape `runStages` takes)
|
|
133
|
+
* @param {string} cwd
|
|
134
|
+
* @returns {{ ok: true } | { ok: false, reds: ByteRed[] }}
|
|
135
|
+
*/
|
|
136
|
+
export function checkStageByteSignature(stages: any[], cwd: string): {
|
|
137
|
+
ok: true;
|
|
138
|
+
} | {
|
|
139
|
+
ok: false;
|
|
140
|
+
reds: ByteRed[];
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* M2's minting helper (PRD item 27 §3) — fills `close[].sha256` from disk for
|
|
144
|
+
* every addressable-script stage in `spec.close` (`closeScriptCandidateToken`'s
|
|
145
|
+
* scope: an interpreter cmd or a bare absolute executable, e.g. a `.sh`
|
|
146
|
+
* wrapper). Pure: never writes anything itself (the CLI wrapper,
|
|
147
|
+
* `scripts/sign-close.mjs`, gates on `--write`) and never throws on a
|
|
148
|
+
* missing/unreadable script — it names the miss in `missing` and leaves
|
|
149
|
+
* that stage's `sha256` untouched, so a caller can refuse to sign (and
|
|
150
|
+
* refuse to report a hash it never computed) a spec it could not fully
|
|
151
|
+
* hash. A stage outside this rung's scope (a relative bare executable, a
|
|
152
|
+
* declared-kind stage) passes through unchanged and is never listed as
|
|
153
|
+
* missing — it was never addressable to begin with (same scope
|
|
154
|
+
* `readCloseScripts`/`checkCloseAbsolutePaths` already draw).
|
|
155
|
+
*
|
|
156
|
+
* Only ever operates on `spec.close` (the command-close array) — a
|
|
157
|
+
* `closeDecl` spec has no script path to sign and `signCloseScripts` reports
|
|
158
|
+
* `ok:false` for one rather than silently doing nothing to it.
|
|
159
|
+
* @param {any} spec resolved job spec (parsed `jobs/<x>.json`)
|
|
160
|
+
* @param {string} cwd base for resolving a RELATIVE script path (jobs in this
|
|
161
|
+
* repo only ever use absolute `cmd` paths; relative resolution is here for
|
|
162
|
+
* parity with `readCloseScripts` and any future relative-path spec)
|
|
163
|
+
* @returns {{ ok: boolean, spec: any, changes: {stage: string, path: string, old: string|null, new: string}[], missing: {stage: string, path: string}[] }}
|
|
164
|
+
*/
|
|
165
|
+
export function signCloseScripts(spec: any, cwd: string): {
|
|
166
|
+
ok: boolean;
|
|
167
|
+
spec: any;
|
|
168
|
+
changes: {
|
|
169
|
+
stage: string;
|
|
170
|
+
path: string;
|
|
171
|
+
old: string | null;
|
|
172
|
+
new: string;
|
|
173
|
+
}[];
|
|
174
|
+
missing: {
|
|
175
|
+
stage: string;
|
|
176
|
+
path: string;
|
|
177
|
+
}[];
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* A close script is entitled to name a real system path (`/usr/bin/env`,
|
|
181
|
+
* `/etc/hosts`, …) without that being the F8/F129 hazard — only a path a
|
|
182
|
+
* runner would actually relocate (a patient checkout, a scratch dir) is.
|
|
183
|
+
* @type {string[]}
|
|
184
|
+
*/
|
|
185
|
+
export const SYSTEM_PATH_PREFIXES: string[];
|
|
186
|
+
/** the exact env var name a close script reads for its own books directory
|
|
187
|
+
* (Part B, PRD item 27/M3) — named once so the detector below and the runner
|
|
188
|
+
* that sets it (`src/ralph.js`'s `runClose`) can never drift into two
|
|
189
|
+
* spellings of the same contract. */
|
|
190
|
+
export const CLOSE_DIR_ENV_VAR: "BARELOOP_CLOSE_DIR";
|
|
191
|
+
export type ByteRed = {
|
|
192
|
+
stage: string;
|
|
193
|
+
path: string;
|
|
194
|
+
expected: string;
|
|
195
|
+
actual: string | null;
|
|
196
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The $0 timing preflight (PRD item 27, M3 §2): run every close STAGE once,
|
|
3
|
+
* ignoring verdicts — first-red-wins stays for the real verdict precheck
|
|
4
|
+
* (`src/planrun.js`'s `check-preflight`); this is a separate, sequential pass
|
|
5
|
+
* (F131/F68: never two closes in flight against one tree) whose only purpose
|
|
6
|
+
* is TIMING. A declared-close stage (no `.cmd`, a kind executor) has nothing
|
|
7
|
+
* a command runner can spawn and is skipped — it contributes no reading, not
|
|
8
|
+
* a zero one (a silent zero would understate the real close's later cost).
|
|
9
|
+
*
|
|
10
|
+
* @param {any[]} stages a close stage chain (`runStages`'s own shape)
|
|
11
|
+
* @param {{cwd?: string, redact?: (s: string) => string, ceilingMs?: number}} [opts]
|
|
12
|
+
* `ceilingMs` (test seam only — never wired to any spec/runner surface):
|
|
13
|
+
* overrides `TIMING_PREFLIGHT_CEILING_MS` so a hang-detection test does not
|
|
14
|
+
* have to wait 10 real minutes. Production callers never pass it.
|
|
15
|
+
* @returns {Promise<{
|
|
16
|
+
* perStage: {name: string, ms: number, exitCode: number|null, timedOut: boolean}[],
|
|
17
|
+
* slowestMs: number, slowestName: string|null, anyTimedOut: boolean,
|
|
18
|
+
* }>}
|
|
19
|
+
*/
|
|
20
|
+
export function timeCloseStages(stages: any[], { cwd, redact, ceilingMs }?: {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
redact?: (s: string) => string;
|
|
23
|
+
ceilingMs?: number;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
perStage: {
|
|
26
|
+
name: string;
|
|
27
|
+
ms: number;
|
|
28
|
+
exitCode: number | null;
|
|
29
|
+
timedOut: boolean;
|
|
30
|
+
}[];
|
|
31
|
+
slowestMs: number;
|
|
32
|
+
slowestName: string | null;
|
|
33
|
+
anyTimedOut: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* The ceiling formula (PRD item 27, M3): `max(FLOOR, K × slowest measured
|
|
37
|
+
* stage)`, unless an explicit override is given — the override is the
|
|
38
|
+
* OPERATOR'S number (the rates-passthrough shape, F113: guesstimate + loud
|
|
39
|
+
* sign + customer override), so it may legally sit ABOVE or BELOW the
|
|
40
|
+
* autoset estimate; this is not tighten-only.
|
|
41
|
+
* @param {{slowestMs: number, overrideMs?: number|null}} args
|
|
42
|
+
* @returns {{ ceilingMs: number, source: 'estimated'|'override' }}
|
|
43
|
+
*/
|
|
44
|
+
export function computeCloseTimeoutCeiling({ slowestMs, overrideMs }: {
|
|
45
|
+
slowestMs: number;
|
|
46
|
+
overrideMs?: number | null;
|
|
47
|
+
}): {
|
|
48
|
+
ceilingMs: number;
|
|
49
|
+
source: "estimated" | "override";
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* ONE resolver for "what per-stage close timeout does this job get", shared
|
|
53
|
+
* by `src/planrun.js` (the in-run path) and `scripts/run-u.mjs` (which must
|
|
54
|
+
* know the answer BEFORE it spawns the outside watchdog — F67 — so the
|
|
55
|
+
* watchdog's own stale/grace windows are sized off the real ceiling rather
|
|
56
|
+
* than a hardcoded guess). Two callers computing this independently is
|
|
57
|
+
* exactly the class of drift this whole rung exists to close (F129's own
|
|
58
|
+
* lesson, one level up): this function is the only place the precedence is
|
|
59
|
+
* spelled out.
|
|
60
|
+
*
|
|
61
|
+
* Precedence: a signed spec override (`job.closeTimeoutMs`) wins outright,
|
|
62
|
+
* with NO timing pass run (the pass would waste real machine time and, on a
|
|
63
|
+
* deliberately slow/hung stage, block for up to `TIMING_PREFLIGHT_CEILING_MS`
|
|
64
|
+
* for no purpose). Absent that, every stage is timed once (F131's autoset
|
|
65
|
+
* design) and the ceiling is `max(FLOOR, K × slowest)`.
|
|
66
|
+
* @param {{job: any, stages: any[], cwd: string, redact?: (s: string) => string, ceilingMs?: number}} args
|
|
67
|
+
* `ceilingMs` — see `timeCloseStages`; test seam only.
|
|
68
|
+
* @returns {Promise<{
|
|
69
|
+
* closeTimeoutMs: number|null, source: 'estimated'|'override',
|
|
70
|
+
* timing: Awaited<ReturnType<typeof timeCloseStages>>|null, timedOut: boolean,
|
|
71
|
+
* }>} `closeTimeoutMs: null, timedOut: true` means a stage never finished the
|
|
72
|
+
* timing pass — the caller reds `close-timing-red` rather than trusting any
|
|
73
|
+
* number here.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveCloseTimeoutMs({ job, stages, cwd, redact, ceilingMs }: {
|
|
76
|
+
job: any;
|
|
77
|
+
stages: any[];
|
|
78
|
+
cwd: string;
|
|
79
|
+
redact?: (s: string) => string;
|
|
80
|
+
ceilingMs?: number;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
closeTimeoutMs: number | null;
|
|
83
|
+
source: "estimated" | "override";
|
|
84
|
+
timing: Awaited<ReturnType<typeof timeCloseStages>> | null;
|
|
85
|
+
timedOut: boolean;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* The printed banner (build spec M3 §2's exact wording) — ONE spelling, so
|
|
89
|
+
* the run-u tail and the bundle CLI can never drift into two announcements of
|
|
90
|
+
* the same number. `source` accepts 'explicit' too: a caller-passed
|
|
91
|
+
* `closeTimeoutMs` runtime option (the pre-M3 shell/test knob, kept for
|
|
92
|
+
* backward compatibility and test control — never the signed spec field) is
|
|
93
|
+
* a real override but not a SIGNED one, so it earns its own honest word
|
|
94
|
+
* rather than being folded into 'signed override'.
|
|
95
|
+
* @param {{ceilingMs: number, source: 'estimated'|'override'|'explicit', slowestMs?: number, slowestName?: string|null}} args
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function closeTimeoutBanner({ ceilingMs, source, slowestMs, slowestName }: {
|
|
99
|
+
ceilingMs: number;
|
|
100
|
+
source: "estimated" | "override" | "explicit";
|
|
101
|
+
slowestMs?: number;
|
|
102
|
+
slowestName?: string | null;
|
|
103
|
+
}): string;
|
|
104
|
+
/** hamr, 2026-09-07: the floor, verbatim. Also the value `runClose`'s own
|
|
105
|
+
* un-autoset default has always used — kept as its own named constant here
|
|
106
|
+
* (not re-exported from ralph.js) because the two are different CLAIMS: one is
|
|
107
|
+
* "what runClose does if nobody says otherwise", the other is "the arbiter's
|
|
108
|
+
* chosen floor for autoset" — they happen to be the same number today, and a
|
|
109
|
+
* future change to either must not silently move the other. */
|
|
110
|
+
export const CLOSE_TIMEOUT_FLOOR_MS: 120000;
|
|
111
|
+
/** hamr, 2026-09-07, verbatim: "K = 5 ('2 and 5 are fine')." */
|
|
112
|
+
export const CLOSE_TIMEOUT_K: 5;
|
|
113
|
+
/** `runClose`'s own shipped default when nobody passes a timeout at all
|
|
114
|
+
* (`src/ralph.js:221`) — the ceiling the $0 timing PASS ITSELF runs each
|
|
115
|
+
* stage under is a multiple of this, never of the floor above (the two are
|
|
116
|
+
* the same number today; named separately on purpose, see the constant
|
|
117
|
+
* above). */
|
|
118
|
+
export const LIBRARY_DEFAULT_CLOSE_TIMEOUT_MS: 120000;
|
|
119
|
+
/**
|
|
120
|
+
* The generous PROVISIONAL ceiling the timing preflight runs every stage
|
|
121
|
+
* under, so "a genuinely hung stage still ends" (build spec, M3 §2) rather
|
|
122
|
+
* than blocking the timing pass forever. Deliberately the library default × K
|
|
123
|
+
* (600s) — NOT the autoset/override ceiling this module computes, which is
|
|
124
|
+
* only known after the pass completes.
|
|
125
|
+
*/
|
|
126
|
+
export const TIMING_PREFLIGHT_CEILING_MS: number;
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { makeSpine } from "./spine.js";
|
|
2
2
|
export { runPlan } from "./planrun.js";
|
|
3
|
+
export { main as cliMain } from "./cli.js";
|
|
3
4
|
export { ralph, runClose, runStages, CLOSE_FAULTS } from "./ralph.js";
|
|
4
5
|
export { globToPrefix, scanSecrets } from "./validate.js";
|
|
5
6
|
export { validateJob, jobSpecHash, checkApproval, checkMenu, resolveWorkerModel, CLOSE_TYPES, CLASS_BY_CLOSE, GOLD_COMPARE, CADENCE_UNITS, PROVIDERS, CONDITION_KEYS, TOOL_MENU, LOCKED_TOOLS, STORE_VERBS, VERDICT_TYPES, LOCKED_VERDICTS } from "./job.js";
|
|
@@ -8,7 +9,7 @@ export { snapshotScope, evalExits } from "./exits.js";
|
|
|
8
9
|
export { runJob, ACCOUNTED_ROUND_TYPES } from "./run.js";
|
|
9
10
|
export { BRIDGE_SCHEMA, validateBridge, deriveStatus, listingRow, loadGate, mintBridge, appendGreen, appendRed, loadBridge, loadRegistry, saveBridge, makeRegistry, registryExists, QUARANTINED_VERDICTS, QUARANTINED_CODE, quarantinesCredit, newestEligibleVersion, reuseEligibility, recordDoor } from "./bridges.js";
|
|
10
11
|
export { renderListing, selectionPrompt } from "./selection.js";
|
|
11
|
-
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow } from "./reuse.js";
|
|
12
|
+
export { validateEnvelope, resolveTrySpec, resolveReuse, reuseSpecHash, selectBridge, runReuse, REUSE_GRADED_RED, readResume, resumeTreeGate, CHECKPOINT_OUTCOMES, PAUSE_TTL_MS, checkpointAgeGate, applyDoorDecision, writeRunGreenRow, shapeForkName } from "./reuse.js";
|
|
12
13
|
export { classifyIncidents, foldLedger, ledgerDeltas, updateLedger, LEDGER_CLASSES, VOUCHED_RATE_SOURCES, rateProvenance, spendProvenance } from "./ledger.js";
|
|
13
14
|
export { workBranchName, prepareWorkBranch, WORK_BRANCH_PREFIX, WORK_BRANCH_RE } from "./workbranch.js";
|
|
14
15
|
export { createReadShim, wrapReadTool, readShimArm, readShimStrategy, READ_SHIM_CAP, READ_SHIM_STRATEGY, READ_SHIM_DIFF_STRATEGY, READ_SHIM_ARMS } from "./readshim.js";
|
|
@@ -26,3 +27,5 @@ export { runBehaviour, formatBehaviour } from "./behaviour.js";
|
|
|
26
27
|
export { replayRun, formatReplay } from "./replay.js";
|
|
27
28
|
export { runInterview, authorCloseForJob, assembleSpec, prepareSigning, refusalEvents, refuseLockedKind, GENRE, REFUSAL_LIB, REFUSAL_CATEGORY, AUTHORED_SPEC_FIELDS } from "./authorjob.js";
|
|
28
29
|
export { PROMPT_REGISTERS, isPromptFile } from "./promptregisters.js";
|
|
30
|
+
export { exportBundle, bundleHash, readBundle, resolveBundleSpec, checkEnvelope, bless, verifyBlessing, appendHistory, checkBundleDeps } from "./bundle.js";
|
|
31
|
+
export { readCloseScripts, checkCloseAbsolutePaths, absolutePathLiteralsOf, hashCloseScriptBytes, checkCloseByteSignature, checkStageByteSignature, signCloseScripts } from "./close-integrity.js";
|