@crewhaus/tool-harness-synthesizer 0.1.4 → 0.1.6

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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Track D (§55) — `tool-harness-synthesizer`. Thompson-sampled tree
3
+ * search over candidate verifier functions for skills and tools.
4
+ *
5
+ * Source: AutoHarness (Lou et al., Google DeepMind, March 2026,
6
+ * arxiv 2603.03329). The paper's headline finding: a smaller LLM
7
+ * (Gemini-2.5-Flash) plus a synthesized code harness beats a larger
8
+ * LLM (Gemini-2.5-Pro) at near-zero inference cost. The trick is to
9
+ * have the LLM synthesize TWO functions iteratively, with the
10
+ * environment as critic:
11
+ *
12
+ * - `propose_action(obs)` — candidate generator
13
+ * - `is_legal_action(obs, action)` — verifier
14
+ *
15
+ * If the verifier returns `True` but the action is invalid, refine
16
+ * BOTH functions; if it returns `False` and the action is invalid,
17
+ * refine only the proposer. This split-refinement is the empirical
18
+ * winning move.
19
+ *
20
+ * In CrewHaus, the equivalent is to synthesize verifier code per
21
+ * skill or tool: an `is_valid_output(input, output)` function for any
22
+ * tool that has objective validity criteria. The verifier becomes a
23
+ * reusable artifact under `.crewhaus/verifiers/<name>.ts` and feeds
24
+ * into the `eval-optimizer-orchestrator` via a `MutationProvider`
25
+ * variant that proposes verifier-aware prompt edits.
26
+ *
27
+ * v0 ships:
28
+ * - `synthesizeVerifier(spec)` — tree search over candidate verifier
29
+ * code strings (the LLM call is supplied by the caller so this
30
+ * package stays pure with respect to the model)
31
+ * - `thompsonPick(nodes)` — Thompson sampling over tree nodes
32
+ * - `VerifierMutationProvider` — adapter to plug verifier search
33
+ * into the existing optimizer
34
+ * - `runVerifier(code, samples)` — scores a candidate by running it
35
+ * inside a locked-down `@crewhaus/sandbox` container; the candidate
36
+ * code never executes on this host (see its SECURITY note).
37
+ *
38
+ * Cited paper: AutoHarness (arxiv 2603.03329, Lou et al., 2026-03).
39
+ */
40
+ import { CrewhausError } from "@crewhaus/errors";
41
+ import type { MutationProvider, OptimizerState, ProviderMutation } from "@crewhaus/prompt-optimizer";
42
+ import { type Sandbox } from "@crewhaus/sandbox";
43
+ export declare class HarnessSynthesizerError extends CrewhausError {
44
+ readonly name = "HarnessSynthesizerError";
45
+ constructor(message: string, cause?: unknown);
46
+ }
47
+ /**
48
+ * One sample of behavior the verifier should be measured against.
49
+ * `expected` is whether the verifier should accept (true) or reject
50
+ * (false) this sample. Both classes are required for non-degenerate
51
+ * search — a verifier that returns `true` for everything passes the
52
+ * `expected: true` set perfectly.
53
+ *
54
+ * NOTE: `input`/`output` cross an isolation boundary (see `runVerifier`)
55
+ * and are marshaled as JSON, so they must be JSON-serializable. The
56
+ * verifier observes structured copies, not live host references — an
57
+ * intentional consequence of running it out-of-process.
58
+ */
59
+ export type VerifierSample = {
60
+ readonly input: unknown;
61
+ readonly output: unknown;
62
+ readonly expected: boolean;
63
+ };
64
+ /**
65
+ * A candidate verifier — a code string + the per-sample score it
66
+ * achieved on the last evaluation. `code` is a function body string
67
+ * with the signature `(input: unknown, output: unknown) => boolean`.
68
+ * It's stored as a string so the search can mutate it and feed it
69
+ * back to the LLM.
70
+ *
71
+ * SECURITY: `code` is attacker-influenceable (caller seed candidates +
72
+ * refiner output, which a future model-backed refiner derives from
73
+ * skill/tool I/O that can carry injected content). It is NEVER compiled
74
+ * or invoked on this host. Execution happens via `runVerifier`, which
75
+ * evaluates the candidate inside a locked-down `@crewhaus/sandbox`
76
+ * container with no ambient authority — see the SECURITY note on
77
+ * `runVerifier`.
78
+ */
79
+ export type VerifierCandidate = {
80
+ readonly id: string;
81
+ readonly code: string;
82
+ readonly score: number;
83
+ /** AutoHarness's heuristic value — average over samples, in [0, 1]. */
84
+ readonly heuristic: number;
85
+ /** Beta posterior parameters for Thompson sampling. */
86
+ readonly alpha: number;
87
+ readonly beta: number;
88
+ };
89
+ /**
90
+ * The Refiner: takes a failing candidate + concrete failure cases and
91
+ * returns a new code string. In production, this is a model call; for
92
+ * testing it's a deterministic rule-based mutation. Either way, the
93
+ * signature is the same.
94
+ *
95
+ * SECURITY: the string this returns is executed (in isolation — see
96
+ * `runVerifier`), so a model-backed refiner is effectively running
97
+ * model output as code. The container is the trust boundary, NOT the
98
+ * model — never relax `runVerifier`'s isolation on the assumption that
99
+ * refiner output is trustworthy.
100
+ */
101
+ export type RefinerFn = (current: VerifierCandidate, failures: ReadonlyArray<VerifierSample>) => Promise<string>;
102
+ /** Isolation options shared by `runVerifier` and the inner search. */
103
+ export type RunVerifierOptions = {
104
+ /**
105
+ * Isolation backend. Defaults to `createSandbox()` (honors the
106
+ * `CREWHAUS_SANDBOX` env; `docker` by default). The non-isolating
107
+ * `noop` backend is REFUSED — see the SECURITY note on `runVerifier`.
108
+ */
109
+ readonly sandbox?: Sandbox;
110
+ /** Per-evaluation wall-clock budget (ms). Default: 10_000. */
111
+ readonly timeoutMs?: number;
112
+ /** Container image. Default: `node:22-alpine` (on the sandbox allowlist). */
113
+ readonly image?: string;
114
+ };
115
+ /**
116
+ * The Critic: runs `code` against a sample set and returns the per-
117
+ * sample verdict and the heuristic value. Deterministic given the same
118
+ * code + samples (modulo the verifier's own determinism).
119
+ *
120
+ * SECURITY (FR-007): `code` is untrusted and is NOT run in this process.
121
+ * `runVerifier` ships `{ code, samples }` to a locked-down
122
+ * `@crewhaus/sandbox` container (`--network none`, `--read-only`, no
123
+ * host env, cpu/mem caps, wall-clock kill, `no-new-privileges`) and
124
+ * scores the returned verdicts here on the host — the container never
125
+ * receives `expected`. Inside the jail the candidate is compiled with
126
+ * the runtime's dynamic-function constructor and invoked over the
127
+ * samples; an exfiltration/RCE attempt (e.g. a refiner emitting a
128
+ * network call) is contained by the jail. FAIL CLOSED: when no real
129
+ * isolation backend is available (the `noop` backend, or none),
130
+ * `runVerifier` throws rather than executing untrusted code unsandboxed,
131
+ * mirroring the `requiresSandbox` floor enforced elsewhere in the repo.
132
+ *
133
+ * Behavior contract: per-sample runtime errors are caught (verdict
134
+ * `false`, `errors++`), matching the prior in-process semantics. Code
135
+ * that fails to compile, times out, or otherwise crashes the harness
136
+ * throws a `HarnessSynthesizerError`.
137
+ */
138
+ export declare function runVerifier(code: string, samples: ReadonlyArray<VerifierSample>, opts?: RunVerifierOptions): Promise<{
139
+ readonly verdicts: ReadonlyArray<boolean>;
140
+ readonly heuristic: number;
141
+ readonly errors: number;
142
+ }>;
143
+ /**
144
+ * Thompson sampling over a node population. Picks the index whose
145
+ * posterior sample is highest. Each node has a Beta(alpha, beta)
146
+ * posterior over its heuristic value; the alpha/beta are accumulated
147
+ * across iterations as the search refines.
148
+ */
149
+ export declare function thompsonPick(nodes: ReadonlyArray<VerifierCandidate>, rng?: () => number): number;
150
+ export type SynthesizeOptions = {
151
+ /** Initial seed candidates. Must be non-empty; provides the starting tree. */
152
+ readonly seedCandidates: ReadonlyArray<string>;
153
+ /** Samples the verifier is scored against. */
154
+ readonly samples: ReadonlyArray<VerifierSample>;
155
+ /** The refiner — usually an LLM-backed function. */
156
+ readonly refiner: RefinerFn;
157
+ /** Maximum tree-search iterations. Default: 16 (paper's median is ~14). */
158
+ readonly maxIterations?: number;
159
+ /** Target heuristic value — stop when reached. Default: 1.0 (100% correct). */
160
+ readonly target?: number;
161
+ /** RNG for Thompson sampling. Default: Math.random. */
162
+ readonly rng?: () => number;
163
+ /**
164
+ * Isolation backend reused across every evaluation in the inner search.
165
+ * Defaults to one `createSandbox()` per call (closed when the search
166
+ * ends). See `runVerifier` for the isolation/fail-closed contract.
167
+ */
168
+ readonly sandbox?: Sandbox;
169
+ /** Per-evaluation wall-clock budget (ms) forwarded to `runVerifier`. */
170
+ readonly timeoutMs?: number;
171
+ };
172
+ export type SynthesizeResult = {
173
+ readonly best: VerifierCandidate;
174
+ readonly iterations: number;
175
+ readonly converged: boolean;
176
+ readonly trajectory: ReadonlyArray<VerifierCandidate>;
177
+ };
178
+ /**
179
+ * Run the tree search. Returns the best candidate found, the
180
+ * iteration count, and whether the target heuristic was reached.
181
+ * Pure with respect to randomness: pass `rng` for determinism.
182
+ *
183
+ * One isolation backend is created (or reused, if `opts.sandbox` is
184
+ * supplied) for the whole search and closed on exit — see `runVerifier`.
185
+ */
186
+ export declare function synthesizeVerifier(opts: SynthesizeOptions): Promise<SynthesizeResult>;
187
+ /**
188
+ * `MutationProvider` adapter so verifier search can drop into the
189
+ * existing eval-optimizer-orchestrator loop. The provider's `next()`
190
+ * runs one iteration of the inner tree search and emits a
191
+ * prompt-edit that references the synthesized verifier.
192
+ *
193
+ * Typical wiring (programmatic): construct this provider with the
194
+ * spec's skill samples and a `refiner` function, then pass it to
195
+ * `optimizeSpec({ mutator: new VerifierMutationProvider(...) })`. The
196
+ * orchestrator runs the standard search loop, but each "mutation" is
197
+ * a freshly-synthesized verifier persisted to .crewhaus/verifiers/.
198
+ * (CLI `--mutator verifier-synthesis` wiring is a follow-up; the CLI
199
+ * today exposes `rule-based` and `claude` only.)
200
+ *
201
+ * SECURITY: the inner search executes refiner-produced code. Pass a
202
+ * `sandbox` (or rely on the default) — `synthesizeVerifier`/`runVerifier`
203
+ * isolate every evaluation and fail closed without a real backend. A
204
+ * model-backed `refiner` is only safe to wire because of that jail.
205
+ */
206
+ export declare class VerifierMutationProvider implements MutationProvider {
207
+ private readonly samples;
208
+ private readonly refiner;
209
+ private readonly seedCandidates;
210
+ private readonly maxInnerIterations;
211
+ private readonly sandbox?;
212
+ readonly name = "verifier-synthesis";
213
+ private synthesisIterations;
214
+ constructor(samples: ReadonlyArray<VerifierSample>, refiner: RefinerFn, seedCandidates: ReadonlyArray<string>, maxInnerIterations?: number, sandbox?: Sandbox | undefined);
215
+ next(state: OptimizerState): Promise<ProviderMutation>;
216
+ }
package/dist/index.js ADDED
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Track D (§55) — `tool-harness-synthesizer`. Thompson-sampled tree
3
+ * search over candidate verifier functions for skills and tools.
4
+ *
5
+ * Source: AutoHarness (Lou et al., Google DeepMind, March 2026,
6
+ * arxiv 2603.03329). The paper's headline finding: a smaller LLM
7
+ * (Gemini-2.5-Flash) plus a synthesized code harness beats a larger
8
+ * LLM (Gemini-2.5-Pro) at near-zero inference cost. The trick is to
9
+ * have the LLM synthesize TWO functions iteratively, with the
10
+ * environment as critic:
11
+ *
12
+ * - `propose_action(obs)` — candidate generator
13
+ * - `is_legal_action(obs, action)` — verifier
14
+ *
15
+ * If the verifier returns `True` but the action is invalid, refine
16
+ * BOTH functions; if it returns `False` and the action is invalid,
17
+ * refine only the proposer. This split-refinement is the empirical
18
+ * winning move.
19
+ *
20
+ * In CrewHaus, the equivalent is to synthesize verifier code per
21
+ * skill or tool: an `is_valid_output(input, output)` function for any
22
+ * tool that has objective validity criteria. The verifier becomes a
23
+ * reusable artifact under `.crewhaus/verifiers/<name>.ts` and feeds
24
+ * into the `eval-optimizer-orchestrator` via a `MutationProvider`
25
+ * variant that proposes verifier-aware prompt edits.
26
+ *
27
+ * v0 ships:
28
+ * - `synthesizeVerifier(spec)` — tree search over candidate verifier
29
+ * code strings (the LLM call is supplied by the caller so this
30
+ * package stays pure with respect to the model)
31
+ * - `thompsonPick(nodes)` — Thompson sampling over tree nodes
32
+ * - `VerifierMutationProvider` — adapter to plug verifier search
33
+ * into the existing optimizer
34
+ * - `runVerifier(code, samples)` — scores a candidate by running it
35
+ * inside a locked-down `@crewhaus/sandbox` container; the candidate
36
+ * code never executes on this host (see its SECURITY note).
37
+ *
38
+ * Cited paper: AutoHarness (arxiv 2603.03329, Lou et al., 2026-03).
39
+ */
40
+ import { CrewhausError } from "@crewhaus/errors";
41
+ import { createSandbox } from "@crewhaus/sandbox";
42
+ import { runVerifierInSandbox } from "./sandboxed-eval";
43
+ export class HarnessSynthesizerError extends CrewhausError {
44
+ name = "HarnessSynthesizerError";
45
+ constructor(message, cause) {
46
+ super("config", message, cause);
47
+ }
48
+ }
49
+ /**
50
+ * The Critic: runs `code` against a sample set and returns the per-
51
+ * sample verdict and the heuristic value. Deterministic given the same
52
+ * code + samples (modulo the verifier's own determinism).
53
+ *
54
+ * SECURITY (FR-007): `code` is untrusted and is NOT run in this process.
55
+ * `runVerifier` ships `{ code, samples }` to a locked-down
56
+ * `@crewhaus/sandbox` container (`--network none`, `--read-only`, no
57
+ * host env, cpu/mem caps, wall-clock kill, `no-new-privileges`) and
58
+ * scores the returned verdicts here on the host — the container never
59
+ * receives `expected`. Inside the jail the candidate is compiled with
60
+ * the runtime's dynamic-function constructor and invoked over the
61
+ * samples; an exfiltration/RCE attempt (e.g. a refiner emitting a
62
+ * network call) is contained by the jail. FAIL CLOSED: when no real
63
+ * isolation backend is available (the `noop` backend, or none),
64
+ * `runVerifier` throws rather than executing untrusted code unsandboxed,
65
+ * mirroring the `requiresSandbox` floor enforced elsewhere in the repo.
66
+ *
67
+ * Behavior contract: per-sample runtime errors are caught (verdict
68
+ * `false`, `errors++`), matching the prior in-process semantics. Code
69
+ * that fails to compile, times out, or otherwise crashes the harness
70
+ * throws a `HarnessSynthesizerError`.
71
+ */
72
+ export async function runVerifier(code, samples, opts = {}) {
73
+ const ownsSandbox = opts.sandbox === undefined;
74
+ const sandbox = opts.sandbox ?? createSandbox();
75
+ try {
76
+ // FR-007 — fail closed: never run untrusted verifier code without
77
+ // a real isolation boundary.
78
+ if (sandbox.backend === "noop") {
79
+ throw new HarnessSynthesizerError("refusing to evaluate verifier code in a noop sandbox (no isolation); set CREWHAUS_SANDBOX=docker|podman");
80
+ }
81
+ return await runVerifierInSandbox(sandbox, code, samples, {
82
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
83
+ ...(opts.image !== undefined ? { image: opts.image } : {}),
84
+ });
85
+ }
86
+ finally {
87
+ if (ownsSandbox)
88
+ await sandbox.close();
89
+ }
90
+ }
91
+ /**
92
+ * Thompson sampling over a node population. Picks the index whose
93
+ * posterior sample is highest. Each node has a Beta(alpha, beta)
94
+ * posterior over its heuristic value; the alpha/beta are accumulated
95
+ * across iterations as the search refines.
96
+ */
97
+ export function thompsonPick(nodes, rng = Math.random) {
98
+ if (nodes.length === 0)
99
+ throw new HarnessSynthesizerError("thompsonPick called on empty list");
100
+ let bestIdx = 0;
101
+ let bestSample = Number.NEGATIVE_INFINITY;
102
+ for (let i = 0; i < nodes.length; i++) {
103
+ const n = nodes[i];
104
+ if (n === undefined)
105
+ continue;
106
+ const sample = betaSample(n.alpha, n.beta, rng);
107
+ if (sample > bestSample) {
108
+ bestSample = sample;
109
+ bestIdx = i;
110
+ }
111
+ }
112
+ return bestIdx;
113
+ }
114
+ /**
115
+ * Quick-and-deterministic Beta sample using two gamma samples
116
+ * (Marsaglia–Tsang). For the sizes we deal with (alpha, beta < 100),
117
+ * the approximation is fast and stable.
118
+ */
119
+ function betaSample(a, b, rng) {
120
+ const x = gammaSample(a, rng);
121
+ const y = gammaSample(b, rng);
122
+ return x / (x + y);
123
+ }
124
+ function gammaSample(shape, rng) {
125
+ // For shape >= 1 use Marsaglia-Tsang; for shape < 1 use Ahrens-Dieter.
126
+ if (shape < 1) {
127
+ // Use shape+1 then transform by U^(1/shape).
128
+ const x = gammaSample(shape + 1, rng);
129
+ const u = Math.max(rng(), 1e-12);
130
+ return x * u ** (1 / shape);
131
+ }
132
+ const d = shape - 1 / 3;
133
+ const c = 1 / Math.sqrt(9 * d);
134
+ // Loop until a valid sample.
135
+ for (let i = 0; i < 64; i++) {
136
+ let x;
137
+ let v;
138
+ do {
139
+ const u1 = Math.max(rng(), 1e-12);
140
+ const u2 = Math.max(rng(), 1e-12);
141
+ // Box-Muller for standard normal.
142
+ x = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
143
+ v = 1 + c * x;
144
+ } while (v <= 0);
145
+ v = v * v * v;
146
+ const u = rng();
147
+ if (u < 1 - 0.0331 * x * x * x * x)
148
+ return d * v;
149
+ if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v)))
150
+ return d * v;
151
+ }
152
+ // Fallback — extremely rare. Return the deterministic mean.
153
+ return shape;
154
+ }
155
+ /**
156
+ * Run the tree search. Returns the best candidate found, the
157
+ * iteration count, and whether the target heuristic was reached.
158
+ * Pure with respect to randomness: pass `rng` for determinism.
159
+ *
160
+ * One isolation backend is created (or reused, if `opts.sandbox` is
161
+ * supplied) for the whole search and closed on exit — see `runVerifier`.
162
+ */
163
+ export async function synthesizeVerifier(opts) {
164
+ if (opts.seedCandidates.length === 0) {
165
+ throw new HarnessSynthesizerError("at least one seed candidate is required");
166
+ }
167
+ if (opts.samples.length === 0) {
168
+ throw new HarnessSynthesizerError("at least one sample is required to score the verifier");
169
+ }
170
+ const target = opts.target ?? 1.0;
171
+ const rng = opts.rng ?? Math.random;
172
+ const maxIter = opts.maxIterations ?? 16;
173
+ // One isolation backend reused across the whole inner search; close it
174
+ // on exit only if we created it (the caller owns an injected one).
175
+ const ownsSandbox = opts.sandbox === undefined;
176
+ const sandbox = opts.sandbox ?? createSandbox();
177
+ const runOpts = {
178
+ sandbox,
179
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
180
+ };
181
+ try {
182
+ // Initialize the candidate pool from seeds.
183
+ const pool = [];
184
+ for (let i = 0; i < opts.seedCandidates.length; i++) {
185
+ const code = opts.seedCandidates[i];
186
+ const { heuristic } = await runVerifier(code, opts.samples, runOpts);
187
+ pool.push({
188
+ id: `seed_${i}`,
189
+ code,
190
+ score: heuristic,
191
+ heuristic,
192
+ // Beta starts uniform; update with observed correct/incorrect counts.
193
+ alpha: 1 + Math.round(heuristic * opts.samples.length),
194
+ beta: 1 + Math.round((1 - heuristic) * opts.samples.length),
195
+ });
196
+ }
197
+ const trajectory = [...pool];
198
+ // Early exit if a seed already satisfies the target.
199
+ let best = pool.reduce((a, b) => (a.heuristic >= b.heuristic ? a : b));
200
+ if (best.heuristic >= target) {
201
+ return { best, iterations: 0, converged: true, trajectory };
202
+ }
203
+ for (let iter = 0; iter < maxIter; iter++) {
204
+ const pickIdx = thompsonPick(pool, rng);
205
+ const parent = pool[pickIdx];
206
+ // Compute concrete failures for the refiner.
207
+ const { verdicts } = await runVerifier(parent.code, opts.samples, runOpts);
208
+ const failures = [];
209
+ for (let i = 0; i < opts.samples.length; i++) {
210
+ const s = opts.samples[i];
211
+ const v = verdicts[i];
212
+ if (v !== s.expected)
213
+ failures.push(s);
214
+ }
215
+ let newCode;
216
+ try {
217
+ newCode = await opts.refiner(parent, failures);
218
+ }
219
+ catch (err) {
220
+ throw new HarnessSynthesizerError(`refiner threw on iteration ${iter}: ${err.message}`, err);
221
+ }
222
+ const { heuristic } = await runVerifier(newCode, opts.samples, runOpts);
223
+ const child = {
224
+ id: `cand_${iter}`,
225
+ code: newCode,
226
+ score: heuristic,
227
+ heuristic,
228
+ alpha: 1 + Math.round(heuristic * opts.samples.length),
229
+ beta: 1 + Math.round((1 - heuristic) * opts.samples.length),
230
+ };
231
+ pool.push(child);
232
+ trajectory.push(child);
233
+ if (heuristic > best.heuristic)
234
+ best = child;
235
+ if (best.heuristic >= target) {
236
+ return { best, iterations: iter + 1, converged: true, trajectory };
237
+ }
238
+ }
239
+ return { best, iterations: maxIter, converged: false, trajectory };
240
+ }
241
+ finally {
242
+ if (ownsSandbox)
243
+ await sandbox.close();
244
+ }
245
+ }
246
+ /**
247
+ * `MutationProvider` adapter so verifier search can drop into the
248
+ * existing eval-optimizer-orchestrator loop. The provider's `next()`
249
+ * runs one iteration of the inner tree search and emits a
250
+ * prompt-edit that references the synthesized verifier.
251
+ *
252
+ * Typical wiring (programmatic): construct this provider with the
253
+ * spec's skill samples and a `refiner` function, then pass it to
254
+ * `optimizeSpec({ mutator: new VerifierMutationProvider(...) })`. The
255
+ * orchestrator runs the standard search loop, but each "mutation" is
256
+ * a freshly-synthesized verifier persisted to .crewhaus/verifiers/.
257
+ * (CLI `--mutator verifier-synthesis` wiring is a follow-up; the CLI
258
+ * today exposes `rule-based` and `claude` only.)
259
+ *
260
+ * SECURITY: the inner search executes refiner-produced code. Pass a
261
+ * `sandbox` (or rely on the default) — `synthesizeVerifier`/`runVerifier`
262
+ * isolate every evaluation and fail closed without a real backend. A
263
+ * model-backed `refiner` is only safe to wire because of that jail.
264
+ */
265
+ export class VerifierMutationProvider {
266
+ samples;
267
+ refiner;
268
+ seedCandidates;
269
+ maxInnerIterations;
270
+ sandbox;
271
+ name = "verifier-synthesis";
272
+ synthesisIterations = 0;
273
+ constructor(samples, refiner, seedCandidates, maxInnerIterations = 4, sandbox) {
274
+ this.samples = samples;
275
+ this.refiner = refiner;
276
+ this.seedCandidates = seedCandidates;
277
+ this.maxInnerIterations = maxInnerIterations;
278
+ this.sandbox = sandbox;
279
+ }
280
+ async next(state) {
281
+ this.synthesisIterations++;
282
+ const result = await synthesizeVerifier({
283
+ seedCandidates: this.seedCandidates,
284
+ samples: this.samples,
285
+ refiner: this.refiner,
286
+ maxIterations: this.maxInnerIterations,
287
+ ...(this.sandbox !== undefined ? { sandbox: this.sandbox } : {}),
288
+ });
289
+ const annotation = `\n\n[verifier ${result.best.id}, h=${result.best.heuristic.toFixed(3)}]`;
290
+ return {
291
+ prompt: state.best.prompt + annotation,
292
+ mutations: [{ kind: "rephrase-instruction" }],
293
+ rationale: `verifier-synthesis pass ${this.synthesisIterations}: ${result.best.id} reached heuristic ${result.best.heuristic.toFixed(3)} in ${result.iterations} inner iterations${result.converged ? " (converged)" : ""}`,
294
+ };
295
+ }
296
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * FR-007 — sandboxed evaluation of untrusted verifier code.
3
+ *
4
+ * This module is the ONLY place candidate verifier code is compiled and
5
+ * run. It is `tool-harness-synthesizer`'s trust boundary: `code` strings
6
+ * are attacker-influenceable (caller seeds + refiner output), so they are
7
+ * NEVER executed on this host. `runVerifierInSandbox` ships them to a
8
+ * locked-down `@crewhaus/sandbox` container (`--network none`,
9
+ * `--read-only`, no host env, cpu/mem caps, wall-clock kill,
10
+ * `no-new-privileges`) and scores the returned verdicts on the host —
11
+ * the container never receives the `expected` labels.
12
+ *
13
+ * The container runs `VERIFIER_HARNESS` (a fixed `node -e` script). The
14
+ * untrusted `code` is delivered as STDIN DATA, never interpolated into
15
+ * that script string — so it cannot break out of the harness.
16
+ */
17
+ import type { Sandbox } from "@crewhaus/sandbox";
18
+ import { type VerifierSample } from "./index";
19
+ /** Sentinel that frames the harness's single JSON result line on stdout. */
20
+ export declare const VERIFIER_SENTINEL = "__CREWHAUS_VERIFIER__";
21
+ /** Input/output pair handed to the verifier (labels stripped). */
22
+ type VerifierIO = {
23
+ readonly input: unknown;
24
+ readonly output: unknown;
25
+ };
26
+ /** Payload marshaled across the isolation boundary. */
27
+ type VerifierPayload = {
28
+ readonly code: string;
29
+ readonly samples: ReadonlyArray<VerifierIO>;
30
+ };
31
+ /** Per-sample result from inside the jail (or the in-process test double). */
32
+ export type VerifierEvalResult = {
33
+ readonly verdicts: ReadonlyArray<boolean>;
34
+ readonly errors: number;
35
+ } | {
36
+ readonly compileError: string;
37
+ };
38
+ /**
39
+ * Compile `payload.code` and run it over `payload.samples`, collecting a
40
+ * boolean verdict per sample. A compile failure returns `{ compileError }`;
41
+ * a per-sample runtime throw is caught (verdict `false`, `errors++`).
42
+ *
43
+ * Single source of truth for verifier scoring: it runs both (a) inside the
44
+ * container — `VERIFIER_HARNESS` embeds this function's own source via
45
+ * `.toString()` — and (b) in unit tests via a fake sandbox. It MUST stay
46
+ * self-contained: reference no imports or module scope, or the in-container
47
+ * copy throws a ReferenceError.
48
+ */
49
+ export declare function evalVerifierPayload(payload: VerifierPayload): VerifierEvalResult;
50
+ /**
51
+ * Marshal `{ code, samples }` (labels stripped) into the jail, run the
52
+ * harness, parse the framed result, and score verdicts against the
53
+ * host-held `expected` labels.
54
+ *
55
+ * Throws `HarnessSynthesizerError` on: non-serializable samples, compile
56
+ * failure, timeout, non-zero harness exit, or unparseable output. Caught
57
+ * per-sample runtime errors are reflected in `errors` (not thrown).
58
+ */
59
+ export declare function runVerifierInSandbox(sandbox: Sandbox, code: string, samples: ReadonlyArray<VerifierSample>, opts?: {
60
+ readonly timeoutMs?: number;
61
+ readonly image?: string;
62
+ }): Promise<{
63
+ readonly verdicts: ReadonlyArray<boolean>;
64
+ readonly heuristic: number;
65
+ readonly errors: number;
66
+ }>;
67
+ export {};