harnery 0.26.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/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/harness.d.ts.map +1 -1
- package/dist/commands/harness.js +2 -0
- package/dist/commands/work.d.ts +2 -0
- package/dist/commands/work.d.ts.map +1 -1
- package/dist/commands/work.js +47 -4
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +64 -0
- package/dist/core/harnesses/attest-projection.d.ts +44 -0
- package/dist/core/harnesses/attest-projection.d.ts.map +1 -0
- package/dist/core/harnesses/attest-projection.js +113 -0
- package/dist/core/harnesses/attest.d.ts +9 -0
- package/dist/core/harnesses/attest.d.ts.map +1 -1
- package/dist/core/harnesses/attest.js +14 -1
- package/dist/core/harnesses/attestation.d.ts +1 -1
- package/dist/core/harnesses/attestation.d.ts.map +1 -1
- package/dist/core/harnesses/attestation.js +7 -1
- package/dist/core/harnesses/bench.d.ts.map +1 -1
- package/dist/core/harnesses/bench.js +13 -0
- package/dist/core/harnesses/profiles.d.ts +7 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -1
- package/dist/core/harnesses/profiles.js +9 -0
- package/dist/core/harnesses/types.d.ts +19 -1
- package/dist/core/harnesses/types.d.ts.map +1 -1
- package/dist/core/harnesses/types.js +1 -0
- package/dist/core/work/index.d.ts +1 -1
- package/dist/core/work/index.d.ts.map +1 -1
- package/dist/core/work/index.js +1 -1
- package/dist/core/work/runner.d.ts.map +1 -1
- package/dist/core/work/runner.js +8 -1
- package/dist/core/work/state.d.ts +24 -1
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +108 -14
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +3 -6
- package/dist/core/workflow/attempt-context.d.ts.map +1 -1
- package/dist/core/workflow/attempt-context.js +38 -1
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +61 -19
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/journal.d.ts +30 -0
- package/dist/core/workflow/journal.d.ts.map +1 -0
- package/dist/core/workflow/journal.js +70 -0
- package/dist/core/workflow/proof.d.ts +3 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +1 -0
- package/dist/core/workflow/sandbox-projection.d.ts +57 -0
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -0
- package/dist/core/workflow/sandbox-projection.js +95 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +14 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +18 -2
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +14 -1
- package/dist/core/workflow/types.d.ts +62 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +2 -0
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +1 -0
- package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/inspect.js +14 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +50 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/reclaim.js +88 -0
- package/dist/core/workflow/workspaces/state.d.ts +1 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +9 -30
- package/dist/lib/browser/client.d.ts +50 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +103 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.d.ts.map +1 -1
- package/dist/lib/exec.js +6 -1
- package/dist/lib/tunnel/error-page.d.ts +15 -0
- package/dist/lib/tunnel/error-page.d.ts.map +1 -0
- package/dist/lib/tunnel/error-page.js +98 -0
- package/dist/lib/tunnel/gate.d.ts +1 -12
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +59 -4
- package/package.json +1 -1
- package/src/commands/browse.ts +33 -1
- package/src/commands/harness.ts +6 -0
- package/src/commands/work.ts +80 -13
- package/src/commands/workflow.ts +74 -0
- package/src/core/harnesses/attest-projection.ts +151 -0
- package/src/core/harnesses/attest.ts +23 -1
- package/src/core/harnesses/attestation.ts +7 -1
- package/src/core/harnesses/bench.ts +16 -0
- package/src/core/harnesses/profiles.ts +11 -0
- package/src/core/harnesses/types.ts +20 -0
- package/src/core/work/index.ts +1 -0
- package/src/core/work/runner.ts +8 -0
- package/src/core/work/state.ts +177 -27
- package/src/core/workflow/approvals.ts +3 -6
- package/src/core/workflow/attempt-context.ts +47 -1
- package/src/core/workflow/engine.ts +106 -33
- package/src/core/workflow/index.ts +1 -0
- package/src/core/workflow/journal.ts +90 -0
- package/src/core/workflow/proof.ts +4 -0
- package/src/core/workflow/sandbox-projection.ts +150 -0
- package/src/core/workflow/spawn-claude.ts +18 -1
- package/src/core/workflow/spawn-codex.ts +25 -2
- package/src/core/workflow/spawn-cursor.ts +18 -1
- package/src/core/workflow/types.ts +66 -0
- package/src/core/workflow/workspaces/index.ts +2 -0
- package/src/core/workflow/workspaces/inspect.ts +15 -0
- package/src/core/workflow/workspaces/reclaim.ts +109 -0
- package/src/core/workflow/workspaces/state.ts +13 -33
- package/src/lib/browser/client.ts +129 -0
- package/src/lib/exec.ts +10 -1
- package/src/lib/tunnel/error-page.ts +114 -0
- package/src/lib/tunnel/gate.ts +66 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"browse.d.ts","sourceRoot":"","sources":["../../src/commands/browse.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiJ1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAiQN"}
|
package/dist/commands/browse.js
CHANGED
|
@@ -55,6 +55,7 @@ export function registerBrowseCommand(program, emitParam, context) {
|
|
|
55
55
|
.option("--press <key>", "Press a key after navigation/fill (e.g., Enter)")
|
|
56
56
|
.option("--wait-for <selector>", "Wait for this selector before capturing output")
|
|
57
57
|
.option("--evaluate <js>", "Run JS in the page context after navigation; result printed to stdout")
|
|
58
|
+
.option("--capture-evaluate <js>", "In trio mode, evaluate JS inside the exact screenshot viewport immediately before capture; result is written as captureEval")
|
|
58
59
|
.option("--batch <steps>", "Run multiple steps in one session, semicolon-separated. Each step is one of: " +
|
|
59
60
|
"`click <selector>`, `fill <selector=>value>`, `press <key>`, `wait <selector|ms>`, `eval <js>`, `reload`. " +
|
|
60
61
|
'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state.')
|
|
@@ -312,6 +313,9 @@ async function runBrowse(url, opts, context) {
|
|
|
312
313
|
devOverlay = await captureDevOverlay(browser.currentPage);
|
|
313
314
|
}
|
|
314
315
|
if (printMode) {
|
|
316
|
+
if (opts.captureEvaluate) {
|
|
317
|
+
throw new Error("--capture-evaluate requires trio mode so its result can be bound to a screenshot.");
|
|
318
|
+
}
|
|
315
319
|
if (opts.baseline || opts.diff) {
|
|
316
320
|
throw new Error("--baseline / --diff require trio mode (screenshot file). Remove --snapshot/--html/--json or capture the screenshot first.");
|
|
317
321
|
}
|
|
@@ -551,11 +555,28 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
551
555
|
const skipScreenshot = opts.screenshot === false || opts.domOnly === true;
|
|
552
556
|
let pngPath;
|
|
553
557
|
let pngBytes;
|
|
558
|
+
let captureEvalResult;
|
|
559
|
+
let captureViewport;
|
|
560
|
+
let captureEvidence;
|
|
554
561
|
if (!skipScreenshot) {
|
|
555
562
|
pngPath = `${prefix}.png`;
|
|
556
|
-
|
|
563
|
+
if (opts.captureEvaluate) {
|
|
564
|
+
const captured = await browser.screenshotWithEvaluation(pngPath, opts.captureEvaluate, {
|
|
565
|
+
fullPage: opts.fullPage !== false,
|
|
566
|
+
});
|
|
567
|
+
pngBytes = captured.bytes;
|
|
568
|
+
captureEvalResult = captured.evaluation;
|
|
569
|
+
captureViewport = captured.viewport;
|
|
570
|
+
captureEvidence = captured.evidence;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
pngBytes = await browser.screenshot(pngPath, { fullPage: opts.fullPage !== false });
|
|
574
|
+
}
|
|
557
575
|
written.push(pngPath);
|
|
558
576
|
}
|
|
577
|
+
else if (opts.captureEvaluate) {
|
|
578
|
+
throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot / --dom-only.");
|
|
579
|
+
}
|
|
559
580
|
const htmlPath = `${prefix}.html`;
|
|
560
581
|
const html = await browser.htmlContent(opts.selector);
|
|
561
582
|
writeFileSync(htmlPath, html);
|
|
@@ -577,6 +598,11 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
577
598
|
envelope.screenshotBytes = pngBytes;
|
|
578
599
|
if (opts.evaluate)
|
|
579
600
|
envelope.eval = evalResult;
|
|
601
|
+
if (opts.captureEvaluate) {
|
|
602
|
+
envelope.captureEval = captureEvalResult;
|
|
603
|
+
envelope.captureViewport = captureViewport;
|
|
604
|
+
envelope.captureEvidence = captureEvidence;
|
|
605
|
+
}
|
|
580
606
|
if (opts.networkHar)
|
|
581
607
|
envelope.har = resolve(opts.networkHar);
|
|
582
608
|
if (visibility)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EAIxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/commands/harness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EAIxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAmBjE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA4JhF;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAgBhF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,MAAM,CAU9E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAkB7D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CA6BpE;AAgBD,mFAAmF;AACnF,YAAY,EAAE,WAAW,EAAE,CAAC"}
|
package/dist/commands/harness.js
CHANGED
|
@@ -69,6 +69,7 @@ export function registerHarnessCommand(program, emit) {
|
|
|
69
69
|
.option("--yes", "Confirm that this spends real vendor tokens")
|
|
70
70
|
.option("--subscription-only", "Scrub API-key vars so the child uses its stored login (repo default via config.jsonc workflow.subscriptionOnly)")
|
|
71
71
|
.option("--timeout <ms>", "Per-harness probe timeout in milliseconds")
|
|
72
|
+
.option("--projection", "Also probe whether a declared sandbox is enforced; costs two extra turns per capable harness")
|
|
72
73
|
.option("--json", "Machine-readable attestation report")
|
|
73
74
|
.action(async (harnesses, opts) => {
|
|
74
75
|
if (!opts.yes) {
|
|
@@ -94,6 +95,7 @@ export function registerHarnessCommand(program, emit) {
|
|
|
94
95
|
harnesses,
|
|
95
96
|
timeoutMs,
|
|
96
97
|
subscriptionOnly,
|
|
98
|
+
projection: opts.projection === true,
|
|
97
99
|
});
|
|
98
100
|
if (opts.json) {
|
|
99
101
|
emit.config({ format: "json" });
|
package/dist/commands/work.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { EmitContext } from "../commander.js";
|
|
3
|
+
import { type WorkAttempt } from "../core/work/index.js";
|
|
3
4
|
export declare function registerWorkCommand(program: Command, emit: EmitContext): void;
|
|
5
|
+
export declare function renderAttemptRow(attempt: WorkAttempt): string;
|
|
4
6
|
//# sourceMappingURL=work.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"work.d.ts","sourceRoot":"","sources":["../../src/commands/work.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAUnD,OAAO,EAUL,KAAK,WAAW,EAEjB,MAAM,uBAAuB,CAAC;AAsC/B,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA8F7E;AAgND,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAM7D"}
|
package/dist/commands/work.js
CHANGED
|
@@ -4,6 +4,7 @@ import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
|
|
|
4
4
|
import { loadPolicyFile } from "../core/policy/index.js";
|
|
5
5
|
import { acceptWorkItem, cancelWorkItem, createWorkItem, listWorkItems, readWorkItem, reconcileAllWorkItems, reconcileWorkItem, reopenWorkItem, runWorkItem, } from "../core/work/index.js";
|
|
6
6
|
import { WorkflowParkedError } from "../core/workflow/index.js";
|
|
7
|
+
const MAX_ATTEMPT_JOURNAL_ERROR_RENDER = 240;
|
|
7
8
|
export function registerWorkCommand(program, emit) {
|
|
8
9
|
const registry = createBuiltinHarnessRegistry();
|
|
9
10
|
const harnesses = registry.ids();
|
|
@@ -182,14 +183,43 @@ function registerGovernanceCommand(work, name, emit) {
|
|
|
182
183
|
: "Explicitly reopen terminal or blocked work for a future attempt.")
|
|
183
184
|
.option("--actor <name>", "Actor recorded in the governance receipt")
|
|
184
185
|
.option("--reason <text>", "Bounded decision reason")
|
|
185
|
-
.option("--json", "Emit the complete work record as JSON")
|
|
186
|
-
|
|
186
|
+
.option("--json", "Emit the complete work record as JSON");
|
|
187
|
+
const command = work.commands.at(-1);
|
|
188
|
+
if (name === "reopen") {
|
|
189
|
+
command.option("--finding <text>", "Correction the next attempt must address (repeatable)", collectValue, []);
|
|
190
|
+
}
|
|
191
|
+
if (name === "accept") {
|
|
192
|
+
command.option("--dispose <id=outcome[:reason]>", "Dispose of an open finding: fixed, or deferred:<reason> (repeatable)", collectValue, []);
|
|
193
|
+
}
|
|
194
|
+
command.action((workId, opts) => {
|
|
187
195
|
withWorkRoot(emit, (coordRoot) => {
|
|
188
196
|
const fn = name === "accept" ? acceptWorkItem : name === "cancel" ? cancelWorkItem : reopenWorkItem;
|
|
189
|
-
emitWork(fn(coordRoot, workId,
|
|
197
|
+
emitWork(fn(coordRoot, workId, {
|
|
198
|
+
...opts,
|
|
199
|
+
...(opts.finding?.length ? { findings: opts.finding } : {}),
|
|
200
|
+
...(opts.dispose?.length ? { dispositions: opts.dispose.map(parseDisposition) } : {}),
|
|
201
|
+
}), opts.json, emit, true);
|
|
190
202
|
});
|
|
191
203
|
});
|
|
192
204
|
}
|
|
205
|
+
function collectValue(value, previous) {
|
|
206
|
+
return [...previous, value];
|
|
207
|
+
}
|
|
208
|
+
/** `<id>=fixed` or `<id>=deferred:<reason>`. Deferring without a reason is
|
|
209
|
+
* rejected downstream, so a lapsed finding cannot look like a decision. */
|
|
210
|
+
function parseDisposition(input) {
|
|
211
|
+
const split = input.indexOf("=");
|
|
212
|
+
if (split <= 0)
|
|
213
|
+
throw new Error(`invalid --dispose ${JSON.stringify(input)}; expected <id>=<outcome>`);
|
|
214
|
+
const id = input.slice(0, split).trim();
|
|
215
|
+
const rest = input.slice(split + 1).trim();
|
|
216
|
+
const [outcome, ...reasonParts] = rest.split(":");
|
|
217
|
+
if (outcome !== "fixed" && outcome !== "deferred") {
|
|
218
|
+
throw new Error(`invalid --dispose outcome ${JSON.stringify(outcome)}; expected fixed or deferred`);
|
|
219
|
+
}
|
|
220
|
+
const reason = reasonParts.join(":").trim();
|
|
221
|
+
return { id, outcome, ...(reason ? { reason } : {}) };
|
|
222
|
+
}
|
|
193
223
|
function emitWork(record, json, emit, detail = false) {
|
|
194
224
|
if (json) {
|
|
195
225
|
emit.config({ format: "json" });
|
|
@@ -215,7 +245,7 @@ function emitWork(record, json, emit, detail = false) {
|
|
|
215
245
|
lines.push("acceptance:", ...record.intent.acceptance.map((item) => ` - ${item}`));
|
|
216
246
|
}
|
|
217
247
|
if (projection.attempts.length) {
|
|
218
|
-
lines.push("attempts:", ...projection.attempts.map(
|
|
248
|
+
lines.push("attempts:", ...projection.attempts.map(renderAttemptRow));
|
|
219
249
|
}
|
|
220
250
|
if (projection.approval_id)
|
|
221
251
|
lines.push(`approval: ${projection.approval_id}`);
|
|
@@ -223,6 +253,19 @@ function emitWork(record, json, emit, detail = false) {
|
|
|
223
253
|
lines.push(`proof: ${projection.proof_path}`);
|
|
224
254
|
emit.text(`${lines.join("\n")}\n`);
|
|
225
255
|
}
|
|
256
|
+
export function renderAttemptRow(attempt) {
|
|
257
|
+
const statusDetail = attempt.status === "journal_unreadable"
|
|
258
|
+
? `: ${renderAttemptJournalError(attempt.journal_error)}`
|
|
259
|
+
: "";
|
|
260
|
+
return ` ${attempt.number}. ${attempt.run_id} ${attempt.status}${statusDetail}`;
|
|
261
|
+
}
|
|
262
|
+
function renderAttemptJournalError(error) {
|
|
263
|
+
const normalized = (error ?? "").replace(/\s+/g, " ").trim();
|
|
264
|
+
const value = normalized || "unknown journal read error";
|
|
265
|
+
if (value.length <= MAX_ATTEMPT_JOURNAL_ERROR_RENDER)
|
|
266
|
+
return value;
|
|
267
|
+
return `${value.slice(0, MAX_ATTEMPT_JOURNAL_ERROR_RENDER - 3).trimEnd()}...`;
|
|
268
|
+
}
|
|
226
269
|
function renderWorkRow(record) {
|
|
227
270
|
const projection = record.projection;
|
|
228
271
|
return `${record.intent.id} ${projection.state.padEnd(17)} ${projection.attempts_used}/${record.intent.max_attempts} ${projection.next_action.padEnd(21)} ${record.intent.title}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAyEnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmuBjF"}
|
|
@@ -459,6 +459,70 @@ export function registerWorkflowCommand(program, emit) {
|
|
|
459
459
|
process.exit(1);
|
|
460
460
|
}
|
|
461
461
|
});
|
|
462
|
+
workflow
|
|
463
|
+
.command("reclaim <run-id>")
|
|
464
|
+
.description("Resolve a workspace preserved because it was dirty: salvage the work to its branch, then release.")
|
|
465
|
+
.option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
|
|
466
|
+
.option("--discard", "Throw the uncommitted work away instead of salvaging it")
|
|
467
|
+
.option("--json", "Emit the reclaim preparation and cleanup result as JSON")
|
|
468
|
+
.action(async (runId, opts) => {
|
|
469
|
+
const coordRoot = findCoordRoot();
|
|
470
|
+
if (!coordRoot) {
|
|
471
|
+
emit.error({
|
|
472
|
+
code: "no_coord_root",
|
|
473
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
474
|
+
});
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
if (!opts.yes) {
|
|
478
|
+
emit.error({
|
|
479
|
+
code: "reclaim_confirmation_required",
|
|
480
|
+
message: opts.discard
|
|
481
|
+
? "reclaim --discard permanently destroys uncommitted work; pass --yes to confirm"
|
|
482
|
+
: "workspace reclaim commits uncommitted work to its branch and then releases the worktree; pass --yes to confirm",
|
|
483
|
+
});
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const { inspectWorkflowWorkspace, prepareReclaim, cleanupWorkspace } = await import("../core/workflow/index.js");
|
|
488
|
+
const inspection = inspectWorkflowWorkspace(coordRoot, runId);
|
|
489
|
+
if (!inspection.ok) {
|
|
490
|
+
emit.error({ code: "workflow_reclaim_failed", message: inspection.error });
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
const allocation = inspection.value.allocation;
|
|
494
|
+
if (!allocation) {
|
|
495
|
+
emit.error({
|
|
496
|
+
code: "workflow_reclaim_unavailable",
|
|
497
|
+
message: `run ${runId} has no provider-owned Git workspace to reclaim`,
|
|
498
|
+
});
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
const preparation = prepareReclaim({
|
|
502
|
+
worktreePath: allocation.active_root,
|
|
503
|
+
mode: opts.discard ? "discard" : "salvage",
|
|
504
|
+
runId,
|
|
505
|
+
});
|
|
506
|
+
// Cleanup still owns removal. Reclaim only changes whether the tree it
|
|
507
|
+
// finds is dirty, so there is one audited release path, not two.
|
|
508
|
+
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
509
|
+
const cleanup = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
510
|
+
if (opts.json) {
|
|
511
|
+
emit.config({ format: "json" });
|
|
512
|
+
emit.data({ preparation, cleanup });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
emit.text(`reclaim ${preparation.action}: ${preparation.detail}\n` +
|
|
516
|
+
`workspace cleanup ${cleanup.status}: ${cleanup.binding_id}\n`);
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
emit.error({
|
|
520
|
+
code: "workflow_reclaim_failed",
|
|
521
|
+
message: error instanceof Error ? error.message : String(error),
|
|
522
|
+
});
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
462
526
|
const approvals = workflow
|
|
463
527
|
.command("approvals")
|
|
464
528
|
.description("Inspect and resolve durable workflow policy approvals.");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live probe for the `filesystemPolicyProjection` capability (ADR 0041).
|
|
3
|
+
*
|
|
4
|
+
* Every other attested dimension can be read off a single successful turn: the
|
|
5
|
+
* result either carries a session id or it does not. Projection cannot. A
|
|
6
|
+
* sandbox that is declared but not enforced looks exactly like a sandbox that is
|
|
7
|
+
* enforced, because in both cases the CLI accepts the flag and exits zero. The
|
|
8
|
+
* only way to tell them apart is to give a child something to write and look at
|
|
9
|
+
* the filesystem afterwards.
|
|
10
|
+
*
|
|
11
|
+
* That introduces its own confound. If the treatment child never attempts the
|
|
12
|
+
* write, the sentinel is absent for a reason that has nothing to do with the
|
|
13
|
+
* sandbox, and absence would be read as enforcement. So the probe runs a control
|
|
14
|
+
* first, under a mode that permits the write. Only a control that actually
|
|
15
|
+
* produced its sentinel licenses any reading of the treatment run; otherwise the
|
|
16
|
+
* probe reports inconclusive and records nothing, which is the same prerequisite
|
|
17
|
+
* rule the main attestation applies to a failed turn.
|
|
18
|
+
*/
|
|
19
|
+
import type { SpawnRequest, SpawnResult } from "../workflow/types.js";
|
|
20
|
+
import type { HarnessAdapter } from "./types.js";
|
|
21
|
+
export declare function projectionPrompt(sentinel: string): string;
|
|
22
|
+
export type ProjectionObservation = "supported" | "unsupported" | "inconclusive";
|
|
23
|
+
export interface ProjectionProbeResult {
|
|
24
|
+
observation: ProjectionObservation;
|
|
25
|
+
/** Why, in one line, suitable for an attestation note or a report row. */
|
|
26
|
+
detail: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProjectionProbeOptions {
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
subscriptionOnly: boolean;
|
|
31
|
+
/** Test seam. Defaults to the adapter's production spawner. */
|
|
32
|
+
spawn?: (request: SpawnRequest) => Promise<SpawnResult>;
|
|
33
|
+
/** Test seam. Defaults to a fresh directory under the OS temp root. */
|
|
34
|
+
workdir?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run the control/treatment pair and report what the filesystem showed.
|
|
38
|
+
*
|
|
39
|
+
* Returns `inconclusive` rather than throwing for every expected failure, so a
|
|
40
|
+
* probe that cannot reach a verdict degrades into "nothing recorded" instead of
|
|
41
|
+
* failing the whole attestation sweep.
|
|
42
|
+
*/
|
|
43
|
+
export declare function probeFilesystemProjection(adapter: HarnessAdapter, opts: ProjectionProbeOptions): Promise<ProjectionProbeResult>;
|
|
44
|
+
//# sourceMappingURL=attest-projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attest-projection.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAWjD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;AAEjF,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,qBAAqB,CAAC;IACnC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxD,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,qBAAqB,CAAC,CAoDhC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live probe for the `filesystemPolicyProjection` capability (ADR 0041).
|
|
3
|
+
*
|
|
4
|
+
* Every other attested dimension can be read off a single successful turn: the
|
|
5
|
+
* result either carries a session id or it does not. Projection cannot. A
|
|
6
|
+
* sandbox that is declared but not enforced looks exactly like a sandbox that is
|
|
7
|
+
* enforced, because in both cases the CLI accepts the flag and exits zero. The
|
|
8
|
+
* only way to tell them apart is to give a child something to write and look at
|
|
9
|
+
* the filesystem afterwards.
|
|
10
|
+
*
|
|
11
|
+
* That introduces its own confound. If the treatment child never attempts the
|
|
12
|
+
* write, the sentinel is absent for a reason that has nothing to do with the
|
|
13
|
+
* sandbox, and absence would be read as enforcement. So the probe runs a control
|
|
14
|
+
* first, under a mode that permits the write. Only a control that actually
|
|
15
|
+
* produced its sentinel licenses any reading of the treatment run; otherwise the
|
|
16
|
+
* probe reports inconclusive and records nothing, which is the same prerequisite
|
|
17
|
+
* rule the main attestation applies to a failed turn.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
/** Distinct names so a control artifact can never be mistaken for a treatment
|
|
23
|
+
* one, even if a child writes both or the directory is reused. */
|
|
24
|
+
const CONTROL_SENTINEL = "harnery-projection-control.txt";
|
|
25
|
+
const TREATMENT_SENTINEL = "harnery-projection-treatment.txt";
|
|
26
|
+
/** A child needs a few turns to run a shell command and report back; one turn is
|
|
27
|
+
* not enough to both act and answer. */
|
|
28
|
+
const PROJECTION_MAX_TURNS = 4;
|
|
29
|
+
export function projectionPrompt(sentinel) {
|
|
30
|
+
return `Create a file named ${sentinel} in the current directory containing the text ok. Then reply with the single word: done`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run the control/treatment pair and report what the filesystem showed.
|
|
34
|
+
*
|
|
35
|
+
* Returns `inconclusive` rather than throwing for every expected failure, so a
|
|
36
|
+
* probe that cannot reach a verdict degrades into "nothing recorded" instead of
|
|
37
|
+
* failing the whole attestation sweep.
|
|
38
|
+
*/
|
|
39
|
+
export async function probeFilesystemProjection(adapter, opts) {
|
|
40
|
+
if (!adapter.profile.sandboxProjection) {
|
|
41
|
+
// Nothing to observe: the adapter refuses a projection before launch, which
|
|
42
|
+
// is a fact about our own code and is already covered by unit tests. Spending
|
|
43
|
+
// a vendor turn here would attest nothing.
|
|
44
|
+
return {
|
|
45
|
+
observation: "inconclusive",
|
|
46
|
+
detail: "the adapter declares no sandbox projection, so there is nothing to observe live",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const spawn = opts.spawn ?? ((request) => adapter.spawn(request));
|
|
50
|
+
const ownsWorkdir = !opts.workdir;
|
|
51
|
+
const workdir = opts.workdir ?? mkdtempSync(join(tmpdir(), "harnery-projection-"));
|
|
52
|
+
try {
|
|
53
|
+
const control = await runTurn(spawn, workdir, CONTROL_SENTINEL, "workspace-write", opts);
|
|
54
|
+
if (!control.ok) {
|
|
55
|
+
return {
|
|
56
|
+
observation: "inconclusive",
|
|
57
|
+
detail: `the control turn did not complete (${control.detail})`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!existsSync(join(workdir, CONTROL_SENTINEL))) {
|
|
61
|
+
return {
|
|
62
|
+
observation: "inconclusive",
|
|
63
|
+
detail: "the control child did not create its file even though writing was permitted, so an absent treatment file would prove nothing",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const treatment = await runTurn(spawn, workdir, TREATMENT_SENTINEL, "read-only", opts);
|
|
67
|
+
// A read-only child may well fail its turn: refusing the write is the point.
|
|
68
|
+
// So unlike the control, a failed treatment turn is still evidence, and only
|
|
69
|
+
// the filesystem decides.
|
|
70
|
+
const wrote = existsSync(join(workdir, TREATMENT_SENTINEL));
|
|
71
|
+
if (wrote) {
|
|
72
|
+
return {
|
|
73
|
+
observation: "unsupported",
|
|
74
|
+
detail: "the child wrote under a read-only projection, so the declared mode is not enforced",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
observation: "supported",
|
|
79
|
+
detail: `a read-only projection blocked a write the same child performed when permitted${treatment.ok ? "" : ` (treatment turn also reported: ${treatment.detail})`}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (ownsWorkdir)
|
|
84
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function runTurn(spawn, cwd, sentinel, mode, opts) {
|
|
88
|
+
try {
|
|
89
|
+
const result = await spawn({
|
|
90
|
+
prompt: projectionPrompt(sentinel),
|
|
91
|
+
timeoutMs: opts.timeoutMs,
|
|
92
|
+
maxTurns: PROJECTION_MAX_TURNS,
|
|
93
|
+
cwd,
|
|
94
|
+
subscriptionOnly: opts.subscriptionOnly,
|
|
95
|
+
filesystemPolicy: { mode },
|
|
96
|
+
});
|
|
97
|
+
return { ok: result.ok, detail: result.ok ? "completed" : boundedDetail(result.error) };
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return { ok: false, detail: boundedDetail(error.message) };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const MAX_DETAIL_CHARS = 160;
|
|
104
|
+
/** Same tail-preserving rule as the main probe: a CLI prints its banner first
|
|
105
|
+
* and the reason it failed last. */
|
|
106
|
+
function boundedDetail(reason) {
|
|
107
|
+
if (!reason)
|
|
108
|
+
return "no error reported";
|
|
109
|
+
const collapsed = reason.replace(/\s+/g, " ").trim();
|
|
110
|
+
if (!collapsed)
|
|
111
|
+
return "no error reported";
|
|
112
|
+
return collapsed.length > MAX_DETAIL_CHARS ? `…${collapsed.slice(-MAX_DETAIL_CHARS)}` : collapsed;
|
|
113
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* parallel test rig.
|
|
7
7
|
*/
|
|
8
8
|
import type { SpawnResult } from "../workflow/types.js";
|
|
9
|
+
import { probeFilesystemProjection } from "./attest-projection.js";
|
|
9
10
|
import type { AttestableDimension, HarnessAttestation } from "./attestation.js";
|
|
10
11
|
import type { HarnessRegistry } from "./registry.js";
|
|
11
12
|
import type { CapabilitySupport, HarnessId } from "./types.js";
|
|
@@ -47,6 +48,14 @@ export interface RunHarnessAttestationOptions {
|
|
|
47
48
|
spawn?: (harness: HarnessId, prompt: string, timeoutMs: number) => Promise<SpawnResult>;
|
|
48
49
|
/** Test seam. Defaults to writing under the coord root. */
|
|
49
50
|
persist?: (record: HarnessAttestation) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Also probe `filesystemPolicyProjection` (ADR 0041). Off by default because
|
|
53
|
+
* it costs two extra turns per capable harness, against one for everything
|
|
54
|
+
* else: the observation needs a control run to be readable at all.
|
|
55
|
+
*/
|
|
56
|
+
projection?: boolean;
|
|
57
|
+
/** Test seam for the projection probe. */
|
|
58
|
+
probeProjection?: typeof probeFilesystemProjection;
|
|
50
59
|
now?: () => Date;
|
|
51
60
|
}
|
|
52
61
|
export declare function runHarnessAttestation(registry: HarnessRegistry, opts?: RunHarnessAttestationOptions): Promise<HarnessAttestationReport>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAQhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE/D;+CAC+C;AAC/C,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAqBtD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB;8DAC0D;IAC1D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;mDAG+C;IAC/C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxF,2DAA2D;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,wBAAwB,CAAC,
|
|
1
|
+
{"version":3,"file":"attest.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAQhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE/D;+CAC+C;AAC/C,eAAO,MAAM,kBAAkB,mCAAmC,CAAC;AAEnE,eAAO,MAAM,8BAA8B,SAAU,CAAC;AAqBtD,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB;8DAC0D;IAC1D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;mDAG+C;IAC/C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IACxF,2DAA2D;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,OAAO,yBAAyB,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,wBAAwB,CAAC,CAyGnC"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* uses, so what gets attested is the path production takes rather than a
|
|
6
6
|
* parallel test rig.
|
|
7
7
|
*/
|
|
8
|
+
import { probeFilesystemProjection } from "./attest-projection.js";
|
|
8
9
|
import { ATTESTATION_SCHEMA_VERSION, profileDigest, sealAttestation, writeAttestation, } from "./attestation.js";
|
|
9
10
|
import { probeBinaryVersion } from "./bench.js";
|
|
10
11
|
/** Fixed and content-free, so nothing user-supplied can reach the record and
|
|
@@ -88,6 +89,18 @@ export async function runHarnessAttestation(registry, opts = {}) {
|
|
|
88
89
|
sessionId: result.sessionId ? "supported" : "unsupported",
|
|
89
90
|
cost: result.costUsd !== undefined ? "supported" : "unsupported",
|
|
90
91
|
};
|
|
92
|
+
let projectionNote = "";
|
|
93
|
+
if (opts.projection) {
|
|
94
|
+
const probe = opts.probeProjection ?? probeFilesystemProjection;
|
|
95
|
+
const outcome = await probe(adapter, { timeoutMs, subscriptionOnly });
|
|
96
|
+
// An inconclusive probe records nothing for the dimension, leaving the
|
|
97
|
+
// declaration to stand on its own rather than dressing a non-observation
|
|
98
|
+
// as an observation.
|
|
99
|
+
if (outcome.observation !== "inconclusive") {
|
|
100
|
+
observations.filesystemPolicyProjection = outcome.observation;
|
|
101
|
+
}
|
|
102
|
+
projectionNote = `; projection ${outcome.observation}: ${outcome.detail}`;
|
|
103
|
+
}
|
|
91
104
|
const record = sealAttestation({
|
|
92
105
|
schema_version: ATTESTATION_SCHEMA_VERSION,
|
|
93
106
|
harness: id,
|
|
@@ -107,7 +120,7 @@ export async function runHarnessAttestation(registry, opts = {}) {
|
|
|
107
120
|
binaryVersion,
|
|
108
121
|
observations,
|
|
109
122
|
durationMs: result.durationMs,
|
|
110
|
-
note: `observed on ${binaryVersion}`,
|
|
123
|
+
note: `observed on ${binaryVersion}${projectionNote}`,
|
|
111
124
|
});
|
|
112
125
|
}
|
|
113
126
|
return {
|
|
@@ -16,7 +16,7 @@ export declare const ATTESTATION_SCHEMA_VERSION = 2;
|
|
|
16
16
|
/** Dimensions one minimal live turn can honestly establish. Everything else
|
|
17
17
|
* needs a purpose-built scenario and stays outside the record rather than
|
|
18
18
|
* being guessed at. */
|
|
19
|
-
export declare const ATTESTABLE_DIMENSIONS: readonly ["invocation", "finalResult", "sessionId", "cost"];
|
|
19
|
+
export declare const ATTESTABLE_DIMENSIONS: readonly ["invocation", "finalResult", "sessionId", "cost", "filesystemPolicyProjection"];
|
|
20
20
|
export type AttestableDimension = (typeof ATTESTABLE_DIMENSIONS)[number];
|
|
21
21
|
export interface HarnessAttestation {
|
|
22
22
|
schema_version: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/E,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;uBAEuB;AACvB,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE/E,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;uBAEuB;AACvB,eAAO,MAAM,qBAAqB,2FAMxB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,SAAS,CAAC;IACnB;kFAC8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB;kDAC8C;IAC9C,cAAc,EAAE,MAAM,CAAC;IACvB;;8DAE0D;IAC1D,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB;6DACyD;IACzD,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACtE,kEAAkE;IAClE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,uBAA4B,GAAG,MAAM,CAI1E;AAED;wDACwD;AACxD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAM7D;AAMD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAChD,kBAAkB,CAEpB;AASD;;aAEa;AACb,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,GAAE,uBAA4B,GACjC,MAAM,CAmBR;AAED;;kEAEkE;AAClE,wBAAgB,eAAe,CAC7B,OAAO,EAAE,SAAS,EAClB,IAAI,GAAE,uBAA4B,GACjC,kBAAkB,GAAG,IAAI,CAc3B;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,SAAS,GAClB,kBAAkB,GAAG,IAAI,CAe3B;AAED;uBACuB;AACvB,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,kBAAkB,GAAG,IAAI,EACjC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,OAAO,EAAE,cAAc,EACvB,gBAAgB,CAAC,EAAE,OAAO,GACzB,MAAM,IAAI,kBAAkB,CAK9B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,kBAAkB,EAAE,CAczF;AAED;;;oDAGoD;AACpD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,IAAI,EAAE,uBAAuB,GAAG;IAC9B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAChD;wCACoC;IACpC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GACA;IACD,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC,cAAc,CAAC,CAAA;KAAE,CAAC,CAAC;IAClG,mBAAmB,EAAE,MAAM,CACzB,MAAM,EACN;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CACvE,CAAC;CACH,CAoCA"}
|
|
@@ -19,7 +19,13 @@ export const ATTESTATION_SCHEMA_VERSION = 2;
|
|
|
19
19
|
/** Dimensions one minimal live turn can honestly establish. Everything else
|
|
20
20
|
* needs a purpose-built scenario and stays outside the record rather than
|
|
21
21
|
* being guessed at. */
|
|
22
|
-
export const ATTESTABLE_DIMENSIONS = [
|
|
22
|
+
export const ATTESTABLE_DIMENSIONS = [
|
|
23
|
+
"invocation",
|
|
24
|
+
"finalResult",
|
|
25
|
+
"sessionId",
|
|
26
|
+
"cost",
|
|
27
|
+
"filesystemPolicyProjection",
|
|
28
|
+
];
|
|
23
29
|
export function attestationsDir(opts = {}) {
|
|
24
30
|
const root = opts.coordRoot ?? monorepoRoot();
|
|
25
31
|
if (!root)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/bench.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuB,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EAEjB,0BAA0B,EAC1B,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,0BAA0B,CAAC;AAEjG;;;;;;wEAMwE;AACxE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACtC;sDACkD;IAClD,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD;qDACiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,kBAAkB,GAAG,IAAI,CAAC;IACtE;wDACoD;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAkDD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,mBAAwB,GAC7B,kBAAkB,CAwEpB;
|
|
1
|
+
{"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/bench.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAuB,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,iBAAiB,EAEjB,0BAA0B,EAC1B,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,GACT,OAAO,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,QAAQ,GAAG,UAAU,GAAG,0BAA0B,CAAC;AAEjG;;;;;;wEAMwE;AACxE,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,iBAAiB,GAAG,gBAAgB,CAAC;IAC/C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACtC;sDACkD;IAClD,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnD,4EAA4E;IAC5E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACjD;qDACiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,KAAK,kBAAkB,GAAG,IAAI,CAAC;IACtE;wDACoD;IACpD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAkDD,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,eAAe,EACzB,IAAI,GAAE,mBAAwB,GAC7B,kBAAkB,CAwEpB;AA2LD;;kDAEkD;AAClD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIhE"}
|
|
@@ -186,6 +186,18 @@ function observeAdapter(adapter) {
|
|
|
186
186
|
catch {
|
|
187
187
|
planningFailed = true;
|
|
188
188
|
}
|
|
189
|
+
// Plan a second invocation carrying a filesystem policy. Offline this can only
|
|
190
|
+
// show whether the adapter *renders* the projection, never whether the vendor
|
|
191
|
+
// enforces it; enforcement needs the live probe (ADR 0041). An adapter that
|
|
192
|
+
// declares no projection throws here, which is the correct rendering.
|
|
193
|
+
let projectionRendered = false;
|
|
194
|
+
try {
|
|
195
|
+
const projected = adapter.buildInvocation({ ...request, filesystemPolicy: { mode: "read-only" } }, "/harnery-bench/final.txt").argv;
|
|
196
|
+
projectionRendered = projected.join(" ") !== argv.join(" ");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
projectionRendered = false;
|
|
200
|
+
}
|
|
189
201
|
let normalized = null;
|
|
190
202
|
try {
|
|
191
203
|
normalized = adapter.normalizeResult(adapter.fixture.raw);
|
|
@@ -221,6 +233,7 @@ function observeAdapter(adapter) {
|
|
|
221
233
|
: costObserved),
|
|
222
234
|
toolEvidence: fromAdapter(normalized && "toolEvidence" in normalized ? "supported" : "unsupported"),
|
|
223
235
|
policyMapping: NOT_CHECKED,
|
|
236
|
+
filesystemPolicyProjection: fromAdapter(projectionRendered ? "supported" : "unsupported"),
|
|
224
237
|
interruption: NOT_CHECKED,
|
|
225
238
|
streaming: NOT_CHECKED,
|
|
226
239
|
steering: NOT_CHECKED,
|
|
@@ -34,6 +34,13 @@ export declare const BUILTIN_HARNESS_PROFILES: {
|
|
|
34
34
|
readonly date: "2026-07-25";
|
|
35
35
|
readonly version: "codex-cli 0.144.5";
|
|
36
36
|
};
|
|
37
|
+
readonly sandboxProjection: {
|
|
38
|
+
readonly modes: {
|
|
39
|
+
readonly "read-only": "read-only";
|
|
40
|
+
readonly "workspace-write": "workspace-write";
|
|
41
|
+
};
|
|
42
|
+
readonly writableRoots: true;
|
|
43
|
+
};
|
|
37
44
|
readonly capabilities: HarnessCapabilities;
|
|
38
45
|
};
|
|
39
46
|
readonly cursor: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../../src/core/harnesses/profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAgCvF;gFACgF;AAChF,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Fc,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,wBAAwB,CAAC;AAErE,eAAO,MAAM,mBAAmB,iDAE/B,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,gBAAgB,CAEnE;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAE5E;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAUlF"}
|
|
@@ -13,6 +13,7 @@ function capabilities(overrides) {
|
|
|
13
13
|
cost: unsupported(),
|
|
14
14
|
toolEvidence: unsupported("The final-result adapter does not retain tool events."),
|
|
15
15
|
policyMapping: unsupported("No ALLOW/DENY/ASK translation at the workflow boundary."),
|
|
16
|
+
filesystemPolicyProjection: unsupported("The adapter declares no sandbox projection."),
|
|
16
17
|
interruption: partial("Timeout kills the subprocess; no caller-driven interrupt handle."),
|
|
17
18
|
streaming: unsupported("Workflow children return one normalized final result."),
|
|
18
19
|
steering: unsupported("One prompt is fixed at subprocess launch."),
|
|
@@ -62,8 +63,16 @@ export const BUILTIN_HARNESS_PROFILES = {
|
|
|
62
63
|
modelFamily: "gpt",
|
|
63
64
|
effortValues: ["none", "minimal", "low", "medium", "high", "xhigh"],
|
|
64
65
|
verified: { date: "2026-07-25", version: "codex-cli 0.144.5" },
|
|
66
|
+
// Verified against codex-cli 0.144.5: `--sandbox <mode>` plus
|
|
67
|
+
// `sandbox_workspace_write.writable_roots`. Without the writable-root entry
|
|
68
|
+
// a workspace-write child still cannot write a repository's .git directory.
|
|
69
|
+
sandboxProjection: {
|
|
70
|
+
modes: { "read-only": "read-only", "workspace-write": "workspace-write" },
|
|
71
|
+
writableRoots: true,
|
|
72
|
+
},
|
|
65
73
|
capabilities: capabilities({
|
|
66
74
|
effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
|
|
75
|
+
filesystemPolicyProjection: supported("Mode renders to --sandbox; writable roots to sandbox_workspace_write.writable_roots."),
|
|
67
76
|
maxTurns: unsupported("codex exec exposes no turn-ceiling flag."),
|
|
68
77
|
sessionId: unsupported("--output-last-message carries no session id."),
|
|
69
78
|
cost: unsupported("The final-message path carries no usage or cost."),
|