humanish 0.109.1 → 0.110.1
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/ARCHITECTURE.md +14 -11
- package/CHANGELOG.md +24 -0
- package/CONTEXT.md +8 -6
- package/README.md +97 -48
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +11 -0
- package/dist/actors/local-agent/cli.js +103 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +18 -11
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +96 -44
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +89 -67
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/commands/study-format.js +29 -2
- package/dist/cli/commands/study-route-computer-use.js +2 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +2 -0
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/study.js +4 -3
- package/dist/cli/commands/tui.js +4 -3
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/doctor.d.ts +7 -2
- package/dist/cli/doctor.js +47 -8
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +316 -0
- package/dist/cli/first-run-path.d.ts +5 -4
- package/dist/cli/first-run-path.js +14 -7
- package/dist/cli/io.d.ts +17 -3
- package/dist/cli/io.js +22 -4
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/comms/receiving-runtime.js +1 -1
- package/dist/comms/setup.js +1 -1
- package/dist/evidence/redaction.d.ts +7 -3
- package/dist/evidence/redaction.js +68 -24
- package/dist/feedback/export-bundle.js +6 -1
- package/dist/feedback/export.js +3 -0
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.d.ts +7 -0
- package/dist/keys/key-resolution.js +10 -8
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +18 -15
- package/dist/observer/render.js +8 -2
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +43 -34
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +4 -4
- package/dist/routes/computer-use/preflight.js +2 -2
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/computer-use/types.d.ts +6 -0
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/scripted/types.d.ts +1 -0
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/bundle.js +4 -2
- package/dist/routes/terminal/credentials.js +1 -1
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +23 -17
- package/dist/routes/terminal/plan.js +4 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/session.js +1 -1
- package/dist/routes/terminal/types.d.ts +3 -0
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +26 -1
- package/dist/run/bundle.d.ts +30 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +11 -7
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +10 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +18 -14
- package/dist/run/reclaim.js +179 -105
- package/dist/run/results.d.ts +11 -18
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +177 -72
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +138 -0
- package/dist/run/sandbox-ids.d.ts +53 -0
- package/dist/run/sandbox-ids.js +198 -0
- package/dist/run/sandbox-receipts.d.ts +16 -0
- package/dist/run/sandbox-receipts.js +70 -0
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -20
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -137
- package/dist/run/streams.d.ts +3 -2
- package/dist/run-study.js +6 -1
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +7 -4
- package/dist/study/init-templates.js +2 -2
- package/dist/study/init.js +14 -3
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/types.d.ts +1 -1
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +128 -35
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui-app.js +133 -133
- package/dist/verify/artifacts.d.ts +4 -0
- package/dist/verify/artifacts.js +8 -0
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +53 -28
- package/dist/verify/verify.d.ts +4 -1
- package/dist/verify/verify.js +27 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +90 -7
- package/docs/contracts/schemas.md +39 -22
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +5 -3
- package/skills/humanish/SKILL.md +18 -4
package/dist/run/run.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
// token that lets the Observer and automatic analysis read it. Routes build bundles and write
|
|
3
3
|
// their own evidence files; this module owns when the run starts, how it is published, and how it
|
|
4
4
|
// is closed on every exit.
|
|
5
|
+
import { redactText } from "../evidence/redaction.js";
|
|
5
6
|
import { buildObserverData } from "../observer/data.js";
|
|
6
7
|
import { renderObserver } from "../observer/render.js";
|
|
7
8
|
import { RUN_BUNDLE_FILE, PUBLIC_TARGET_CWD } from "./bundle.js";
|
|
9
|
+
import { judgeExecution, } from "./judge.js";
|
|
8
10
|
import {} from "./results.js";
|
|
9
11
|
import { createRunArtifactPaths, validatePreparedRunArtifactPaths, } from "./paths.js";
|
|
10
12
|
import { registerActiveRun } from "./active-runs.js";
|
|
11
13
|
import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./contained-output.js";
|
|
12
|
-
import {
|
|
14
|
+
import { scrubRunSandboxIds, withPublicSandboxIds } from "./sandbox-ids.js";
|
|
15
|
+
import { beginRunStatus, runStatusOutcome, } from "./status.js";
|
|
13
16
|
/** An entry point called in a state that does not admit it. */
|
|
14
17
|
class RunLifecycleError extends Error {
|
|
15
18
|
name = "RunLifecycleError";
|
|
@@ -23,38 +26,169 @@ const issueKey = Symbol("FinishedRun");
|
|
|
23
26
|
*/
|
|
24
27
|
export class FinishedRun {
|
|
25
28
|
#observer;
|
|
26
|
-
#
|
|
29
|
+
#recordFailure;
|
|
30
|
+
#outcome;
|
|
27
31
|
runId;
|
|
28
32
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
29
33
|
paths;
|
|
30
|
-
constructor(key, runId, paths, observer,
|
|
34
|
+
constructor(key, runId, paths, observer, outcome, recordFailure) {
|
|
31
35
|
if (key !== issueKey)
|
|
32
36
|
throw new Error("Only Run.finish issues a FinishedRun.");
|
|
33
37
|
this.runId = runId;
|
|
34
38
|
this.paths = paths;
|
|
35
39
|
this.#observer = observer;
|
|
36
|
-
this.#
|
|
40
|
+
this.#outcome = outcome;
|
|
41
|
+
this.#recordFailure = recordFailure;
|
|
37
42
|
}
|
|
38
43
|
static isIssued(value) {
|
|
39
44
|
return typeof value === "object" && value !== null && #observer in value;
|
|
40
45
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return target.render(target.cwd, this.runId, { open: target.open, expectedRun: this.paths });
|
|
46
|
+
/**
|
|
47
|
+
* The run's ok and execution outcome as run.json records them: what the route passed to
|
|
48
|
+
* finish, plus an Observer that did not render. A route's result reads them from here.
|
|
49
|
+
*/
|
|
50
|
+
get outcome() {
|
|
51
|
+
return this.#outcome;
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
51
|
-
* Observer render
|
|
54
|
+
* Render the Observer for exactly these paths, never a directory re-resolved by name. An
|
|
55
|
+
* Observer that did not render is an `evidence` failure: it is added to run.json's outcome and
|
|
56
|
+
* status.json under the route's policy before this resolves. No page exists then that could
|
|
57
|
+
* show the run without it. A render that throws records nothing and rejects.
|
|
52
58
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
async renderObserver() {
|
|
60
|
+
const target = this.#observer;
|
|
61
|
+
if (target === undefined)
|
|
62
|
+
throw new RunLifecycleError("This run was started without an Observer.");
|
|
63
|
+
const observer = await target.render(target.cwd, this.runId, {
|
|
64
|
+
open: target.open,
|
|
65
|
+
expectedRun: this.paths,
|
|
66
|
+
});
|
|
67
|
+
if (!observer.ok) {
|
|
68
|
+
this.#outcome = await this.#recordFailure({
|
|
69
|
+
kind: "evidence",
|
|
70
|
+
message: observer.error?.message ?? "The Observer did not render.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return observer;
|
|
55
74
|
}
|
|
56
75
|
}
|
|
57
76
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
77
|
+
/** An execution outcome as run.json records it, each message through the shape redaction. */
|
|
78
|
+
function redactedExecution(execution) {
|
|
79
|
+
const redacted = (failure) => ({
|
|
80
|
+
kind: failure.kind,
|
|
81
|
+
message: redactText(failure.message),
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
succeeded: execution.succeeded,
|
|
85
|
+
failures: execution.failures.map(redacted),
|
|
86
|
+
...(execution.warnings === undefined ? {} : { warnings: execution.warnings.map(redacted) }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Where FinishedRun renders the Observer, when the run was started with one. */
|
|
90
|
+
function observerTarget(options) {
|
|
91
|
+
return options.observer === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: {
|
|
94
|
+
cwd: options.cwd,
|
|
95
|
+
open: options.observer.open,
|
|
96
|
+
render: options.observer.render ?? renderObserver,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function runPublisher(args) {
|
|
100
|
+
const { options, runId, paths, runStatus, now, admit } = args;
|
|
101
|
+
const observer = observerTarget(options);
|
|
102
|
+
let pointerWritten = false;
|
|
103
|
+
// The bundle of the last write asked for, and the outcome every later write carries once a
|
|
104
|
+
// signal stopped the run, so no write after the interrupt can drop it.
|
|
105
|
+
let lastBundle;
|
|
106
|
+
let interrupted;
|
|
107
|
+
// One chain serializes every write. A rejection reaches only the caller of that write; the
|
|
108
|
+
// chain itself continues, so a failed snapshot never blocks the final publication.
|
|
109
|
+
let tail = Promise.resolve();
|
|
110
|
+
const enqueue = (write) => {
|
|
111
|
+
const written = tail.then(write);
|
|
112
|
+
tail = written.then(() => undefined, () => undefined);
|
|
113
|
+
return written;
|
|
114
|
+
};
|
|
115
|
+
const writePointer = async () => {
|
|
116
|
+
const pointer = {
|
|
117
|
+
schema: "humanish.latest-run.v1",
|
|
118
|
+
runId,
|
|
119
|
+
path: paths.relativeRunRoot,
|
|
120
|
+
updatedAt: new Date(now()).toISOString(),
|
|
121
|
+
};
|
|
122
|
+
await writePreparedRunLatestPointer(paths, json(pointer), "utf8");
|
|
123
|
+
pointerWritten = true;
|
|
124
|
+
};
|
|
125
|
+
/** run.json with its outcome, then `afterBundle`, then the review and projections. */
|
|
126
|
+
const writeBundleFiles = async (bundle, outcome, afterBundle) => {
|
|
127
|
+
await validatePreparedRunArtifactPaths(paths);
|
|
128
|
+
// The run owns `outcome`: a bundle read back from disk keeps none of its own.
|
|
129
|
+
const { outcome: _previous, ...evidence } = bundle;
|
|
130
|
+
const recorded = interrupted ?? outcome;
|
|
131
|
+
const publicBundle = await withPublicSandboxIds(paths, {
|
|
132
|
+
...evidence,
|
|
133
|
+
cwd: PUBLIC_TARGET_CWD,
|
|
134
|
+
...(recorded === undefined ? {} : { outcome: recorded }),
|
|
135
|
+
});
|
|
136
|
+
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(publicBundle), "utf8");
|
|
137
|
+
await afterBundle(publicBundle);
|
|
138
|
+
await writeContainedOutputFile(paths, "review.json", json(publicBundle.review), "utf8");
|
|
139
|
+
await writeContainedOutputFile(paths, "review.md", options.renderReview(publicBundle), "utf8");
|
|
140
|
+
const events = publicBundle.events.map((event) => JSON.stringify(event)).join("\n");
|
|
141
|
+
await writeContainedOutputFile(paths, "events.ndjson", `${events}\n`, "utf8");
|
|
142
|
+
await writeContainedOutputFile(paths, "observer/observer-data.json", json(buildObserverData(publicBundle)), "utf8");
|
|
143
|
+
};
|
|
144
|
+
const snapshot = async (bundle) => {
|
|
145
|
+
await writeBundleFiles(bundle, undefined, async () => { });
|
|
146
|
+
if (!pointerWritten)
|
|
147
|
+
await writePointer();
|
|
148
|
+
};
|
|
149
|
+
const finishedOutcome = (outcome) => ({
|
|
150
|
+
state: "finished",
|
|
151
|
+
ok: outcome.ok,
|
|
152
|
+
execution: redactedExecution(outcome.execution),
|
|
153
|
+
});
|
|
154
|
+
const publish = async (bundle, outcome) => {
|
|
155
|
+
await writeBundleFiles(bundle, finishedOutcome(outcome), (publicBundle) => runStatus.finish(runStatusOutcome(publicBundle)));
|
|
156
|
+
await scrubRunSandboxIds(paths);
|
|
157
|
+
await writePointer();
|
|
158
|
+
let current = { ok: outcome.ok, execution: outcome.execution };
|
|
159
|
+
// An Observer that did not render, judged under the route's policy and written to run.json
|
|
160
|
+
// and status.json, so the two agree with the result the route returns. A run directory that
|
|
161
|
+
// can no longer be written (replaced, or a disk error) keeps the files it has, and the
|
|
162
|
+
// result still carries the failure.
|
|
163
|
+
const recordFailure = (failure) => admit(enqueue(async () => {
|
|
164
|
+
const { failures, warnings = [] } = current.execution;
|
|
165
|
+
const execution = judgeExecution([...failures, ...warnings, failure], outcome.policy);
|
|
166
|
+
current = { ok: current.ok && execution.succeeded, execution };
|
|
167
|
+
await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
|
|
168
|
+
return current;
|
|
169
|
+
}));
|
|
170
|
+
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure);
|
|
171
|
+
};
|
|
172
|
+
const interrupt = async (signal) => {
|
|
173
|
+
interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
|
|
174
|
+
const written = lastBundle;
|
|
175
|
+
if (written === undefined)
|
|
176
|
+
return;
|
|
177
|
+
// A failed write leaves status.json as the record of the interrupt.
|
|
178
|
+
await enqueue(() => writeBundleFiles(written, undefined, async () => { })).catch(() => undefined);
|
|
179
|
+
};
|
|
180
|
+
return {
|
|
181
|
+
snapshot(bundle) {
|
|
182
|
+
lastBundle = bundle;
|
|
183
|
+
return enqueue(() => snapshot(bundle));
|
|
184
|
+
},
|
|
185
|
+
publish(bundle, outcome) {
|
|
186
|
+
lastBundle = bundle;
|
|
187
|
+
return enqueue(() => publish(bundle, outcome));
|
|
188
|
+
},
|
|
189
|
+
interrupt,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
58
192
|
/**
|
|
59
193
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
60
194
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
@@ -89,70 +223,40 @@ export async function runScope(fn) {
|
|
|
89
223
|
if (!created.ok)
|
|
90
224
|
return created;
|
|
91
225
|
// beginRunStatus reads only the mode and the study provenance from the run's options.
|
|
92
|
-
|
|
226
|
+
const runStatus = beginRunStatus(created.paths, { ...options, runId });
|
|
227
|
+
status = runStatus;
|
|
228
|
+
let interruptBundle;
|
|
93
229
|
// Registered before the first write lands: a signal while it is still settling reaches
|
|
94
|
-
// the handle, whose interrupt queues behind that write.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
230
|
+
// the handle, whose interrupt queues behind that write. The interrupt lands in
|
|
231
|
+
// status.json first, then in run.json once the run has written one.
|
|
232
|
+
unregister = registerActiveRun({
|
|
233
|
+
cwd: options.cwd,
|
|
234
|
+
runId,
|
|
235
|
+
paths: created.paths,
|
|
236
|
+
status: {
|
|
237
|
+
async interrupt(signal) {
|
|
238
|
+
const recorded = await runStatus.interrupt(signal);
|
|
239
|
+
if (recorded)
|
|
240
|
+
await interruptBundle?.(signal);
|
|
241
|
+
return recorded;
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
await runStatus.started;
|
|
246
|
+
const run = openRun(options, runId, created.paths, runStatus);
|
|
247
|
+
interruptBundle = run.interrupt;
|
|
248
|
+
return { ok: true, run: run.run };
|
|
98
249
|
})());
|
|
99
250
|
};
|
|
100
251
|
const openRun = (options, runId, paths, runStatus) => {
|
|
101
252
|
const now = options.now ?? Date.now;
|
|
102
253
|
const createdAt = new Date(now()).toISOString();
|
|
103
|
-
const
|
|
104
|
-
? undefined
|
|
105
|
-
: {
|
|
106
|
-
cwd: options.cwd,
|
|
107
|
-
open: options.observer.open,
|
|
108
|
-
render: options.observer.render ?? renderObserver,
|
|
109
|
-
};
|
|
254
|
+
const publisher = runPublisher({ options, runId, paths, runStatus, now, admit });
|
|
110
255
|
let finishCalled = false;
|
|
111
|
-
let pointerWritten = false;
|
|
112
|
-
// One chain serializes every write. A rejection reaches only the caller of that write; the
|
|
113
|
-
// chain itself continues, so a failed snapshot never blocks the final publication.
|
|
114
|
-
let tail = Promise.resolve();
|
|
115
|
-
const enqueue = (write) => {
|
|
116
|
-
const written = tail.then(write);
|
|
117
|
-
tail = written.then(() => undefined, () => undefined);
|
|
118
|
-
return written;
|
|
119
|
-
};
|
|
120
256
|
const checkIdentity = (bundle) => bundle.runId !== runId || bundle.mode !== options.mode
|
|
121
257
|
? "The bundle names another run or mode than this run."
|
|
122
258
|
: undefined;
|
|
123
|
-
const
|
|
124
|
-
const pointer = {
|
|
125
|
-
schema: "humanish.latest-run.v1",
|
|
126
|
-
runId,
|
|
127
|
-
path: paths.relativeRunRoot,
|
|
128
|
-
updatedAt: new Date(now()).toISOString(),
|
|
129
|
-
};
|
|
130
|
-
await writePreparedRunLatestPointer(paths, json(pointer), "utf8");
|
|
131
|
-
pointerWritten = true;
|
|
132
|
-
};
|
|
133
|
-
/** run.json, then `afterBundle`, then the review and projections. */
|
|
134
|
-
const writeBundleFiles = async (bundle, afterBundle) => {
|
|
135
|
-
await validatePreparedRunArtifactPaths(paths);
|
|
136
|
-
const publicBundle = { ...bundle, cwd: PUBLIC_TARGET_CWD };
|
|
137
|
-
await writeContainedOutputFile(paths, RUN_BUNDLE_FILE, json(publicBundle), "utf8");
|
|
138
|
-
await afterBundle(publicBundle);
|
|
139
|
-
await writeContainedOutputFile(paths, "review.json", json(publicBundle.review), "utf8");
|
|
140
|
-
await writeContainedOutputFile(paths, "review.md", options.renderReview(publicBundle), "utf8");
|
|
141
|
-
const events = publicBundle.events.map((event) => JSON.stringify(event)).join("\n");
|
|
142
|
-
await writeContainedOutputFile(paths, "events.ndjson", `${events}\n`, "utf8");
|
|
143
|
-
await writeContainedOutputFile(paths, "observer/observer-data.json", json(buildObserverData(publicBundle)), "utf8");
|
|
144
|
-
};
|
|
145
|
-
const snapshot = async (bundle) => {
|
|
146
|
-
await writeBundleFiles(bundle, async () => { });
|
|
147
|
-
if (!pointerWritten)
|
|
148
|
-
await writePointer();
|
|
149
|
-
};
|
|
150
|
-
const publish = async (bundle) => {
|
|
151
|
-
await writeBundleFiles(bundle, (publicBundle) => runStatus.finish(runStatusOutcome(publicBundle)));
|
|
152
|
-
await writePointer();
|
|
153
|
-
return new FinishedRun(issueKey, runId, paths, observer, runStatus);
|
|
154
|
-
};
|
|
155
|
-
return {
|
|
259
|
+
const run = {
|
|
156
260
|
runId,
|
|
157
261
|
createdAt,
|
|
158
262
|
mode: options.mode,
|
|
@@ -166,9 +270,9 @@ export async function runScope(fn) {
|
|
|
166
270
|
const mismatch = checkIdentity(bundle);
|
|
167
271
|
if (mismatch !== undefined)
|
|
168
272
|
return refuse(mismatch);
|
|
169
|
-
return admit(
|
|
273
|
+
return admit(publisher.snapshot(bundle));
|
|
170
274
|
},
|
|
171
|
-
finish(bundle) {
|
|
275
|
+
finish(bundle, outcome) {
|
|
172
276
|
if (closed)
|
|
173
277
|
return refuse("The run scope has closed.");
|
|
174
278
|
if (finishCalled)
|
|
@@ -177,12 +281,13 @@ export async function runScope(fn) {
|
|
|
177
281
|
if (mismatch !== undefined)
|
|
178
282
|
return refuse(mismatch);
|
|
179
283
|
finishCalled = true;
|
|
180
|
-
return admit(
|
|
284
|
+
return admit(publisher.publish(bundle, outcome).then((issued) => {
|
|
181
285
|
finished = issued;
|
|
182
286
|
return issued;
|
|
183
287
|
}));
|
|
184
288
|
},
|
|
185
289
|
};
|
|
290
|
+
return { run, interrupt: publisher.interrupt };
|
|
186
291
|
};
|
|
187
292
|
let outcome;
|
|
188
293
|
try {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { PreparedOutputRoot } from "./contained-output.js";
|
|
2
|
+
import type { SandboxOwnerTags } from "./sandbox-receipts.js";
|
|
3
|
+
/** Refuses a sandbox create for a run directory whose process is stopping. */
|
|
4
|
+
export declare class SandboxCreateRefusedError extends Error {
|
|
5
|
+
constructor();
|
|
6
|
+
}
|
|
7
|
+
/** One sandbox a create in this process reported, and what its route did with it. */
|
|
8
|
+
export interface CreatedSandbox {
|
|
9
|
+
sandboxId: string;
|
|
10
|
+
/** The participant or subject label the receipt also records. */
|
|
11
|
+
participantId: string;
|
|
12
|
+
/** True once the route's own release confirmed the sandbox gone. */
|
|
13
|
+
released: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The E2B metadata that ties a sandbox to the directory that journals it. `runId` is the
|
|
17
|
+
* directory's name: the run id, or a preflight probe's id. `runKey` is a digest of the directory's
|
|
18
|
+
* inode and creation time, so two projects that both use `--run-id smoke` never match each other's
|
|
19
|
+
* sandboxes, and a renamed project directory still matches its own. `tool` is the owner tag every
|
|
20
|
+
* route already sets.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sandboxOwnerTags(root: PreparedOutputRoot): SandboxOwnerTags;
|
|
23
|
+
/** A create in progress; the acquire step in src/substrates/e2b/sandbox.ts holds one per create. */
|
|
24
|
+
export interface SandboxCreateTicket {
|
|
25
|
+
/** Whether the directory's process has started stopping; checked before each attempt. */
|
|
26
|
+
stopping(): boolean;
|
|
27
|
+
/** The provider reported a sandbox id for this create. Safe to call twice with one id. */
|
|
28
|
+
created(sandboxId: string): void;
|
|
29
|
+
/** The route's release confirmed this sandbox gone. */
|
|
30
|
+
released(sandboxId: string): void;
|
|
31
|
+
/** The create resolved or threw, and its receipt write, if any, has finished. */
|
|
32
|
+
settled(): void;
|
|
33
|
+
}
|
|
34
|
+
/** Register a create for `root`. Throws SandboxCreateRefusedError once the directory is stopping. */
|
|
35
|
+
export declare function beginSandboxCreate(root: PreparedOutputRoot, participantId: string): SandboxCreateTicket;
|
|
36
|
+
/**
|
|
37
|
+
* Run `write` once per directory and return its promise to every caller. Creates that start
|
|
38
|
+
* together then resume in the order they called, so the order they reach the provider does not
|
|
39
|
+
* depend on which file append finishes first. A write that reports failure is forgotten, so the
|
|
40
|
+
* next create tries again.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordSandboxOwnerOnce(root: PreparedOutputRoot, write: () => Promise<boolean>): Promise<void>;
|
|
43
|
+
/** What the signal handler holds after stopping a directory's creates. */
|
|
44
|
+
export interface StoppedSandboxCreates {
|
|
45
|
+
/**
|
|
46
|
+
* Calls `listener` at once for every sandbox a create has reported so far, then for each one an
|
|
47
|
+
* in-flight create reports later, until the returned function is called.
|
|
48
|
+
*/
|
|
49
|
+
watch(listener: (sandbox: CreatedSandbox) => void): () => void;
|
|
50
|
+
/** Resolves once every create that was in flight at the stop has settled. */
|
|
51
|
+
settled: Promise<void>;
|
|
52
|
+
/** How many of those creates have not settled yet. */
|
|
53
|
+
inFlight(): number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Stop `root`'s creates: any create that starts from now on is refused. Called synchronously when
|
|
57
|
+
* the signal arrives, before anything is awaited, so no create can slip in after it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function stopSandboxCreates(root: PreparedOutputRoot): StoppedSandboxCreates;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The sandboxes this process is creating for each run directory, and the E2B tags that name the
|
|
2
|
+
// directory. A receipt reaches sandbox-receipts.ndjson only after `Sandbox.create` resolves, and
|
|
3
|
+
// on @e2b/desktop that is after desktop startup, seconds after E2B has created the sandbox. The
|
|
4
|
+
// CLI's signal handler (src/cli/commands/run-signals.ts) uses this registry to cover that window:
|
|
5
|
+
// it stops new creates, kills every id a create has reported, and kills each in-flight create's
|
|
6
|
+
// sandbox as soon as its id arrives. The tags let `humanish reclaim` find a sandbox whose id
|
|
7
|
+
// never reached this process at all.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
/** Refuses a sandbox create for a run directory whose process is stopping. */
|
|
11
|
+
export class SandboxCreateRefusedError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("The run is stopping, so no further sandbox is created.");
|
|
14
|
+
this.name = "SandboxCreateRefusedError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const directories = new Map();
|
|
18
|
+
// A process runs one study at a time; the cap only bounds a long-lived library caller, and only
|
|
19
|
+
// idle entries count against it.
|
|
20
|
+
const MAX_REMEMBERED_DIRECTORIES = 64;
|
|
21
|
+
function identityOf(root) {
|
|
22
|
+
return "physicalRunRoot" in root
|
|
23
|
+
? { where: root.physicalRunRoot, identity: root.runRootIdentity }
|
|
24
|
+
: { where: root.physicalPath, identity: root.identity };
|
|
25
|
+
}
|
|
26
|
+
function directoryKey(root) {
|
|
27
|
+
const { where, identity } = identityOf(root);
|
|
28
|
+
return `${where}\0${identity.dev}:${identity.ino}:${identity.birthtimeNs}`;
|
|
29
|
+
}
|
|
30
|
+
function directoryFor(root) {
|
|
31
|
+
const key = directoryKey(root);
|
|
32
|
+
const existing = directories.get(key);
|
|
33
|
+
if (existing !== undefined)
|
|
34
|
+
return existing;
|
|
35
|
+
const created = {
|
|
36
|
+
stopping: false,
|
|
37
|
+
created: new Map(),
|
|
38
|
+
inFlight: new Set(),
|
|
39
|
+
listeners: new Set(),
|
|
40
|
+
};
|
|
41
|
+
directories.set(key, created);
|
|
42
|
+
// Only an idle entry is forgotten: a stopping one keeps refusing creates, and one with a create
|
|
43
|
+
// in flight or a sandbox its route has not released is still needed by the signal handler.
|
|
44
|
+
for (const [oldest, entry] of directories) {
|
|
45
|
+
if (directories.size <= MAX_REMEMBERED_DIRECTORIES)
|
|
46
|
+
break;
|
|
47
|
+
if (entry === created || !idle(entry))
|
|
48
|
+
continue;
|
|
49
|
+
directories.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
return created;
|
|
52
|
+
}
|
|
53
|
+
function idle(entry) {
|
|
54
|
+
return (!entry.stopping &&
|
|
55
|
+
entry.inFlight.size === 0 &&
|
|
56
|
+
[...entry.created.values()].every((sandbox) => sandbox.released));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The E2B metadata that ties a sandbox to the directory that journals it. `runId` is the
|
|
60
|
+
* directory's name: the run id, or a preflight probe's id. `runKey` is a digest of the directory's
|
|
61
|
+
* inode and creation time, so two projects that both use `--run-id smoke` never match each other's
|
|
62
|
+
* sandboxes, and a renamed project directory still matches its own. `tool` is the owner tag every
|
|
63
|
+
* route already sets.
|
|
64
|
+
*/
|
|
65
|
+
export function sandboxOwnerTags(root) {
|
|
66
|
+
const { where, identity } = identityOf(root);
|
|
67
|
+
const runKey = createHash("sha256")
|
|
68
|
+
.update(`${identity.ino}:${identity.birthtimeNs}`)
|
|
69
|
+
.digest("hex")
|
|
70
|
+
.slice(0, 16);
|
|
71
|
+
return { tool: "humanish", runId: path.basename(where), runKey };
|
|
72
|
+
}
|
|
73
|
+
/** Register a create for `root`. Throws SandboxCreateRefusedError once the directory is stopping. */
|
|
74
|
+
export function beginSandboxCreate(root, participantId) {
|
|
75
|
+
const directory = directoryFor(root);
|
|
76
|
+
if (directory.stopping)
|
|
77
|
+
throw new SandboxCreateRefusedError();
|
|
78
|
+
let settle;
|
|
79
|
+
const done = new Promise((resolve) => {
|
|
80
|
+
settle = resolve;
|
|
81
|
+
});
|
|
82
|
+
directory.inFlight.add(done);
|
|
83
|
+
return {
|
|
84
|
+
stopping: () => directory.stopping,
|
|
85
|
+
created(sandboxId) {
|
|
86
|
+
if (directory.created.has(sandboxId))
|
|
87
|
+
return;
|
|
88
|
+
const sandbox = { sandboxId, participantId, released: false };
|
|
89
|
+
directory.created.set(sandboxId, sandbox);
|
|
90
|
+
for (const listener of directory.listeners)
|
|
91
|
+
listener(sandbox);
|
|
92
|
+
},
|
|
93
|
+
released(sandboxId) {
|
|
94
|
+
const sandbox = directory.created.get(sandboxId);
|
|
95
|
+
if (sandbox !== undefined)
|
|
96
|
+
sandbox.released = true;
|
|
97
|
+
},
|
|
98
|
+
settled() {
|
|
99
|
+
directory.inFlight.delete(done);
|
|
100
|
+
settle();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run `write` once per directory and return its promise to every caller. Creates that start
|
|
106
|
+
* together then resume in the order they called, so the order they reach the provider does not
|
|
107
|
+
* depend on which file append finishes first. A write that reports failure is forgotten, so the
|
|
108
|
+
* next create tries again.
|
|
109
|
+
*/
|
|
110
|
+
export function recordSandboxOwnerOnce(root, write) {
|
|
111
|
+
const directory = directoryFor(root);
|
|
112
|
+
const pending = (directory.ownerLine ??= write().then((written) => {
|
|
113
|
+
if (!written && directory.ownerLine === pending)
|
|
114
|
+
delete directory.ownerLine;
|
|
115
|
+
}));
|
|
116
|
+
return pending;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Stop `root`'s creates: any create that starts from now on is refused. Called synchronously when
|
|
120
|
+
* the signal arrives, before anything is awaited, so no create can slip in after it.
|
|
121
|
+
*/
|
|
122
|
+
export function stopSandboxCreates(root) {
|
|
123
|
+
const directory = directoryFor(root);
|
|
124
|
+
directory.stopping = true;
|
|
125
|
+
const pending = [...directory.inFlight];
|
|
126
|
+
return {
|
|
127
|
+
watch(listener) {
|
|
128
|
+
for (const sandbox of directory.created.values())
|
|
129
|
+
listener(sandbox);
|
|
130
|
+
directory.listeners.add(listener);
|
|
131
|
+
return () => {
|
|
132
|
+
directory.listeners.delete(listener);
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
settled: Promise.all(pending).then(() => undefined),
|
|
136
|
+
inFlight: () => pending.filter((create) => directory.inFlight.has(create)).length,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
2
|
+
import { type PreparedRunArtifactPaths } from "./paths.js";
|
|
3
|
+
/** `text` with every occurrence of each raw id replaced by its label, longest id first. */
|
|
4
|
+
export declare function scrubSandboxIds(text: string, ids: Iterable<string>, label?: (id: string) => string): string;
|
|
5
|
+
/**
|
|
6
|
+
* `value` as it may be written or returned: ids at the sandbox-id keys become the marker and a
|
|
7
|
+
* digest, and any raw id left in free text or a URL becomes its label.
|
|
8
|
+
*/
|
|
9
|
+
export declare function publicSandboxView<T>(value: T, ids: Iterable<string>): T;
|
|
10
|
+
/**
|
|
11
|
+
* The raw ids at sandbox-id keys in `value`, such as a run.json recorded before 0.110. A value
|
|
12
|
+
* shorter than an E2B id is left to the key rule, so the text scrub never rewrites a common word.
|
|
13
|
+
*/
|
|
14
|
+
export declare function keyedSandboxIds(value: unknown): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Every raw sandbox id a run's files name: the receipts', and any at a sandbox-id key in its
|
|
17
|
+
* JSON, which is where a run from before 0.110 that has no receipts still records them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inventorySandboxIds(files: readonly {
|
|
20
|
+
path: string;
|
|
21
|
+
bytes: Buffer;
|
|
22
|
+
}[]): string[];
|
|
23
|
+
/** Whether a run file's JSON holds a raw id at a sandbox-id key, which no receipt may name. */
|
|
24
|
+
export declare function holdsKeyedSandboxId(file: string, text: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
27
|
+
* failed; empty when there are none or the journal cannot be read.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readRunSandboxIds(root: PreparedOutputRoot): Promise<string[]>;
|
|
30
|
+
/**
|
|
31
|
+
* `value` as a run writes it: raw sandbox ids stay in the run's sandbox-receipts.ndjson, and the
|
|
32
|
+
* file names each by marker and digest.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withPublicSandboxIds<T>(root: PreparedOutputRoot, value: T): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* The bytes of `file` with each raw id replaced by its label: the same buffer when none occurs, or
|
|
37
|
+
* when the bytes are not the plain text verify and export read as text.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scrubSandboxIdBytes(file: string, bytes: Buffer, ids: readonly string[]): Buffer;
|
|
40
|
+
/**
|
|
41
|
+
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
42
|
+
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
43
|
+
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
44
|
+
* Best effort: verify grades a file this misses.
|
|
45
|
+
*/
|
|
46
|
+
export declare function scrubRunSandboxIds(paths: PreparedRunArtifactPaths): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* A run's result as it may be returned or printed: the same object when it names no sandbox,
|
|
49
|
+
* otherwise its public view, scrubbed of the ids the run's receipts journal.
|
|
50
|
+
*/
|
|
51
|
+
export declare function publicRunResult<T extends {
|
|
52
|
+
runId?: string;
|
|
53
|
+
}>(result: T, cwd: string): Promise<T>;
|