faberun 0.27.0 → 0.28.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/README.md +9 -3
- package/package.json +1 -1
- package/skills/faberun/SKILL.md +3 -3
- package/skills/faberun/references/contract.md +9 -9
- package/skills/faberun/references/local-env.md +11 -0
- package/skills/faberun/references/operations.md +26 -33
- package/src/campaign/brief.mjs +41 -9
- package/src/campaign/campaign-brief.mjs +18 -3
- package/src/campaign/index.mjs +22 -9
- package/src/campaign/ledger.mjs +72 -4
- package/src/campaign/metrics.mjs +29 -2
- package/src/campaign/record.mjs +34 -0
- package/src/cli/campaign.mjs +35 -12
- package/src/cli/contract.mjs +11 -1
- package/src/cli/launch.mjs +12 -2
- package/src/cli/manual.mjs +24 -8
- package/src/cli/setup.mjs +5 -0
- package/src/cli/skills.mjs +18 -7
- package/src/cli/uninstall.mjs +542 -0
- package/src/cli/walkthrough.mjs +462 -0
- package/src/cli.mjs +67 -7
- package/src/contract/articles.mjs +30 -0
- package/src/contract/definition-of-done.mjs +109 -0
- package/src/contract/index.mjs +7 -4
- package/src/contract/judge-independence.mjs +18 -0
- package/src/contract/runtime.mjs +5 -1
- package/src/contract/scope-findings.mjs +120 -0
- package/src/contract/snapshot.mjs +19 -5
- package/src/contract/task-packet.mjs +49 -1
- package/src/contract/worker-result.mjs +10 -0
- package/src/engine/assignment.mjs +45 -0
- package/src/engine/detach.mjs +18 -0
- package/src/engine/gate.mjs +10 -2
- package/src/engine/judge-gate.mjs +24 -9
- package/src/engine/live-preflight.mjs +114 -18
- package/src/engine/prompts.mjs +41 -3
- package/src/engine/resume.mjs +119 -12
- package/src/engine/retry.mjs +11 -11
- package/src/engine/run-command.mjs +80 -4
- package/src/engine/run-identity.mjs +27 -14
- package/src/engine/runtime-discovery.mjs +15 -9
- package/src/engine/worker-env.mjs +179 -0
- package/src/harnesses/agy/index.mjs +11 -0
- package/src/harnesses/catalogue.mjs +29 -0
- package/src/harnesses/claude/index.mjs +98 -1
- package/src/harnesses/codex/index.mjs +15 -0
- package/src/harnesses/dsh/index.mjs +14 -0
- package/src/harnesses/exec-jsonl/index.mjs +12 -0
- package/src/harnesses/fx/index.mjs +13 -0
- package/src/harnesses/index.mjs +21 -10
- package/src/harnesses/replay/index.mjs +12 -0
- package/src/harnesses/zcode/index.mjs +19 -0
- package/src/host/config.mjs +8 -2
- package/src/host/environment-report.mjs +85 -0
- package/src/host/home.mjs +27 -0
- package/src/host/install-registry.mjs +511 -0
- package/src/host/preflight.mjs +32 -9
- package/src/host/uninstall-roots.mjs +196 -0
- package/src/plan/proof-check.mjs +15 -0
- package/src/plan/repo-facts.mjs +95 -11
- package/src/plan/rounds.mjs +27 -0
- package/src/repo/declared-paths.mjs +74 -6
- package/src/repo/integrate.mjs +1 -1
- package/src/repo/scope-closure.mjs +59 -0
- package/src/repo/signal-block.mjs +26 -0
- package/src/repo/signal.mjs +9 -24
- package/src/repo/workspace.mjs +189 -14
- package/src/report/campaign-brief.mjs +4 -0
- package/src/report/message.mjs +37 -6
- package/src/report/progress.mjs +13 -2
- package/src/run/paths.mjs +14 -1
- package/src/run/usage.mjs +66 -1
- package/src/util.mjs +18 -2
|
@@ -16,10 +16,35 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
16
16
|
import { processStartToken } from "../run/lock.mjs";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
import { runMutation } from "./mutation.mjs";
|
|
19
|
+
import { SANDBOX_BLOCKED_WRITE } from "../contract/worker-result.mjs";
|
|
20
|
+
import { isAbsolute, resolve } from "node:path";
|
|
21
|
+
import { isContained } from "../util.mjs";
|
|
19
22
|
import { spawn } from "node:child_process";
|
|
20
23
|
import { killTarget, spawnInvocation } from "../host/platform.mjs";
|
|
21
24
|
/** @typedef {import("../contract/verification.mjs").VerificationOptions} VerificationOptions */
|
|
22
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The run-level options plus the sandbox mode the caller knows. The mode is not
|
|
28
|
+
* part of the shared verification contract: it is controller environment, read
|
|
29
|
+
* here only to name a refusal the command itself cannot.
|
|
30
|
+
*
|
|
31
|
+
* @typedef {VerificationOptions & {sandboxMode?: string|null}} RunVerificationOptions
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The named classification of a `workspace-write` refusal outside the worktree,
|
|
36
|
+
* carrying the mode that caused it and the path that was denied.
|
|
37
|
+
*
|
|
38
|
+
* @typedef {{classification: "sandbox_blocked_write", mode: string, path: string}} SandboxBlockedWrite
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A bounded attempt result plus the controller's sandbox classification, which
|
|
43
|
+
* rides beside the captured output only when a refusal was recognized.
|
|
44
|
+
*
|
|
45
|
+
* @typedef {VerificationAttemptResult & {attempt: number, sandboxBlockedWrite?: SandboxBlockedWrite}} SandboxAwareAttemptResult
|
|
46
|
+
*/
|
|
47
|
+
|
|
23
48
|
/** @typedef {import("node:child_process").ChildProcess} ChildProcess */
|
|
24
49
|
/** @typedef {import("../contract/verification.mjs").VerificationAttempt} VerificationAttempt */
|
|
25
50
|
/** @typedef {import("../contract/verification.mjs").VerificationAttemptResult} VerificationAttemptResult */
|
|
@@ -62,12 +87,51 @@ function verificationEnv(command) {
|
|
|
62
87
|
}
|
|
63
88
|
return env;
|
|
64
89
|
}
|
|
90
|
+
|
|
91
|
+
// `workspace-write` denies a toolchain's cache in `$HOME` and the tool reports
|
|
92
|
+
// a read-only filesystem: observed 2026-09-22 with Zig under `dsh`,
|
|
93
|
+
// `manifest_create ReadOnlyFileSystem` before its first source file. The
|
|
94
|
+
// sandbox, not the command, is the cause, so the controller names it. `EROFS`
|
|
95
|
+
// and the POSIX message cover the same refusal from other runners.
|
|
96
|
+
const SANDBOX_READ_ONLY_PATTERN = /ReadOnlyFileSystem|EROFS|read-only file system/iu;
|
|
97
|
+
// A denied path is usually quoted (`unable to load '/path': ReadOnlyFileSystem`)
|
|
98
|
+
// or bare after the operation name; either way it is absolute, since only an
|
|
99
|
+
// effect outside the worktree gets refused.
|
|
100
|
+
const ABSOLUTE_PATH_PATTERNS = Object.freeze([/['"]((?:[A-Za-z]:[\\/]|\/)[^'"]+)['"]/gu, /((?:[A-Za-z]:[\\/]|\/)[^\s'",;:)\]]+)/gu]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Classify a failed command whose output names a read-only-filesystem refusal
|
|
104
|
+
* outside the worktree, under `workspace-write`. Returns null for any other
|
|
105
|
+
* mode, a refusal inside the worktree, or output with no path to name.
|
|
106
|
+
*
|
|
107
|
+
* @param {{text?: string, workspace: string, mode?: string|null}} args
|
|
108
|
+
* @returns {SandboxBlockedWrite|null}
|
|
109
|
+
*/
|
|
110
|
+
export function classifySandboxBlockedWrite({ text, workspace, mode }) {
|
|
111
|
+
if (mode !== "workspace-write" || typeof text !== "string") return null;
|
|
112
|
+
const lines = text.split(/\r?\n/u);
|
|
113
|
+
const markerLines = lines.filter((line) => SANDBOX_READ_ONLY_PATTERN.test(line));
|
|
114
|
+
if (markerLines.length === 0) return null;
|
|
115
|
+
// Prefer a path the refusal line itself names; fall back to any path in the
|
|
116
|
+
// output, since `manifest_create ReadOnlyFileSystem` can put the path on a
|
|
117
|
+
// neighbouring line.
|
|
118
|
+
for (const line of [...markerLines, ...lines]) {
|
|
119
|
+
for (const pattern of ABSOLUTE_PATH_PATTERNS) {
|
|
120
|
+
for (const match of line.matchAll(pattern)) {
|
|
121
|
+
const candidate = match[1];
|
|
122
|
+
const path = isAbsolute(candidate) ? candidate : resolve(workspace, candidate);
|
|
123
|
+
if (!isContained(workspace, path)) return { classification: SANDBOX_BLOCKED_WRITE, mode, path };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
65
129
|
/**
|
|
66
130
|
* Run every declared command `repeat` times inside the workspace.
|
|
67
131
|
*
|
|
68
132
|
* @param {unknown} commands
|
|
69
133
|
* @param {string} baseCwd
|
|
70
|
-
* @param {
|
|
134
|
+
* @param {RunVerificationOptions} options
|
|
71
135
|
* @returns {Promise<VerificationResult>}
|
|
72
136
|
*/
|
|
73
137
|
export async function runVerification(commands, baseCwd, options = {}) {
|
|
@@ -92,7 +156,7 @@ export async function runVerification(commands, baseCwd, options = {}) {
|
|
|
92
156
|
* @param {VerificationCommand} command
|
|
93
157
|
* @param {string} baseCwd
|
|
94
158
|
* @param {number} commandIndex
|
|
95
|
-
* @param {
|
|
159
|
+
* @param {RunVerificationOptions} options
|
|
96
160
|
* @returns {Promise<VerificationCommandResult>}
|
|
97
161
|
*/
|
|
98
162
|
async function runRepeatedCommand(command, baseCwd, commandIndex, options) {
|
|
@@ -132,7 +196,7 @@ function signalDeathRetry(result, signal) {
|
|
|
132
196
|
* @param {VerificationCommand} command
|
|
133
197
|
* @param {string} baseCwd
|
|
134
198
|
* @param {number} commandIndex
|
|
135
|
-
* @param {
|
|
199
|
+
* @param {RunVerificationOptions} options
|
|
136
200
|
* @returns {Promise<VerificationCommandResult>}
|
|
137
201
|
*/
|
|
138
202
|
async function runMutationCommand(command, baseCwd, commandIndex, options) {
|
|
@@ -149,7 +213,7 @@ async function runMutationCommand(command, baseCwd, commandIndex, options) {
|
|
|
149
213
|
* @param {string} commandCwd
|
|
150
214
|
* @param {number} attempt
|
|
151
215
|
* @param {AbortSignal|undefined} signal
|
|
152
|
-
* @param {
|
|
216
|
+
* @param {RunVerificationOptions} options
|
|
153
217
|
* @param {number} commandIndex
|
|
154
218
|
* @returns {Promise<VerificationAttemptResult>}
|
|
155
219
|
*/
|
|
@@ -181,6 +245,7 @@ function runCommand(command, baseCwd, commandCwd, attempt, signal, options, comm
|
|
|
181
245
|
if (abortHandler) signal?.removeEventListener("abort", abortHandler);
|
|
182
246
|
if (child?.pid && !error && !signalName && !timedOut) terminateGroup(child);
|
|
183
247
|
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
248
|
+
/** @type {SandboxAwareAttemptResult} */
|
|
184
249
|
const result = {
|
|
185
250
|
attempt,
|
|
186
251
|
stdout: stdout.value(),
|
|
@@ -192,6 +257,17 @@ function runCommand(command, baseCwd, commandCwd, attempt, signal, options, comm
|
|
|
192
257
|
error: error ? String(error.message ?? error) : null,
|
|
193
258
|
passed: !error && !timedOut && exitCode === 0 && !signalName,
|
|
194
259
|
};
|
|
260
|
+
// The controller's own sandbox mode bounds where a toolchain may write;
|
|
261
|
+
// a caller may name it, and a controller launched under `dsh` inherits it
|
|
262
|
+
// in `DSH_PERMISSION_MODE`. Only `workspace-write` classifies.
|
|
263
|
+
if (!result.passed) {
|
|
264
|
+
const sandboxBlockedWrite = classifySandboxBlockedWrite({
|
|
265
|
+
text: `${result.stdout}\n${result.stderr}`,
|
|
266
|
+
workspace: resolve(baseCwd, commandCwd),
|
|
267
|
+
mode: options?.sandboxMode ?? process.env.DSH_PERMISSION_MODE ?? null,
|
|
268
|
+
});
|
|
269
|
+
if (sandboxBlockedWrite) result.sandboxBlockedWrite = sandboxBlockedWrite;
|
|
270
|
+
}
|
|
195
271
|
if (!completionReported && identity) {
|
|
196
272
|
completionReported = true;
|
|
197
273
|
try { options?.onAttemptComplete?.({ ...identity, status: result.passed ? "closed" : "failed", completedAt: new Date().toISOString(), result }); } catch {
|
|
@@ -21,7 +21,7 @@ import { createHash } from "node:crypto";
|
|
|
21
21
|
import { lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
22
22
|
import { join, relative, sep } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
-
import { stableJson } from "../util.mjs";
|
|
24
|
+
import { stableJson, errorCode } from "../util.mjs";
|
|
25
25
|
import { validateRunMetadata } from "../contract/snapshot.mjs";
|
|
26
26
|
import { contractDigest } from "../contract/index.mjs";
|
|
27
27
|
import { RUNS_DIR_NAME, runDirectory } from "../run/paths.mjs";
|
|
@@ -97,6 +97,26 @@ function runDirFor(sourceIdentity) {
|
|
|
97
97
|
return runDirectory(sourceIdentity.cwd, sourceIdentity.contractId);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Whether a run directory never materialized a node. The directory, its
|
|
102
|
+
* frozen contract and its empty `nodes/` folder are written before runtime
|
|
103
|
+
* assignment and identity capture; node states are written after. A launch
|
|
104
|
+
* refused in that window leaves a directory that never held a node, so
|
|
105
|
+
* nothing can resume it and it would refuse the next attempt as an existing
|
|
106
|
+
* run.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} runDir
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
export function runNeverHeldNode(runDir) {
|
|
112
|
+
try {
|
|
113
|
+
return readdirSync(join(runDir, "nodes")).every((name) => !name.endsWith(".json"));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (errorCode(error) === "ENOENT") return true;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* The records a prior run.json already froze, when one exists. On creation
|
|
102
122
|
* this is empty; on every later metadata rewrite it is the source of truth.
|
|
@@ -506,14 +526,10 @@ export async function assertEnvironmentReady(contract, runDir, sourceIdentity) {
|
|
|
506
526
|
// state reads done, this launch replays an accepted transaction, starts no
|
|
507
527
|
// worker and no judge, and can spend no availability.
|
|
508
528
|
if (blocking === null && launchMayDispatch(runDir)) {
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
// runtime; preflightContract validates it, so only a valid number is lifted.
|
|
514
|
-
const override = Number(process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC);
|
|
515
|
-
const timeoutSec = process.env.FABERUN_PREFLIGHT_TIMEOUT_SEC !== undefined && Number.isFinite(override) && override > 0 ? override : undefined;
|
|
516
|
-
const probes = await livePreflightProbes(contract, runDir, sourceIdentity, timeoutSec);
|
|
529
|
+
// The live budget is `live-preflight.mjs`'s measured default (60s, 180s at xhigh;
|
|
530
|
+
// `FABERUN_PREFLIGHT_TIMEOUT_SEC` the operator override). It is not
|
|
531
|
+
// restated here: a second copy is a second default to keep honest.
|
|
532
|
+
const probes = await livePreflightProbes(contract, runDir, sourceIdentity);
|
|
517
533
|
const silent = probes.filter((probe) => liveSilenceCause(probe) !== null);
|
|
518
534
|
writeJsonAtomic(join(runDir, "env-preflight.json"), evidence(silent.length === 0, probes));
|
|
519
535
|
if (silent.length > 0) {
|
|
@@ -537,8 +553,6 @@ export async function assertEnvironmentReady(contract, runDir, sourceIdentity) {
|
|
|
537
553
|
throw Object.assign(new Error(`env_preflight_failed: ${blocking} · the run stays resumable: fix the environment and resume ${runDir}`), { code: "env_preflight_failed" });
|
|
538
554
|
}
|
|
539
555
|
|
|
540
|
-
/** No routed runtime is given less than this by the dispatch gate. */
|
|
541
|
-
const GATE_MINIMUM_PREFLIGHT_SEC = 60;
|
|
542
556
|
/**
|
|
543
557
|
* The live half of the gate for one launch. Every routed runtime either holds
|
|
544
558
|
* a verdict this machine recorded inside its freshness window -- reused, the
|
|
@@ -555,10 +569,9 @@ const GATE_MINIMUM_PREFLIGHT_SEC = 60;
|
|
|
555
569
|
* @param {ValidatedContract} contract
|
|
556
570
|
* @param {string} runDir
|
|
557
571
|
* @param {SourceIdentity|undefined} sourceIdentity
|
|
558
|
-
* @param {number|undefined} timeoutSec
|
|
559
572
|
* @returns {Promise<ProbeResult[]>}
|
|
560
573
|
*/
|
|
561
|
-
async function livePreflightProbes(contract, runDir, sourceIdentity
|
|
574
|
+
async function livePreflightProbes(contract, runDir, sourceIdentity) {
|
|
562
575
|
const routed = reachableRuntimes(contract);
|
|
563
576
|
/** @type {Map<string, RuntimeAvailability>} */
|
|
564
577
|
const fresh = new Map();
|
|
@@ -631,7 +644,7 @@ async function livePreflightProbes(contract, runDir, sourceIdentity, timeoutSec)
|
|
|
631
644
|
};
|
|
632
645
|
});
|
|
633
646
|
}
|
|
634
|
-
const probes = await preflightContract(join(runDir, "contract.json"), {
|
|
647
|
+
const probes = await preflightContract(join(runDir, "contract.json"), { persisted: true });
|
|
635
648
|
// What this launch bought is durable from here on: every ask that reached a
|
|
636
649
|
// provider -- a refusal included, an answer being an answer -- is recorded
|
|
637
650
|
// under the provider's own identity. Silence and a command that never
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeProviderAvailability, probeRuntime } from "../harnesses/index.mjs";
|
|
2
2
|
import { effectiveProvider } from "../contract/provider.mjs";
|
|
3
|
-
import { SAME_VENDOR_REVIEW_MODE, sameVendorTierRefusal } from "../contract/judge-independence.mjs";
|
|
3
|
+
import { SAME_VENDOR_REVIEW_MODE, effectiveJudgeIndependence, sameVendorTierRefusal } from "../contract/judge-independence.mjs";
|
|
4
4
|
|
|
5
5
|
// Availability normalization belongs to the adapter registry, which is where
|
|
6
6
|
// each provider's own exhaustion, balance, and authentication wording is
|
|
@@ -176,10 +176,12 @@ export function availableCandidates(runtimes, availability = {}) {
|
|
|
176
176
|
* copied exactly; callers persist the returned pair in run state.
|
|
177
177
|
*
|
|
178
178
|
* An optional `options.config` narrows the candidate set to the harnesses the
|
|
179
|
-
* operator enabled
|
|
180
|
-
*
|
|
181
|
-
* `
|
|
182
|
-
*
|
|
179
|
+
* operator enabled, prefers its named worker and judge, and -- when the
|
|
180
|
+
* contract declares none (R36) -- supplies the machine-wide
|
|
181
|
+
* `judgeIndependence` opt-in. An empty narrowed set falls back to the
|
|
182
|
+
* unrestricted candidates and is reported through `options.onWarning`; config
|
|
183
|
+
* never overrides an explicit node, gate or runtime-default declaration, and
|
|
184
|
+
* the cross-vendor judge rule still applies.
|
|
183
185
|
*
|
|
184
186
|
* `options.listJudge` (R18) is asked for an omitted judge before the single
|
|
185
187
|
* `config.judge` preference and the strongest-candidate default: it is the
|
|
@@ -194,7 +196,7 @@ export function availableCandidates(runtimes, availability = {}) {
|
|
|
194
196
|
* @param {RuntimeContract} contract
|
|
195
197
|
* @param {Record<string, RuntimeAvailability>} availability
|
|
196
198
|
* @param {ComposeOptions} [options]
|
|
197
|
-
* @returns {Record<string, {worker: string, judge: string}>}
|
|
199
|
+
* @returns {Record<string, {worker: string, judge: string, judgeIndependence?: "same-vendor"}>}
|
|
198
200
|
*/
|
|
199
201
|
export function composeAssignments(contract, availability = {}, options = {}) {
|
|
200
202
|
const allCandidates = availableCandidates(contract.runtimes, availability);
|
|
@@ -206,7 +208,7 @@ export function composeAssignments(contract, availability = {}, options = {}) {
|
|
|
206
208
|
if (restricted.length) candidates = restricted;
|
|
207
209
|
else if (allCandidates.length) options.onWarning?.(`config harnesses · none of ${config.harnesses.join(", ")} is available · using all available runtimes`);
|
|
208
210
|
}
|
|
209
|
-
/** @type {Record<string, {worker: string, judge: string}>} */
|
|
211
|
+
/** @type {Record<string, {worker: string, judge: string, judgeIndependence?: "same-vendor"}>} */
|
|
210
212
|
const assignments = {};
|
|
211
213
|
for (const node of contract.nodes ?? []) {
|
|
212
214
|
const workerOmitted = node.runtime === undefined && contract.runtimeDefaults?.worker === undefined;
|
|
@@ -235,7 +237,11 @@ export function composeAssignments(contract, availability = {}, options = {}) {
|
|
|
235
237
|
// R20: in same-vendor mode a judge of the worker's own provider is
|
|
236
238
|
// admissible when it is another model at an equal or higher declared tier;
|
|
237
239
|
// the strongest such candidate is the default only when no other vendor has one.
|
|
238
|
-
|
|
240
|
+
// R36: a contract that declares no mode inherits the machine config's, and
|
|
241
|
+
// the effective mode is recorded on the assignment so the launch decision
|
|
242
|
+
// survives in the run snapshot.
|
|
243
|
+
const judgeIndependence = effectiveJudgeIndependence(contract, config);
|
|
244
|
+
const sameVendorMode = judgeIndependence === SAME_VENDOR_REVIEW_MODE;
|
|
239
245
|
const judge = declaredJudge
|
|
240
246
|
?? judgeListPick?.chosen
|
|
241
247
|
?? (preferredJudge && effectiveProvider(preferredJudge.runtime) !== workerProvider ? preferredJudge.id : undefined)
|
|
@@ -251,7 +257,7 @@ export function composeAssignments(contract, availability = {}, options = {}) {
|
|
|
251
257
|
const refusal = sameVendorTierRefusal(/** @type {{harness: string, model: string}} */ (workerRuntime), /** @type {{harness: string, model: string}} */ (judgeRuntime));
|
|
252
258
|
if (refusal) throw new Error(`runtime_assignment_judge_unavailable: ${refusal.message} for node ${node.id}`);
|
|
253
259
|
}
|
|
254
|
-
assignments[node.id] = { worker, judge: judge ?? worker };
|
|
260
|
+
assignments[node.id] = { worker, judge: judge ?? worker, ...(judgeIndependence === undefined ? {} : { judgeIndependence }) };
|
|
255
261
|
}
|
|
256
262
|
return assignments;
|
|
257
263
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a worker, a judge, or an availability probe may receive.
|
|
3
|
+
*
|
|
4
|
+
* A controller variable reaches a child only when something named it. The
|
|
5
|
+
* names a child may see are the union of:
|
|
6
|
+
*
|
|
7
|
+
* - the base operating-system names below,
|
|
8
|
+
* - the names the selected adapter declares in its `declaredEnvironment`,
|
|
9
|
+
* - the runtime's `*.env_key` names, and
|
|
10
|
+
* - the explicit `envPassthrough` names on the runtime or the contract.
|
|
11
|
+
*
|
|
12
|
+
* Everything else the controller holds -- an `AWS_*`, a `GITHUB_TOKEN`, a
|
|
13
|
+
* `DATABASE_URL` exported in a shell profile -- is dropped. Verification
|
|
14
|
+
* commands are not workers and keep the full controller environment
|
|
15
|
+
* (`judge-gate.mjs`); only worker, judge, and availability-probe spawns route
|
|
16
|
+
* through here.
|
|
17
|
+
*
|
|
18
|
+
* The adapter lists are imported from each adapter directly instead of through
|
|
19
|
+
* `harnesses/catalogue.mjs`: the registry imports this module for its probe, so
|
|
20
|
+
* reaching the catalogue from here would close a runtime import cycle the repo
|
|
21
|
+
* forbids. The catalogue keeps its own copy of the same names for R2, and both
|
|
22
|
+
* read the same adapter exports.
|
|
23
|
+
*/
|
|
24
|
+
import { declaredEnvironment as claudeEnvironment } from "../harnesses/claude/index.mjs";
|
|
25
|
+
import { declaredEnvironment as codexEnvironment } from "../harnesses/codex/index.mjs";
|
|
26
|
+
import { declaredEnvironment as agyEnvironment } from "../harnesses/agy/index.mjs";
|
|
27
|
+
import { declaredEnvironment as dshEnvironment } from "../harnesses/dsh/index.mjs";
|
|
28
|
+
import { declaredEnvironment as fxEnvironment } from "../harnesses/fx/index.mjs";
|
|
29
|
+
import { declaredEnvironment as zcodeEnvironment } from "../harnesses/zcode/index.mjs";
|
|
30
|
+
import { declaredEnvironment as execJsonlEnvironment } from "../harnesses/exec-jsonl/index.mjs";
|
|
31
|
+
import { declaredEnvironment as replayEnvironment } from "../harnesses/replay/index.mjs";
|
|
32
|
+
import { NOTIFY_ENV_NAMES } from "../notify/index.mjs";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The base operating-system names every child needs to resolve a binary, find
|
|
36
|
+
* its home, and speak the locale. `LC_*` is a family, matched by prefix; the
|
|
37
|
+
* Windows entries are listed on every platform because the machine that reads
|
|
38
|
+
* them is the machine that has them.
|
|
39
|
+
*
|
|
40
|
+
* @type {readonly string[]}
|
|
41
|
+
*/
|
|
42
|
+
export const BASE_ENV_NAMES = Object.freeze([
|
|
43
|
+
"PATH",
|
|
44
|
+
"HOME",
|
|
45
|
+
"USER",
|
|
46
|
+
"LOGNAME",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"LANG",
|
|
49
|
+
"TERM",
|
|
50
|
+
"TMPDIR",
|
|
51
|
+
"TZ",
|
|
52
|
+
"SYSTEMROOT",
|
|
53
|
+
"USERPROFILE",
|
|
54
|
+
"APPDATA",
|
|
55
|
+
"LOCALAPPDATA",
|
|
56
|
+
"COMSPEC",
|
|
57
|
+
"PATHEXT",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/** @type {ReadonlySet<string>} */
|
|
61
|
+
const BASE_NAME_SET = new Set(BASE_ENV_NAMES);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The adapter-declared names, keyed by harness. Values never travel here.
|
|
65
|
+
*
|
|
66
|
+
* A `Map` rather than the catalogue's frozen object on purpose: the repo
|
|
67
|
+
* forbids the same top-level body defined twice, and the registry owns the
|
|
68
|
+
* catalogue copy.
|
|
69
|
+
*
|
|
70
|
+
* @type {Map<string, readonly string[]>}
|
|
71
|
+
*/
|
|
72
|
+
const ADAPTER_ENVIRONMENTS = new Map([
|
|
73
|
+
["claude", claudeEnvironment],
|
|
74
|
+
["codex", codexEnvironment],
|
|
75
|
+
["agy", agyEnvironment],
|
|
76
|
+
["dsh", dshEnvironment],
|
|
77
|
+
["fx", fxEnvironment],
|
|
78
|
+
["zcode", zcodeEnvironment],
|
|
79
|
+
["exec-jsonl", execJsonlEnvironment],
|
|
80
|
+
["replay", replayEnvironment],
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} name
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isBaseName(name) {
|
|
88
|
+
return BASE_NAME_SET.has(name) || name.startsWith("LC_");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The `*.env_key` names a runtime's config declares. A config value there is
|
|
93
|
+
* the environment-variable name, never the value, so this reads names alone.
|
|
94
|
+
*
|
|
95
|
+
* @param {{config?: Record<string, unknown>}|null|undefined} runtime
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function envKeyNames(runtime) {
|
|
99
|
+
/** @type {string[]} */
|
|
100
|
+
const names = [];
|
|
101
|
+
for (const [key, value] of Object.entries(runtime?.config ?? {})) {
|
|
102
|
+
if (key.endsWith(".env_key") && typeof value === "string" && value.length > 0) names.push(value);
|
|
103
|
+
}
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The non-base names one runtime may carry: what its adapter declares, what
|
|
109
|
+
* its `*.env_key` config entries name, and what the runtime or the contract
|
|
110
|
+
* explicitly passes through. The notification transport is never one of them,
|
|
111
|
+
* whatever an author writes.
|
|
112
|
+
*
|
|
113
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
114
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
115
|
+
* @returns {string[]}
|
|
116
|
+
*/
|
|
117
|
+
export function declaredEnvironmentNames(runtime, contract) {
|
|
118
|
+
const names = new Set(ADAPTER_ENVIRONMENTS.get(runtime?.harness ?? "") ?? []);
|
|
119
|
+
for (const name of envKeyNames(runtime)) names.add(name);
|
|
120
|
+
for (const name of Array.isArray(runtime?.envPassthrough) ? runtime.envPassthrough : []) names.add(name);
|
|
121
|
+
for (const name of Array.isArray(contract?.envPassthrough) ? contract.envPassthrough : []) names.add(name);
|
|
122
|
+
for (const name of NOTIFY_ENV_NAMES) names.delete(name);
|
|
123
|
+
return [...names];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The base names present in `source`, including every `LC_*` member.
|
|
128
|
+
*
|
|
129
|
+
* @param {Record<string, string|undefined>} [source]
|
|
130
|
+
* @returns {Record<string, string>}
|
|
131
|
+
*/
|
|
132
|
+
export function baseEnvironment(source = process.env) {
|
|
133
|
+
/** @type {Record<string, string>} */
|
|
134
|
+
const env = {};
|
|
135
|
+
// Look the base names up rather than filtering source keys: Windows exposes
|
|
136
|
+
// `Path`, not `PATH`, and `source[name]` is the case-insensitive read there.
|
|
137
|
+
for (const name of BASE_ENV_NAMES) {
|
|
138
|
+
const value = source[name];
|
|
139
|
+
if (value !== undefined) env[name] = value;
|
|
140
|
+
}
|
|
141
|
+
for (const [name, value] of Object.entries(source)) {
|
|
142
|
+
if (value !== undefined && name.startsWith("LC_")) env[name] = value;
|
|
143
|
+
}
|
|
144
|
+
return env;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The non-base allowlisted names present in `source`, with their controller
|
|
149
|
+
* values. This is the overlay a caller hands the gate: the gate already holds
|
|
150
|
+
* the base set in its own environment, so it only needs what the runtime adds
|
|
151
|
+
* on top.
|
|
152
|
+
*
|
|
153
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
154
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
155
|
+
* @param {Record<string, string|undefined>} [source]
|
|
156
|
+
* @returns {Record<string, string>}
|
|
157
|
+
*/
|
|
158
|
+
export function passthroughEnvironment(runtime, contract, source = process.env) {
|
|
159
|
+
/** @type {Record<string, string>} */
|
|
160
|
+
const env = {};
|
|
161
|
+
for (const name of declaredEnvironmentNames(runtime, contract)) {
|
|
162
|
+
if (isBaseName(name)) continue;
|
|
163
|
+
const value = source[name];
|
|
164
|
+
if (value !== undefined) env[name] = value;
|
|
165
|
+
}
|
|
166
|
+
return env;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The complete environment a worker, judge, or availability probe receives.
|
|
171
|
+
*
|
|
172
|
+
* @param {{harness?: string, config?: Record<string, unknown>, envPassthrough?: string[]}|null|undefined} runtime
|
|
173
|
+
* @param {{envPassthrough?: string[]}|null|undefined} [contract]
|
|
174
|
+
* @param {Record<string, string|undefined>} [source]
|
|
175
|
+
* @returns {Record<string, string>}
|
|
176
|
+
*/
|
|
177
|
+
export function workerEnvironment(runtime, contract, source = process.env) {
|
|
178
|
+
return { ...baseEnvironment(source), ...passthroughEnvironment(runtime, contract, source) };
|
|
179
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { normalizeAgyResult, parseVersion } from "../protocol.mjs";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The environment names the agy adapter reads to authenticate and configure the
|
|
5
|
+
* CLI. agy signs in through its own CLI configuration, so the only name read
|
|
6
|
+
* here is the binary override `executable()` honours. Values never travel here.
|
|
7
|
+
*
|
|
8
|
+
* @type {readonly string[]}
|
|
9
|
+
*/
|
|
10
|
+
export const declaredEnvironment = Object.freeze([
|
|
11
|
+
"FABERUN_AGY_BIN",
|
|
12
|
+
]);
|
|
13
|
+
|
|
3
14
|
/**
|
|
4
15
|
* @type {import("../index.mjs").HarnessAdapter}
|
|
5
16
|
*/
|
|
@@ -5,6 +5,14 @@ import { canonicalProvider } from "../contract/provider.mjs";
|
|
|
5
5
|
import { errorMessage } from "../util.mjs";
|
|
6
6
|
import { ANTHROPIC_MODEL_TIERS } from "./model-tiers.mjs";
|
|
7
7
|
import { spawnInvocation } from "../host/platform.mjs";
|
|
8
|
+
import { declaredEnvironment as claudeEnvironment } from "./claude/index.mjs";
|
|
9
|
+
import { declaredEnvironment as codexEnvironment } from "./codex/index.mjs";
|
|
10
|
+
import { declaredEnvironment as agyEnvironment } from "./agy/index.mjs";
|
|
11
|
+
import { declaredEnvironment as dshEnvironment } from "./dsh/index.mjs";
|
|
12
|
+
import { declaredEnvironment as fxEnvironment } from "./fx/index.mjs";
|
|
13
|
+
import { declaredEnvironment as zcodeEnvironment } from "./zcode/index.mjs";
|
|
14
|
+
import { declaredEnvironment as execJsonlEnvironment } from "./exec-jsonl/index.mjs";
|
|
15
|
+
import { declaredEnvironment as replayEnvironment } from "./replay/index.mjs";
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Model catalogue report: which models each registered harness can run, the
|
|
@@ -38,6 +46,27 @@ export const MODEL_HARNESS_ORDER = Object.freeze([
|
|
|
38
46
|
"replay",
|
|
39
47
|
]);
|
|
40
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The environment names each registered adapter reads to authenticate and
|
|
51
|
+
* configure itself, keyed by harness in `MODEL_HARNESS_ORDER`. Each list is the
|
|
52
|
+
* adapter's own frozen `declaredEnvironment` export, and only bare names ever
|
|
53
|
+
* travel here, never values. The worker/judge environment builder unions these
|
|
54
|
+
* names with the base operating-system set and the runtime's env keys, so a
|
|
55
|
+
* name missing from this catalogue is a name the child process never receives.
|
|
56
|
+
*
|
|
57
|
+
* @type {Readonly<Record<string, readonly string[]>>}
|
|
58
|
+
*/
|
|
59
|
+
export const DECLARED_HARNESS_ENVIRONMENTS = Object.freeze({
|
|
60
|
+
claude: claudeEnvironment,
|
|
61
|
+
codex: codexEnvironment,
|
|
62
|
+
agy: agyEnvironment,
|
|
63
|
+
dsh: dshEnvironment,
|
|
64
|
+
fx: fxEnvironment,
|
|
65
|
+
zcode: zcodeEnvironment,
|
|
66
|
+
"exec-jsonl": execJsonlEnvironment,
|
|
67
|
+
replay: replayEnvironment,
|
|
68
|
+
});
|
|
69
|
+
|
|
41
70
|
const CLAUDE_EFFORTS = Object.freeze(["low", "medium", "high", "max"]);
|
|
42
71
|
const CODEX_EFFORTS = Object.freeze(["low", "medium", "high", "xhigh"]);
|
|
43
72
|
const AGY_EFFORTS = Object.freeze(["low", "medium", "high"]);
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { normalizeClaudeResult, parseVersion } from "../protocol.mjs";
|
|
2
2
|
import { hookSettings } from "../../host/tool-policy-hook.mjs";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The environment names the Claude adapter reads to authenticate and configure
|
|
6
|
+
* the CLI: the provider credential the CLI resolves for API-key sign-in and the
|
|
7
|
+
* binary override `executable()` honours. Values never travel here.
|
|
8
|
+
*
|
|
9
|
+
* @type {readonly string[]}
|
|
10
|
+
*/
|
|
11
|
+
export const declaredEnvironment = Object.freeze([
|
|
12
|
+
"ANTHROPIC_API_KEY",
|
|
13
|
+
"FABERUN_CLAUDE_BIN",
|
|
14
|
+
]);
|
|
15
|
+
|
|
4
16
|
/** Built-in tools a closed-packet worker needs; every other tool is preamble. */
|
|
5
17
|
export const DEFAULT_CLAUDE_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
6
18
|
|
|
@@ -26,6 +38,88 @@ function claudePreambleArgs(runtime) {
|
|
|
26
38
|
];
|
|
27
39
|
}
|
|
28
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The envelope this adapter returns: the shared provider envelope plus the
|
|
43
|
+
* top-level reset instant a session limit names. The field has to live here
|
|
44
|
+
* rather than in `error` so that `exhaustedUntilOf` keeps parsing the sentence
|
|
45
|
+
* against the caller's clock.
|
|
46
|
+
*
|
|
47
|
+
* @typedef {import("../index.mjs").ProviderEnvelope & {resetAt?: string}} ClaudeEnvelope
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The reset sentence a Claude quota stop writes into its error message, e.g.
|
|
52
|
+
* "You've hit your session limit · resets 6:40pm (America/Sao_Paulo)". The
|
|
53
|
+
* wall-clock form names a time and a zone but no date; an absolute form names
|
|
54
|
+
* the instant directly ("Your limit will reset at 2026-09-08 20:30:00").
|
|
55
|
+
*/
|
|
56
|
+
const SESSION_RESET_SENTENCE = /resets?(?:\s+at)?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(([A-Za-z_]+(?:\/[A-Za-z0-9_+-]+)+|UTC|GMT)\)/iu;
|
|
57
|
+
const ABSOLUTE_RESET_SENTENCE = /reset(?:s| at| on)?\s+(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:?\d{2})?)/iu;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The instant a Claude quota stop names, or null when its message names none.
|
|
61
|
+
*
|
|
62
|
+
* The generic availability classifier (`harnesses/index.mjs`'s
|
|
63
|
+
* `exhaustedUntilOf`) parses the same sentence for the durable refusal store,
|
|
64
|
+
* but the node's own wait decision (`engine/backoff.mjs`'s
|
|
65
|
+
* `quotaResetSchedule`) reads only a structured `resetAt`. This adapter is the
|
|
66
|
+
* protocol boundary and cannot import `harnesses/index.mjs` -- that module
|
|
67
|
+
* imports this one at load -- so the sentence is parsed here. A zone the
|
|
68
|
+
* runtime does not know names no instant, and the failover edge is taken as
|
|
69
|
+
* before.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} message
|
|
72
|
+
* @param {number} [now] the instant a wall-clock sentence is read against
|
|
73
|
+
* @returns {string|null}
|
|
74
|
+
*/
|
|
75
|
+
function sessionResetAt(message, now = Date.now()) {
|
|
76
|
+
const absolute = ABSOLUTE_RESET_SENTENCE.exec(message);
|
|
77
|
+
if (absolute) {
|
|
78
|
+
const value = absolute[1];
|
|
79
|
+
const zoned = value.includes("T") || /(?:Z|[+-]\d{2}:?\d{2})$/u.test(value) ? value : `${value.replace(" ", "T")}Z`;
|
|
80
|
+
const parsed = Date.parse(zoned);
|
|
81
|
+
if (Number.isFinite(parsed)) return new Date(parsed).toISOString();
|
|
82
|
+
}
|
|
83
|
+
const match = SESSION_RESET_SENTENCE.exec(message);
|
|
84
|
+
if (!match) return null;
|
|
85
|
+
let hour = Number(match[1]);
|
|
86
|
+
const minute = Number(match[2] ?? "0");
|
|
87
|
+
const meridiem = match[3]?.toLowerCase();
|
|
88
|
+
if (meridiem === "pm" && hour < 12) hour += 12;
|
|
89
|
+
if (meridiem === "am" && hour === 12) hour = 0;
|
|
90
|
+
if (hour > 23 || minute > 59) return null;
|
|
91
|
+
/** @type {Intl.DateTimeFormatPart[]} */
|
|
92
|
+
let parts;
|
|
93
|
+
try {
|
|
94
|
+
parts = new Intl.DateTimeFormat("en-US", { timeZone: match[4], hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }).formatToParts(new Date(now));
|
|
95
|
+
} catch {
|
|
96
|
+
// An unknown zone name is not a reset time; the caller falls back to failover.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const at = (/** @type {Intl.DateTimeFormatPartTypes} */ type) => Number(parts.find((part) => part.type === type)?.value);
|
|
100
|
+
const offset = Date.UTC(at("year"), at("month") - 1, at("day"), at("hour") % 24, at("minute"), at("second")) - Math.floor(now / 1000) * 1000;
|
|
101
|
+
let target = Date.UTC(at("year"), at("month") - 1, at("day"), hour, minute) - offset;
|
|
102
|
+
if (target <= now) target += 86_400_000;
|
|
103
|
+
return new Date(target).toISOString();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Thread a session-limit reset into the envelope so the node can wait for it.
|
|
108
|
+
* The instant rides the envelope's top-level `resetAt`, the field the node's
|
|
109
|
+
* wait decision (`engine/backoff.mjs`'s `quotaResetSchedule`) reads. It is not
|
|
110
|
+
* written into `error.resetAt`/`exhaustedUntil` on purpose: `exhaustedUntilOf`
|
|
111
|
+
* must keep parsing the sentence against the clock its caller passes, so a test
|
|
112
|
+
* or a replayed envelope can re-read the same message at a chosen instant.
|
|
113
|
+
*
|
|
114
|
+
* @param {ClaudeEnvelope} envelope
|
|
115
|
+
* @returns {ClaudeEnvelope}
|
|
116
|
+
*/
|
|
117
|
+
function withSessionReset(envelope) {
|
|
118
|
+
if (envelope.status !== "exhausted" || envelope.resetAt) return envelope;
|
|
119
|
+
const resetAt = sessionResetAt(String(envelope.error?.message ?? ""));
|
|
120
|
+
return resetAt ? { ...envelope, resetAt } : envelope;
|
|
121
|
+
}
|
|
122
|
+
|
|
29
123
|
/**
|
|
30
124
|
* The environment one runtime's CLI runs under when its contract points it at
|
|
31
125
|
* an Anthropic-compatible endpoint other than Anthropic's own, e.g. GLM on
|
|
@@ -174,7 +268,10 @@ export const claudeHarness = {
|
|
|
174
268
|
return declaredBaseUrl(runtime) === null;
|
|
175
269
|
},
|
|
176
270
|
|
|
177
|
-
|
|
271
|
+
/** @param {string} stdout @param {number|null} exitCode @param {string|null} signal @param {import("../index.mjs").NormalizeOptions} [options] @returns {import("../index.mjs").ProviderEnvelope} */
|
|
272
|
+
normalize(stdout, exitCode, signal, options = {}) {
|
|
273
|
+
return withSessionReset(normalizeClaudeResult(stdout, exitCode, signal, options));
|
|
274
|
+
},
|
|
178
275
|
};
|
|
179
276
|
|
|
180
277
|
export const harness = claudeHarness;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { normalizeCodexResult, parseVersion, toml } from "../protocol.mjs";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The environment names the Codex adapter reads to authenticate and configure
|
|
5
|
+
* the CLI: the provider credential for API-key sign-in, the config home
|
|
6
|
+
* `codexHome()` reads when it locates the session file its usage windows come
|
|
7
|
+
* from, and the binary override `executable()` honours. Values never travel
|
|
8
|
+
* here.
|
|
9
|
+
*
|
|
10
|
+
* @type {readonly string[]}
|
|
11
|
+
*/
|
|
12
|
+
export const declaredEnvironment = Object.freeze([
|
|
13
|
+
"CODEX_HOME",
|
|
14
|
+
"FABERUN_CODEX_BIN",
|
|
15
|
+
"OPENAI_API_KEY",
|
|
16
|
+
]);
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* Bound the Codex harness preamble: a closed-packet worker or a read-only judge
|
|
5
20
|
* needs the shell and patch tools, not browser, computer-use, app or sub-agent
|