bareloop 0.21.0 → 0.23.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 +292 -0
- package/README.md +1 -1
- package/bareloop.context.md +177 -24
- package/package.json +3 -3
- package/src/authorflow.js +7 -2
- package/src/authorjob.js +15 -1
- package/src/authorscout.js +83 -9
- package/src/bundle.js +40 -46
- package/src/calibrate.js +10 -0
- package/src/cli.js +52 -19
- package/src/close-integrity.js +325 -0
- package/src/closetimeout.js +165 -0
- package/src/declaredclose.js +9 -2
- package/src/index.js +14 -0
- package/src/job.js +84 -5
- package/src/judged.js +108 -5
- package/src/kinds.js +12 -0
- package/src/plan.js +5 -1
- package/src/planrun.js +392 -46
- package/src/providers.js +176 -0
- package/src/ralph.js +40 -7
- package/src/ratelimit.js +116 -0
- package/src/reuse.js +12 -2
- package/src/reviewdoor.js +26 -6
- package/src/run.js +83 -8
- package/src/transport.js +29 -12
- package/src/validate.js +59 -0
- package/types/authorscout.d.ts +27 -2
- package/types/close-integrity.d.ts +196 -0
- package/types/closetimeout.d.ts +126 -0
- package/types/declaredclose.d.ts +6 -1
- package/types/index.d.ts +1 -0
- package/types/job.d.ts +4 -0
- package/types/judged.d.ts +7 -2
- package/types/kinds.d.ts +9 -0
- package/types/planrun.d.ts +17 -2
- package/types/providers.d.ts +104 -0
- package/types/ralph.d.ts +19 -5
- package/types/ratelimit.d.ts +59 -0
- package/types/reuse.d.ts +2 -0
- package/types/reviewdoor.d.ts +3 -1
- package/types/run.d.ts +33 -2
- package/types/transport.d.ts +10 -4
- package/types/validate.d.ts +48 -0
package/src/run.js
CHANGED
|
@@ -106,7 +106,22 @@ async function primitiveSmoke(workdir) {
|
|
|
106
106
|
* CLOSE-FIX loop (a plan STEP runs under the strike ladder instead)
|
|
107
107
|
* @param {number} [opts.strikeLimit] shell-owned strike ceiling for a plan step's ladder
|
|
108
108
|
* @param {number} [opts.shellCapUsd] the shell's hard USD ceiling
|
|
109
|
-
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory)
|
|
109
|
+
* @param {number} [opts.closeTimeoutMs] close wall-clock cap (shell territory).
|
|
110
|
+
* OPTIONAL as of PRD item 27/M3: absent, the plan flow autosets it from a $0
|
|
111
|
+
* seed timing pass (or reads the spec's own signed `closeTimeoutMs`
|
|
112
|
+
* override) — a caller passing an explicit value here still wins outright
|
|
113
|
+
* (backward compatibility / test control), but production callers
|
|
114
|
+
* (`src/cli.js`, `scripts/run-u.mjs`) pass nothing (F133, run mtqwmb9l: a
|
|
115
|
+
* runner that resolves this itself for its own purposes — e.g. `run-u.mjs`
|
|
116
|
+
* sizing its outside watchdog, F67 — must never feed the number back in
|
|
117
|
+
* here too, or this call's own autoset/override banner prints a SECOND,
|
|
118
|
+
* mislabelled "explicit runner override" line on top of the runner's own
|
|
119
|
+
* correctly-labelled one).
|
|
120
|
+
* @param {string|null} [opts.closeDir] the close's own books directory (PRD item
|
|
121
|
+
* 27/M3 Part B) — pristine copies, calibration thresholds, logs — outside
|
|
122
|
+
* the patient tree. Forwarded to every close invocation as
|
|
123
|
+
* `BARELOOP_CLOSE_DIR`; a close script that needs one and gets none
|
|
124
|
+
* instrument-stops itself (`checkCloseDirRequired`, src/close-integrity.js).
|
|
110
125
|
* @param {unknown} [opts.bridge] Layer 3 — a bridge-v1 entry to reuse as the drafter's
|
|
111
126
|
* STARTING DRAFT (design record 2026-08-01, D4), forwarded to the plan flow. Omitted is
|
|
112
127
|
* the cold path and is byte-identical to a pre-Layer-3 run. WHICH entry (the listing, an
|
|
@@ -202,13 +217,27 @@ async function primitiveSmoke(workdir) {
|
|
|
202
217
|
* @param {{text: string, fromRunid?: string|null, receivedAt?: string|null}|null} [opts.doorRerun]
|
|
203
218
|
* MODULE 8 — this leg IS the fresh engagement a signer commissioned at a previous
|
|
204
219
|
* run's review door, carrying their words. Forwarded verbatim.
|
|
220
|
+
* @param {boolean} [opts.resumable=true] PRD item 27(c)/F130 — does THIS runner
|
|
221
|
+
* support `--resume` at all? `run-u.mjs` does (the default, `true`, byte-identical
|
|
222
|
+
* to before this param existed); the exported bundle CLI does not (F130) and
|
|
223
|
+
* passes `false` so the escalation tail's option strings say "resume is
|
|
224
|
+
* `run-u`-only in v1" instead of naming a flag that would fail if typed. Forwarded
|
|
225
|
+
* verbatim to the plan flow, which owns the three `--resume`-naming readouts.
|
|
205
226
|
* @returns {Promise<string>} outcome: 'green' | 'already-green' | 'escalated' |
|
|
206
227
|
* 'unapproved-spec' | 'job-red' | 'smoke-red' | 'plan-red' | 'check-red' |
|
|
207
228
|
* 'close-red' | 'close-unsupported' | 'recipe-stale' | 'branch-red' | 'pricing-red' | 'provider-red' |
|
|
208
229
|
* 'interpreter-red' | 'cap-halt' | 'wall-halt' | 'step-stalled' |
|
|
209
230
|
* 'hitl-pause' | 'hitl-decision-red' | `step-red:<id>`
|
|
231
|
+
* NOTE: `job-end` can ALSO land with outcome 'runner-drained' (F140/PRD item
|
|
232
|
+
* 28(c)) — the `beforeExit` backstop's own terminal, emitted from OUTSIDE this
|
|
233
|
+
* function's normal control flow when the process drains with no `job-end` at
|
|
234
|
+
* all (a provider promise that never settled). It never appears as this
|
|
235
|
+
* function's own RETURN value (nothing is left to receive it), only as the
|
|
236
|
+
* spine's terminal record. Non-zero exit, `spendComplete:false` always, and
|
|
237
|
+
* deliberately NOT in the resumable/checkpoint set (`src/reuse.js`) — an
|
|
238
|
+
* unknown in-flight state is not a known-safe resume point.
|
|
210
239
|
*/
|
|
211
|
-
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null }) {
|
|
240
|
+
export async function runJob(rawSpec, { approvals, workdir, provider, nativeProvider, providerFor, judgeProvider = null, emit, capRuns = 3, strikeLimit, shellCapUsd = 2, closeTimeoutMs, closeDir = null, layerRoot = false, readShim = false, scout = true, bridge = null, priorSpentUsd = 0, priorSpendComplete = true, priorWallMs = 0, resumeSeed = null, resumeGrades = [], resumeReplans = null, resumeBranch = null, humanRuling = null, heldRuling = null, reviewDoor = null, doorRerun = null, resumable = true }) {
|
|
212
241
|
// THE READ SHIM's ARM, resolved at the door — the FIRST thing this entry does,
|
|
213
242
|
// before the ledger, before the approval gate, before a byte of the spec is read.
|
|
214
243
|
// An unrecognised spelling throws here at zero cost instead of being coerced by
|
|
@@ -324,6 +353,15 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
324
353
|
// than guessing.
|
|
325
354
|
verdictType: job.verdictType,
|
|
326
355
|
...(typeof provider?.model === 'string' ? { model: provider.model } : {}),
|
|
356
|
+
// PRD item 28 / F156 — WHICH provider actually ran, and against which
|
|
357
|
+
// endpoint. Report-only, but without it an archived spine cannot be
|
|
358
|
+
// segmented by provider at all: every cost, duration and outcome row from
|
|
359
|
+
// the day openai-api joined the menu would pool anthropic and non-anthropic
|
|
360
|
+
// runs into one contaminated aggregate, which is the reading error this repo
|
|
361
|
+
// has already paid for once. `baseUrl` rides along only when the job set one
|
|
362
|
+
// (an absent baseUrl is the vendor's own host, not an unknown).
|
|
363
|
+
provider: typeof job.provider === 'string' ? job.provider : null,
|
|
364
|
+
...(typeof job.baseUrl === 'string' ? { baseUrl: job.baseUrl } : {}),
|
|
327
365
|
// F118: replay reads this; report-only. `dirty` is deliberately omitted —
|
|
328
366
|
// it can't be known without a shell, and null = unknown, never faked.
|
|
329
367
|
code: (() => { const { version, sha } = codeVersion(); return { version, sha }; })(),
|
|
@@ -359,12 +397,46 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
359
397
|
: {}),
|
|
360
398
|
});
|
|
361
399
|
|
|
400
|
+
// F140/PRD item 28 (c) — the backstop that fires on the ABSENCE of a
|
|
401
|
+
// `job-end`. A provider promise that never settles (BA-25's class; 0.42.0
|
|
402
|
+
// closed the known drop cases per F141 but the class itself is "an
|
|
403
|
+
// instrument fires on absence", not a specific bug) lets node drain with
|
|
404
|
+
// nothing else holding the event loop, and the process exits with no
|
|
405
|
+
// terminal record at all — no escalation, no outcome, no way to tell a
|
|
406
|
+
// silent drain from an operator kill. `ended` is the one flag every
|
|
407
|
+
// job-end site sets via the local `end()` helper below (never a second
|
|
408
|
+
// spelling of "did we finish" that could fall out of sync with the real
|
|
409
|
+
// emits). One `beforeExit` listener per call, removed in the `finally` so
|
|
410
|
+
// multiple runs in one process (tests included) never leak listeners.
|
|
411
|
+
let ended = false;
|
|
412
|
+
/** @type {(outcome: string, extra?: object) => object} */
|
|
413
|
+
const end = (outcome, extra = {}) => {
|
|
414
|
+
ended = true;
|
|
415
|
+
return emit('job-end', { outcome, ...extra });
|
|
416
|
+
};
|
|
417
|
+
const onBeforeExit = () => {
|
|
418
|
+
if (ended) return;
|
|
419
|
+
// A drained run's in-flight spend is unknowable — the floor already
|
|
420
|
+
// accumulated is honest, an exact-looking total would not be (F6).
|
|
421
|
+
end('runner-drained', {
|
|
422
|
+
...spend(),
|
|
423
|
+
spendComplete: false,
|
|
424
|
+
detail: 'process drained with no job-end — a provider call never settled (F140)',
|
|
425
|
+
});
|
|
426
|
+
// Never process.exit() (can discard queued stdout) — set the code and
|
|
427
|
+
// let node exit on its own; preserve a more specific code if one is
|
|
428
|
+
// already set.
|
|
429
|
+
process.exitCode = process.exitCode || 1;
|
|
430
|
+
};
|
|
431
|
+
process.once('beforeExit', onBeforeExit);
|
|
432
|
+
|
|
433
|
+
try {
|
|
362
434
|
// 2. known-answer smoke before tokens (A3: silent degradation throws nothing)
|
|
363
435
|
const smoke = await primitiveSmoke(workdir);
|
|
364
436
|
emit('primitive-smoke', smoke);
|
|
365
437
|
if (!smoke.ok) {
|
|
366
438
|
emit('escalation', { category: 'smoke-red', decisionReady: true, decision: `The ${smoke.primitive} primitive failed its known-answer check — no run verdict is trustworthy on a degraded primitive.`, options: ['fix the primitive/store', 'abandon the run'], detail: smoke.detail });
|
|
367
|
-
|
|
439
|
+
end('smoke-red', spend());
|
|
368
440
|
return 'smoke-red';
|
|
369
441
|
}
|
|
370
442
|
|
|
@@ -405,7 +477,7 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
405
477
|
};
|
|
406
478
|
const pricingRed = () => {
|
|
407
479
|
emit('escalation', { category: 'pricing-red', decisionReady: true, decision: 'A provider result carried no priced cost — the hard cap cannot govern spend it cannot see (unpriced is never free, F6).', options: ['bind a priced provider/model', 'abandon the run'], spentUsd });
|
|
408
|
-
|
|
480
|
+
end('pricing-red', spend());
|
|
409
481
|
return 'pricing-red';
|
|
410
482
|
};
|
|
411
483
|
|
|
@@ -415,14 +487,14 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
415
487
|
// accounts it natively (F12) and the job-end money contract is unchanged.
|
|
416
488
|
{
|
|
417
489
|
const outcome = await runPlan(job, {
|
|
418
|
-
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd,
|
|
490
|
+
workdir, provider, nativeProvider, providerFor, judgeProvider, emit: meter, capRuns, ...(strikeLimit !== undefined ? { strikeLimit } : {}), closeTimeoutMs, closeDir, layerRoot, readShim, scout, bridge, priorWallMs: chainWallMs, resumeSeed, resumeGrades, resumeReplans, resumeBranch, humanRuling, heldRuling, reviewDoor, doorRerun, priorSpentUsd: chainFoldUsd, resumable,
|
|
419
491
|
remainingUsd: () => Math.min(shellCapUsd, job.budgetUsd - spentUsd),
|
|
420
492
|
isUnpriced: () => unpriced, // F6: let the plan flow bail in-flight, not just after it returns
|
|
421
493
|
spendComplete, // …and let its money-halt readout say whether the remaining it quotes is exact
|
|
422
494
|
});
|
|
423
495
|
if (unpriced) return pricingRed();
|
|
424
496
|
if (outcome.startsWith('step-red:')) {
|
|
425
|
-
|
|
497
|
+
end('step-red', { step: outcome.slice('step-red:'.length), ...spend() });
|
|
426
498
|
} else if (outcome === 'provider-red' || outcome === 'step-stalled') {
|
|
427
499
|
// F44: a transport-throw provider-red never returned a usage figure for the
|
|
428
500
|
// failed call, so the priced sum is a FLOOR, not the total — spendComplete
|
|
@@ -432,10 +504,13 @@ export async function runJob(rawSpec, { approvals, workdir, provider, nativeProv
|
|
|
432
504
|
// the known cost of self-heal — and an unbilled reissue is indistinguishable
|
|
433
505
|
// from a billed one from here. Reporting the priced sum as exact would be F6
|
|
434
506
|
// in a self-heal coat.
|
|
435
|
-
|
|
507
|
+
end(outcome, { ...spend(), spendComplete: false });
|
|
436
508
|
} else {
|
|
437
|
-
|
|
509
|
+
end(outcome, spend());
|
|
438
510
|
}
|
|
439
511
|
return outcome;
|
|
440
512
|
}
|
|
513
|
+
} finally {
|
|
514
|
+
process.removeListener('beforeExit', onBeforeExit);
|
|
515
|
+
}
|
|
441
516
|
}
|
package/src/transport.js
CHANGED
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
//
|
|
4
4
|
// A transport failure is fetch THROWING — the request never produced an HTTP
|
|
5
5
|
// response at all (a TLS fault mid-record, a reset socket, a DNS/network
|
|
6
|
-
// failure)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
6
|
+
// failure) — OR bare-agent's own BA-25 `guardResponseSettles` rejecting a
|
|
7
|
+
// response cut after headers but before the body completed (a peer
|
|
8
|
+
// `'aborted'`/`'error'`/premature `'close'`), tagged `context.bound:
|
|
9
|
+
// 'transport'` (bare-agent 0.42.0, node_modules/bare-agent/src/provider-http.js).
|
|
10
|
+
// That is a DIFFERENT unknown from an HTTP response the provider DID send
|
|
11
|
+
// (4xx/5xx/429): a response means the provider answered, so bare-agent's own
|
|
12
|
+
// retry policy for those stays exactly what it is — this module never widens
|
|
13
|
+
// that. Retrying only the no-response case is the whole point: a call that
|
|
14
|
+
// dies mid-read may already have been billed (retry can pay twice), so the
|
|
15
|
+
// retry budget stays as tight as hamr authorized it — exactly one extra
|
|
16
|
+
// attempt, never configurable upward from a job spec or argv (tighten-only
|
|
17
|
+
// doctrine).
|
|
14
18
|
//
|
|
15
19
|
// Scope: this module wires ONE seam — the worker Loop built in
|
|
16
20
|
// src/planrun.js's `newLoop` (~line 2239), which is the ONE factory behind
|
|
@@ -31,11 +35,17 @@ export const TRANSPORT_MAX_ATTEMPTS = TRANSPORT_RETRIES + 1;
|
|
|
31
35
|
* Is `err` a transport-class failure — the request never produced an HTTP
|
|
32
36
|
* response? True for: a TLS fault mid-record ("bad record mac" and kin), a
|
|
33
37
|
* reset/broken/timed-out socket (ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED,
|
|
34
|
-
* ENETUNREACH — by code on the error itself or on `err.cause`),
|
|
35
|
-
*
|
|
36
|
-
*
|
|
38
|
+
* ENETUNREACH — by code on the error itself or on `err.cause`), `fetch
|
|
39
|
+
* failed` wrapping a network `cause`, and — as of bare-agent 0.42.0 (BA-25)
|
|
40
|
+
* — a `ProviderError` whose `err.context.bound === 'transport'`: the
|
|
41
|
+
* response stream ended (`'aborted'`/`'error'`/premature `'close'`) after
|
|
42
|
+
* headers but before the body completed, which bare-agent itself rejects as
|
|
43
|
+
* retryable with no HTTP status. False for anything carrying an HTTP status
|
|
44
|
+
* (`err.status`/`err.statusCode`, 4xx/5xx/429) or an explicit
|
|
37
45
|
* `err.retryable === false` — those are provider RESPONSES, not transport
|
|
38
|
-
* casualties, and bare-agent's own policy governs them unchanged
|
|
46
|
+
* casualties, and bare-agent's own policy governs them unchanged; both
|
|
47
|
+
* guards run BEFORE the `context.bound` check, so a status or an explicit
|
|
48
|
+
* `retryable:false` wins even if `bound` says 'transport'.
|
|
39
49
|
* @param {any} err
|
|
40
50
|
* @returns {boolean}
|
|
41
51
|
*/
|
|
@@ -47,6 +57,13 @@ export function isTransportFailure(err) {
|
|
|
47
57
|
if (typeof status === 'number') return false;
|
|
48
58
|
if (err.retryable === false) return false;
|
|
49
59
|
|
|
60
|
+
// BA-25 (bare-agent 0.42.0): bare-agent's own transport classification —
|
|
61
|
+
// `guardResponseSettles` tags a body-cut-after-headers rejection with
|
|
62
|
+
// `context.bound: 'transport'`. Only 'transport' qualifies here — a
|
|
63
|
+
// sibling bound like 'idle' (BA-19's deadline-idle guard) already
|
|
64
|
+
// qualifies via its ETIMEDOUT code below, never by bound alone.
|
|
65
|
+
if (err.context && err.context.bound === 'transport') return true;
|
|
66
|
+
|
|
50
67
|
const codeOf = (e) => (e && typeof e.code === 'string' ? e.code : null);
|
|
51
68
|
const TRANSPORT_CODES = new Set(['ECONNRESET', 'EPIPE', 'ETIMEDOUT', 'ECONNREFUSED', 'ENETUNREACH', 'EAI_AGAIN']);
|
|
52
69
|
const code = codeOf(err) ?? codeOf(err.cause);
|
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.
|
package/types/authorscout.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
|
|
|
91
91
|
*
|
|
92
92
|
* @param {{workdir: string, provider?: any,
|
|
93
93
|
* rounds?: number, minBytes?: number, blobMax?: number, maxTokens?: number, ctx?: boolean,
|
|
94
|
-
* attempts?: number, ceilingUsd?: number|null,
|
|
94
|
+
* attempts?: number, ceilingUsd?: number|null, callTimeoutMs?: number,
|
|
95
95
|
* onCall?: (call: {label: string, costUsd: number|null, unpricedRounds: number}) => void,
|
|
96
96
|
* createLoop?: (o: {system: string, policy: any, onLlmResult: any, provider: any}) => any,
|
|
97
97
|
* createSurveyor?: (o: {workdir: string, granted: readonly string[], ctx: boolean}) => Promise<any>}} o
|
|
@@ -99,7 +99,7 @@ export function defaultSurveyor({ workdir, granted, auditPath, ctx }: {
|
|
|
99
99
|
* calls: {label: string, costUsd: number|null, unpricedRounds: number}[],
|
|
100
100
|
* budgetStop: 'cap-halt'|'pricing-red'|null}>}
|
|
101
101
|
*/
|
|
102
|
-
export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, onCall, createLoop, createSurveyor, }: {
|
|
102
|
+
export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, maxTokens, ctx, attempts, ceilingUsd, callTimeoutMs, onCall, createLoop, createSurveyor, }: {
|
|
103
103
|
workdir: string;
|
|
104
104
|
provider?: any;
|
|
105
105
|
rounds?: number;
|
|
@@ -109,6 +109,7 @@ export function runAuthorScout({ workdir, provider, rounds, minBytes, blobMax, m
|
|
|
109
109
|
ctx?: boolean;
|
|
110
110
|
attempts?: number;
|
|
111
111
|
ceilingUsd?: number | null;
|
|
112
|
+
callTimeoutMs?: number;
|
|
112
113
|
onCall?: (call: {
|
|
113
114
|
label: string;
|
|
114
115
|
costUsd: number | null;
|
|
@@ -205,6 +206,30 @@ export function buildSeedListing({ workdir, seedRef, sourcePaths, testPaths, lis
|
|
|
205
206
|
tier: string;
|
|
206
207
|
};
|
|
207
208
|
}>;
|
|
209
|
+
/**
|
|
210
|
+
* F152/PRD 30.4 — the scout's per-call IDLE bound. Every `loop.run()` in this
|
|
211
|
+
* module carried NO time bound at all: a live endpoint that accepts and never
|
|
212
|
+
* answers hangs the process forever (measured) — an open socket is an active
|
|
213
|
+
* handle, so node never drains and no backstop can fire.
|
|
214
|
+
*
|
|
215
|
+
* UNLIKE the run path (`callBounds()`, src/planrun.js:1518), this pipeline has
|
|
216
|
+
* NO wall clock to derive a bound FROM — the authoring pipeline is genuinely
|
|
217
|
+
* clockless (`src/authorjob.js`/`src/authorflow.js`/`scripts/run-author.mjs`
|
|
218
|
+
* carry a money ceiling, `ceilingUsd`/`capStop`, and never a `maxWallMs`).
|
|
219
|
+
* Rather than inventing a fresh number, this reuses the EXACT ceiling the run
|
|
220
|
+
* path itself falls back to when ITS OWN wall is unbounded
|
|
221
|
+
* (`PROVIDER_TIMEOUT_MS`, `src/clock.js` — BA-18's idle timeout, already an
|
|
222
|
+
* approved "no wall present" default one layer up): an authoring-time call is
|
|
223
|
+
* the identical situation, one call earlier.
|
|
224
|
+
*
|
|
225
|
+
* It bounds SOCKET INACTIVITY only (reset by every byte, per BA-18) — a call
|
|
226
|
+
* that is merely slow keeps streaming and is untouched; only a truly silent
|
|
227
|
+
* endpoint trips it. TIGHTEN-ONLY, like every cap in this file: this is an
|
|
228
|
+
* operator-set ceiling fixed here, not a param a caller or a signed spec may
|
|
229
|
+
* widen (a defaulted cap is a silent second ceiling, and there is no clock
|
|
230
|
+
* whose narrower reading could ever tighten it further).
|
|
231
|
+
*/
|
|
232
|
+
export const AUTHOR_CALL_TIMEOUT_MS: 600000;
|
|
208
233
|
/** src/planrun.js:42 — the survey's hard round bound. */
|
|
209
234
|
export const AUTHOR_SCOUT_ROUNDS: 8;
|
|
210
235
|
/** src/planrun.js:44 — the survey is a PROMPT INGREDIENT for the authoring call. */
|
|
@@ -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
|
+
};
|