humanish 0.140.0 → 0.142.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/ARCHITECTURE.md +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +2 -0
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/external-handoff.d.ts +22 -0
- package/dist/actors/computer-use/external-handoff.js +83 -0
- package/dist/actors/computer-use/loop/observation.d.ts +7 -0
- package/dist/actors/computer-use/loop/observation.js +17 -1
- package/dist/actors/computer-use/loop/types.d.ts +6 -0
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/analysis-limits.d.ts +6 -0
- package/dist/analysis/analysis-limits.js +6 -0
- package/dist/analysis/analysis-lock.d.ts +8 -0
- package/dist/analysis/analysis-lock.js +129 -0
- package/dist/analysis/automatic-completion.js +7 -0
- package/dist/analysis/automatic-config.d.ts +5 -2
- package/dist/analysis/automatic-config.js +23 -8
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +17 -6
- package/dist/analysis/execute.js +37 -19
- package/dist/analysis/gemini-wire.d.ts +8 -0
- package/dist/analysis/gemini-wire.js +165 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +2 -0
- package/dist/analysis/job.js +7 -2
- package/dist/analysis/openai-wire.d.ts +5 -0
- package/dist/analysis/openai-wire.js +101 -0
- package/dist/analysis/provider.d.ts +15 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/recording-ended.d.ts +16 -0
- package/dist/analysis/recording-ended.js +29 -0
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.d.ts +1 -3
- package/dist/analysis/service.js +34 -53
- package/dist/analysis/source.d.ts +1 -1
- package/dist/analysis/types.d.ts +8 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +13 -12
- package/dist/browser-control/protocol.d.ts +8 -0
- package/dist/browser-control/protocol.js +21 -0
- package/dist/cli/commands/analysis-signals.d.ts +10 -4
- package/dist/cli/commands/analysis-signals.js +28 -6
- package/dist/cli/commands/analyze.js +9 -7
- package/dist/cli/commands/observe.js +26 -1
- package/dist/cli/commands/run-command.d.ts +1 -1
- package/dist/cli/commands/run-command.js +12 -0
- package/dist/cli/commands/runs.js +8 -2
- package/dist/cli/commands/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +28 -4
- package/dist/cli/commands/study-route-scripted.js +3 -2
- package/dist/cli/commands/study-route-shared-world.js +3 -2
- package/dist/cli/commands/study-route-terminal.js +3 -2
- package/dist/cli/commands/study-run.js +34 -0
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/findings.d.ts +30 -0
- package/dist/cli/findings.js +114 -15
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +3 -1
- package/dist/evidence/browser-hygiene.d.ts +12 -1
- package/dist/evidence/browser-hygiene.js +45 -2
- package/dist/feedback/export-bundle.js +1 -1
- package/dist/guest/external-links.d.ts +14 -0
- package/dist/guest/external-links.js +48 -0
- package/dist/guest/runtime-desktop.js +5 -2
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- package/dist/observer/data.d.ts +4 -0
- package/dist/observer/listen.d.ts +7 -2
- package/dist/observer/listen.js +10 -4
- package/dist/observer/render.d.ts +2 -0
- package/dist/observer/render.js +1 -1
- package/dist/observer/run-routes.js +12 -3
- package/dist/observer-app.html +2 -2
- package/dist/remote/launch.d.ts +53 -0
- package/dist/remote/launch.js +320 -0
- package/dist/remote/observer-proxy.d.ts +11 -0
- package/dist/remote/observer-proxy.js +66 -0
- package/dist/remote/plan.d.ts +35 -0
- package/dist/remote/plan.js +114 -0
- package/dist/remote/reattach.d.ts +17 -0
- package/dist/remote/reattach.js +81 -0
- package/dist/remote/runner.d.ts +68 -0
- package/dist/remote/runner.js +216 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.js +27 -17
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/e2b-desktop/teardown.js +1 -1
- package/dist/routes/computer-use/in-process-desktop.js +2 -2
- package/dist/routes/computer-use/live-phase.js +1 -1
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/types.d.ts +2 -0
- package/dist/run/costs.d.ts +2 -1
- package/dist/run/display.d.ts +6 -1
- package/dist/run/display.js +11 -0
- package/dist/run/host-suspension.js +4 -1
- package/dist/run/preflight-receipts.d.ts +3 -12
- package/dist/run/preflight-receipts.js +7 -67
- package/dist/run/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/process-owner.d.ts +28 -0
- package/dist/run/process-owner.js +83 -0
- package/dist/run/projection.js +1 -1
- package/dist/run/reclaim.d.ts +6 -0
- package/dist/run/reclaim.js +32 -2
- package/dist/run/results.d.ts +1 -1
- package/dist/run/route-shell.js +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run-index.d.ts +3 -1
- package/dist/run/run-index.js +23 -11
- package/dist/run/run.d.ts +29 -5
- package/dist/run/run.js +43 -17
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- package/dist/run/status.d.ts +57 -2
- package/dist/run/status.js +87 -1
- package/dist/run/stored-runs.d.ts +3 -0
- package/dist/run/stored-runs.js +9 -4
- package/dist/run-study.d.ts +5 -0
- package/dist/study/doctor.js +14 -1
- package/dist/study/requirements.d.ts +1 -1
- package/dist/study/requirements.js +8 -6
- package/dist/study/route-inputs.js +4 -0
- package/dist/study/run-study-events.d.ts +8 -2
- package/dist/study/run-study-events.js +9 -2
- package/dist/substrates/e2b/base-module.d.ts +3 -0
- package/dist/substrates/e2b/base-module.js +40 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
- package/dist/substrates/e2b/desktop-browser.js +27 -7
- package/dist/substrates/e2b/desktop-executor.d.ts +7 -0
- package/dist/substrates/e2b/desktop-executor.js +3 -0
- package/dist/substrates/e2b/desktop-handoff.d.ts +22 -0
- package/dist/substrates/e2b/desktop-handoff.js +86 -0
- package/dist/substrates/e2b/sdk.d.ts +24 -0
- package/dist/substrates/e2b/sdk.js +5 -1
- package/dist/substrates/e2b/shell-only.d.ts +5 -0
- package/dist/substrates/e2b/shell-only.js +27 -0
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +115 -115
- package/docs/architecture/observer.md +7 -3
- package/docs/contracts/schemas.md +24 -3
- package/docs/contracts/study-analysis.md +52 -4
- package/docs/product/automatic-analysis.md +55 -9
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +118 -5
package/dist/run/run.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type RunBundle } from "./bundle.js";
|
|
|
3
3
|
import { type ExecutionFailure, type ExecutionOutcome, type OutcomePolicy } from "./judge.js";
|
|
4
4
|
import { type PreparedRunArtifactPaths, type RunIdInUse } from "./paths.js";
|
|
5
5
|
import { RunSecrets } from "./secrets.js";
|
|
6
|
+
import { type RunStatusHandle } from "./status.js";
|
|
6
7
|
import { type HostClock, type HostSuspension } from "./host-suspension.js";
|
|
7
8
|
import type { RunStudyProvenance } from "./study-provenance.js";
|
|
8
9
|
interface StartRunOptions {
|
|
@@ -10,6 +11,12 @@ interface StartRunOptions {
|
|
|
10
11
|
cwd: string;
|
|
11
12
|
/** Caller-supplied id (`--run-id`, library `runId`); `mintRunId` runs when it is absent. */
|
|
12
13
|
runId?: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The run key every sandbox of this run carries (16 lowercase hex), in place of the one derived
|
|
16
|
+
* from the run directory. A remote run's launcher chooses it, so its own directory and the
|
|
17
|
+
* remote one name the same sandboxes.
|
|
18
|
+
*/
|
|
19
|
+
ownerKey?: string | undefined;
|
|
13
20
|
mintRunId: () => string;
|
|
14
21
|
mode: "dry-run" | "live";
|
|
15
22
|
study?: RunStudyProvenance | undefined;
|
|
@@ -43,6 +50,8 @@ interface StartRunPlan {
|
|
|
43
50
|
interface StartRunInput {
|
|
44
51
|
/** Caller-supplied id (`--run-id`, library `runId`); one is minted from the prefix when absent. */
|
|
45
52
|
readonly runId?: string | undefined;
|
|
53
|
+
/** The run key its sandboxes carry; see StartRunOptions.ownerKey. */
|
|
54
|
+
readonly ownerKey?: string | undefined;
|
|
46
55
|
/** Open the Observer page once it renders. */
|
|
47
56
|
readonly open?: boolean | undefined;
|
|
48
57
|
/**
|
|
@@ -170,7 +179,7 @@ export declare class FinishedRun {
|
|
|
170
179
|
readonly runId: string;
|
|
171
180
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
172
181
|
readonly paths: PreparedRunArtifactPaths;
|
|
173
|
-
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>,
|
|
182
|
+
constructor(key: typeof issueKey, runId: string, paths: PreparedRunArtifactPaths, observer: ObserverTarget | undefined, outcome: RecordedOutcome, recordFailure: (failure: ExecutionFailure) => Promise<RecordedOutcome>, status: Pick<RunStatusHandle, "interrupted" | "beginAnalysis">, participantsRan: () => boolean);
|
|
174
183
|
/**
|
|
175
184
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
176
185
|
* when asked, since the signal can arrive after the bundle was published.
|
|
@@ -178,6 +187,12 @@ export declare class FinishedRun {
|
|
|
178
187
|
get interrupted(): boolean;
|
|
179
188
|
/** The route reported a participant's session started (Run.participantStarted). */
|
|
180
189
|
get participantsRan(): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Automatic analysis of this run starts in this process: status.json records when, and its
|
|
192
|
+
* heartbeat runs again, so every surface can say the run is analyzing. Call the returned
|
|
193
|
+
* function with the analysis outcome's state when it ends. After an interrupt both do nothing.
|
|
194
|
+
*/
|
|
195
|
+
beginAnalysis(): ReturnType<RunStatusHandle["beginAnalysis"]>;
|
|
181
196
|
static isIssued(value: unknown): value is FinishedRun;
|
|
182
197
|
/**
|
|
183
198
|
* The run's ok and execution outcome as run.json records them: what the route passed to
|
|
@@ -195,13 +210,22 @@ export declare class FinishedRun {
|
|
|
195
210
|
/**
|
|
196
211
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
197
212
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
198
|
-
* timers or callbacks. It then waits for the calls it admitted and, unless the run
|
|
199
|
-
*
|
|
200
|
-
*
|
|
213
|
+
* timers or callbacks. It then waits for the calls it admitted and, unless the run published,
|
|
214
|
+
* records status.json `interrupted` with reason `no-publish`: a run with no published bundle did
|
|
215
|
+
* not finish, so no surface shows it as passed or counts it in a study's durations. The close cannot replace `fn`'s exception: admitted calls
|
|
216
|
+
* are awaited settled and the status write swallows its own errors. `finished` is set only
|
|
201
217
|
* when `Run.finish` resolved inside the scope, so a refusal cannot hand analysis another run.
|
|
202
218
|
*/
|
|
203
|
-
export declare function runScope<T>(fn: (scope: RunScope) => Promise<T
|
|
219
|
+
export declare function runScope<T>(fn: (scope: RunScope) => Promise<T>, hooks?: {
|
|
220
|
+
/** The run exists: its directory and first status.json write landed. A throw is ignored. */
|
|
221
|
+
onStart?: (runId: string) => void;
|
|
222
|
+
}): Promise<{
|
|
204
223
|
result: T;
|
|
205
224
|
finished: FinishedRun | undefined;
|
|
206
225
|
}>;
|
|
226
|
+
/**
|
|
227
|
+
* Make `runId` the project's latest run. A published run does it as it finishes, and a remote run's
|
|
228
|
+
* launcher once the run is copied back; this is the only writer of the pointer.
|
|
229
|
+
*/
|
|
230
|
+
export declare function pointLatestRunAt(paths: PreparedRunArtifactPaths, runId: string, now?: () => number): Promise<void>;
|
|
207
231
|
export {};
|
package/dist/run/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import { RunSecrets } from "./secrets.js";
|
|
|
20
20
|
import { RUN_STATUS_FILE, beginRunStatus, runStatusOutcome, } from "./status.js";
|
|
21
21
|
import { readRunJsonIfExists, runJsonValue } from "./locate.js";
|
|
22
22
|
import { hostSuspensionEvents, systemHostClock, } from "./host-suspension.js";
|
|
23
|
+
import { pinRunOwnerKey } from "./sandbox-creates.js";
|
|
23
24
|
import { splitStreamTraces } from "./stream-trace.js";
|
|
24
25
|
/** An entry point called in a state that does not admit it. */
|
|
25
26
|
class RunLifecycleError extends Error {
|
|
@@ -35,34 +36,42 @@ const issueKey = Symbol("FinishedRun");
|
|
|
35
36
|
export class FinishedRun {
|
|
36
37
|
#observer;
|
|
37
38
|
#recordFailure;
|
|
38
|
-
#interrupted;
|
|
39
39
|
#outcome;
|
|
40
40
|
runId;
|
|
41
41
|
/** The paths created by startRun and validated at the start of `finish`. */
|
|
42
42
|
paths;
|
|
43
43
|
#participantsRan;
|
|
44
|
-
|
|
44
|
+
#status;
|
|
45
|
+
constructor(key, runId, paths, observer, outcome, recordFailure, status, participantsRan) {
|
|
45
46
|
if (key !== issueKey)
|
|
46
47
|
throw new Error("Only Run.finish issues a FinishedRun.");
|
|
47
48
|
this.runId = runId;
|
|
48
49
|
this.paths = paths;
|
|
49
50
|
this.#participantsRan = participantsRan;
|
|
51
|
+
this.#status = status;
|
|
50
52
|
this.#observer = observer;
|
|
51
53
|
this.#outcome = outcome;
|
|
52
54
|
this.#recordFailure = recordFailure;
|
|
53
|
-
this.#interrupted = interrupted;
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* A signal stopped this run's process: status.json and run.json record it `interrupted`. Read
|
|
57
58
|
* when asked, since the signal can arrive after the bundle was published.
|
|
58
59
|
*/
|
|
59
60
|
get interrupted() {
|
|
60
|
-
return this.#interrupted
|
|
61
|
+
return this.#status.interrupted;
|
|
61
62
|
}
|
|
62
63
|
/** The route reported a participant's session started (Run.participantStarted). */
|
|
63
64
|
get participantsRan() {
|
|
64
65
|
return this.#participantsRan();
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Automatic analysis of this run starts in this process: status.json records when, and its
|
|
69
|
+
* heartbeat runs again, so every surface can say the run is analyzing. Call the returned
|
|
70
|
+
* function with the analysis outcome's state when it ends. After an interrupt both do nothing.
|
|
71
|
+
*/
|
|
72
|
+
beginAnalysis() {
|
|
73
|
+
return this.#status.beginAnalysis();
|
|
74
|
+
}
|
|
66
75
|
static isIssued(value) {
|
|
67
76
|
return typeof value === "object" && value !== null && #observer in value;
|
|
68
77
|
}
|
|
@@ -195,13 +204,7 @@ function runPublisher(args) {
|
|
|
195
204
|
return written;
|
|
196
205
|
};
|
|
197
206
|
const writePointer = async () => {
|
|
198
|
-
|
|
199
|
-
schema: "humanish.latest-run.v1",
|
|
200
|
-
runId,
|
|
201
|
-
path: paths.relativeRunRoot,
|
|
202
|
-
updatedAt: new Date(now()).toISOString(),
|
|
203
|
-
};
|
|
204
|
-
await writePreparedRunLatestPointer(paths, json(pointer), "utf8");
|
|
207
|
+
await pointLatestRunAt(paths, runId, now);
|
|
205
208
|
pointerWritten = true;
|
|
206
209
|
};
|
|
207
210
|
// Each running participant's items so far, appended to its live file as the flushes arrive.
|
|
@@ -262,7 +265,7 @@ function runPublisher(args) {
|
|
|
262
265
|
await writeBundleFiles(bundle, finishedOutcome(current), (publicBundle) => runStatus.restate(runStatusOutcome(publicBundle))).catch(() => undefined);
|
|
263
266
|
return current;
|
|
264
267
|
}));
|
|
265
|
-
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure,
|
|
268
|
+
return new FinishedRun(issueKey, runId, paths, observer, current, recordFailure, runStatus, participantsRan);
|
|
266
269
|
};
|
|
267
270
|
const interrupt = async (signal) => {
|
|
268
271
|
interrupted = { state: "interrupted", ok: false, signal, at: new Date(now()).toISOString() };
|
|
@@ -287,12 +290,13 @@ function runPublisher(args) {
|
|
|
287
290
|
/**
|
|
288
291
|
* Run `fn` as the lifetime of at most one run. When `fn` settles the scope closes synchronously,
|
|
289
292
|
* so the scope and its run admit no further calls, even through references `fn` leaked into
|
|
290
|
-
* timers or callbacks. It then waits for the calls it admitted and, unless the run
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
+
* timers or callbacks. It then waits for the calls it admitted and, unless the run published,
|
|
294
|
+
* records status.json `interrupted` with reason `no-publish`: a run with no published bundle did
|
|
295
|
+
* not finish, so no surface shows it as passed or counts it in a study's durations. The close cannot replace `fn`'s exception: admitted calls
|
|
296
|
+
* are awaited settled and the status write swallows its own errors. `finished` is set only
|
|
293
297
|
* when `Run.finish` resolved inside the scope, so a refusal cannot hand analysis another run.
|
|
294
298
|
*/
|
|
295
|
-
export async function runScope(fn) {
|
|
299
|
+
export async function runScope(fn, hooks = {}) {
|
|
296
300
|
let closed = false;
|
|
297
301
|
let started = false;
|
|
298
302
|
let status;
|
|
@@ -317,6 +321,8 @@ export async function runScope(fn) {
|
|
|
317
321
|
const created = await createRunArtifactPaths(options.cwd, runId);
|
|
318
322
|
if (!created.ok)
|
|
319
323
|
return created;
|
|
324
|
+
if (options.ownerKey !== undefined)
|
|
325
|
+
await pinRunOwnerKey(created.paths, options.ownerKey);
|
|
320
326
|
// beginRunStatus reads only the mode, the study provenance, the sandboxes field and the
|
|
321
327
|
// clock from the run's options.
|
|
322
328
|
const runStatus = beginRunStatus(created.paths, { ...options, runId });
|
|
@@ -339,6 +345,12 @@ export async function runScope(fn) {
|
|
|
339
345
|
},
|
|
340
346
|
});
|
|
341
347
|
await runStatus.started;
|
|
348
|
+
try {
|
|
349
|
+
hooks.onStart?.(runId);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// An announcement cannot stop the run it announces.
|
|
353
|
+
}
|
|
342
354
|
const run = openRun(options, runId, created.paths, runStatus);
|
|
343
355
|
interruptBundle = run.interrupt;
|
|
344
356
|
return { ok: true, run: run.run };
|
|
@@ -414,17 +426,31 @@ export async function runScope(fn) {
|
|
|
414
426
|
}
|
|
415
427
|
finally {
|
|
416
428
|
if (finished === undefined)
|
|
417
|
-
await status?.
|
|
429
|
+
await status?.abandon();
|
|
418
430
|
unregister?.();
|
|
419
431
|
}
|
|
420
432
|
}
|
|
421
433
|
return { result: outcome.result, finished };
|
|
422
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Make `runId` the project's latest run. A published run does it as it finishes, and a remote run's
|
|
437
|
+
* launcher once the run is copied back; this is the only writer of the pointer.
|
|
438
|
+
*/
|
|
439
|
+
export async function pointLatestRunAt(paths, runId, now = Date.now) {
|
|
440
|
+
const pointer = {
|
|
441
|
+
schema: "humanish.latest-run.v1",
|
|
442
|
+
runId,
|
|
443
|
+
path: paths.relativeRunRoot,
|
|
444
|
+
updatedAt: new Date(now()).toISOString(),
|
|
445
|
+
};
|
|
446
|
+
await writePreparedRunLatestPointer(paths, json(pointer), "utf8");
|
|
447
|
+
}
|
|
423
448
|
/** Start a recorded route's run through `start`, the scope's own form, then capture its source. */
|
|
424
449
|
async function startRecordedRun(start, plan, input, route) {
|
|
425
450
|
const started = await start({
|
|
426
451
|
cwd: route.cwd,
|
|
427
452
|
runId: input.runId,
|
|
453
|
+
ownerKey: input.ownerKey,
|
|
428
454
|
mintRunId: () => mintRunId(route.prefix),
|
|
429
455
|
mode: plan.dryRun ? "dry-run" : "live",
|
|
430
456
|
study: plan.study,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
2
2
|
import type { SandboxOwnerTags, SandboxProviderId } from "./sandbox-receipts.js";
|
|
3
3
|
/** Refuses a sandbox create for a run directory whose process is stopping. */
|
|
4
4
|
export declare class SandboxCreateRefusedError extends Error {
|
|
@@ -17,10 +17,17 @@ export interface CreatedSandbox {
|
|
|
17
17
|
* The E2B metadata that ties a sandbox to the directory that journals it. `runId` is the
|
|
18
18
|
* directory's name: the run id, or a preflight probe's id. `runKey` is a digest of the directory's
|
|
19
19
|
* inode and creation time, so two projects that both use `--run-id smoke` never match each other's
|
|
20
|
-
* sandboxes, and a renamed project directory still matches its own.
|
|
21
|
-
*
|
|
20
|
+
* sandboxes, and a renamed project directory still matches its own. A directory whose run was given
|
|
21
|
+
* a key (pinRunOwnerKey) uses that key instead, so two directories on different machines, a remote
|
|
22
|
+
* run's and its launcher's, name the same sandboxes. `tool` is the owner tag every route already
|
|
23
|
+
* sets.
|
|
22
24
|
*/
|
|
23
25
|
export declare function sandboxOwnerTags(root: PreparedOutputRoot): SandboxOwnerTags;
|
|
26
|
+
/**
|
|
27
|
+
* Give a new run directory the run key `key` (16 lowercase hex characters) before it creates any
|
|
28
|
+
* sandbox. The file is created once and never replaced.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pinRunOwnerKey(root: PreparedOutputRoot, key: string): Promise<void>;
|
|
24
31
|
/** A create in progress; the acquire step in src/substrates/e2b/sandbox.ts holds one per create. */
|
|
25
32
|
export interface SandboxCreateTicket {
|
|
26
33
|
/** Whether the directory's process has started stopping; checked before each attempt. */
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// sandbox as soon as its id arrives. The tags let `humanish reclaim` find a sandbox whose id
|
|
9
9
|
// never reached this process at all.
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { closeSync, constants, openSync, readSync } from "node:fs";
|
|
11
12
|
import path from "node:path";
|
|
13
|
+
import { createContainedOutputFile } from "./contained-output.js";
|
|
12
14
|
/** Refuses a sandbox create for a run directory whose process is stopping. */
|
|
13
15
|
export class SandboxCreateRefusedError extends Error {
|
|
14
16
|
constructor() {
|
|
@@ -59,21 +61,61 @@ function idle(entry) {
|
|
|
59
61
|
entry.inFlight.size === 0 &&
|
|
60
62
|
[...entry.created.values()].every((sandbox) => sandbox.released));
|
|
61
63
|
}
|
|
64
|
+
/** The file in a run directory that holds a run key its launcher chose. */
|
|
65
|
+
const OWNER_KEY_FILE = "owner-key";
|
|
66
|
+
const OWNER_KEY = /^[0-9a-f]{16}$/;
|
|
62
67
|
/**
|
|
63
68
|
* The E2B metadata that ties a sandbox to the directory that journals it. `runId` is the
|
|
64
69
|
* directory's name: the run id, or a preflight probe's id. `runKey` is a digest of the directory's
|
|
65
70
|
* inode and creation time, so two projects that both use `--run-id smoke` never match each other's
|
|
66
|
-
* sandboxes, and a renamed project directory still matches its own.
|
|
67
|
-
*
|
|
71
|
+
* sandboxes, and a renamed project directory still matches its own. A directory whose run was given
|
|
72
|
+
* a key (pinRunOwnerKey) uses that key instead, so two directories on different machines, a remote
|
|
73
|
+
* run's and its launcher's, name the same sandboxes. `tool` is the owner tag every route already
|
|
74
|
+
* sets.
|
|
68
75
|
*/
|
|
69
76
|
export function sandboxOwnerTags(root) {
|
|
70
77
|
const { where, identity } = identityOf(root);
|
|
71
|
-
const runKey =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const runKey = pinnedOwnerKey(where) ??
|
|
79
|
+
createHash("sha256")
|
|
80
|
+
.update(`${identity.ino}:${identity.birthtimeNs}`)
|
|
81
|
+
.digest("hex")
|
|
82
|
+
.slice(0, 16);
|
|
75
83
|
return { tool: "humanish", runId: path.basename(where), runKey };
|
|
76
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* The key pinned in the run directory at `where`, if any. The file is opened without following a
|
|
87
|
+
* link, as every other file in a run directory is; a missing or malformed file pins nothing.
|
|
88
|
+
*/
|
|
89
|
+
function pinnedOwnerKey(where) {
|
|
90
|
+
let fd;
|
|
91
|
+
try {
|
|
92
|
+
fd = openSync(path.join(where, OWNER_KEY_FILE), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const buffer = Buffer.alloc(64);
|
|
99
|
+
const length = readSync(fd, buffer, 0, buffer.length, 0);
|
|
100
|
+
const key = buffer.subarray(0, length).toString("utf8").trim();
|
|
101
|
+
return OWNER_KEY.test(key) ? key : undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Give a new run directory the run key `key` (16 lowercase hex characters) before it creates any
|
|
112
|
+
* sandbox. The file is created once and never replaced.
|
|
113
|
+
*/
|
|
114
|
+
export async function pinRunOwnerKey(root, key) {
|
|
115
|
+
if (!OWNER_KEY.test(key))
|
|
116
|
+
throw new Error("A run's owner key is 16 lowercase hexadecimal characters.");
|
|
117
|
+
await createContainedOutputFile(root, OWNER_KEY_FILE, `${key}\n`);
|
|
118
|
+
}
|
|
77
119
|
/** Register a create for `root`. Throws SandboxCreateRefusedError once the directory is stopping. */
|
|
78
120
|
export function beginSandboxCreate(root, participantId, provider) {
|
|
79
121
|
const directory = directoryFor(root);
|
package/dist/run/status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AutomaticAnalysisOutcome } from "../analysis/job.js";
|
|
1
2
|
import type { RunBundle } from "./bundle.js";
|
|
2
3
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
3
4
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
@@ -10,11 +11,26 @@ export declare const RUN_STATUS_FILE = "status.json";
|
|
|
10
11
|
export declare const RUN_STATUS_TOUCH_MS = 5000;
|
|
11
12
|
/**
|
|
12
13
|
* A `running` record whose `updatedAt` is older than this is interrupted, not alive: the process
|
|
13
|
-
* died
|
|
14
|
+
* died before the run ended (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
14
15
|
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
15
16
|
*/
|
|
16
17
|
export declare const RUN_STATUS_STALE_MS: number;
|
|
17
18
|
type RunStatusState = "running" | "finished" | "interrupted";
|
|
19
|
+
/**
|
|
20
|
+
* Why a run is `interrupted` without a signal. `no-publish`: its process left the run without
|
|
21
|
+
* publishing a bundle, as a route that throws or returns a refusal after the run started does.
|
|
22
|
+
* `reclaimed`: `humanish reclaim` found its heartbeat stale and stopped its sandboxes.
|
|
23
|
+
*/
|
|
24
|
+
type RunInterruptReason = "no-publish" | "reclaimed";
|
|
25
|
+
/** How automatic analysis ended, as its job records it. */
|
|
26
|
+
type RunAnalysisEndState = AutomaticAnalysisOutcome["state"];
|
|
27
|
+
/** Automatic analysis in the run's own process, after the run ended. */
|
|
28
|
+
interface RunStatusAnalysis {
|
|
29
|
+
startedAt: string;
|
|
30
|
+
/** Absent while it runs, and on the record of a process that stopped during it. */
|
|
31
|
+
endedAt?: string;
|
|
32
|
+
state?: RunAnalysisEndState;
|
|
33
|
+
}
|
|
18
34
|
/** The signals the CLI's run handler records when it stops a run. */
|
|
19
35
|
export type RunInterruptSignal = "SIGINT" | "SIGTERM" | "SIGHUP";
|
|
20
36
|
/** The outcome summary a finalized record carries. Derived from the bundle; never authoritative. */
|
|
@@ -67,7 +83,14 @@ export interface RunStatusRecord {
|
|
|
67
83
|
completedAt?: string;
|
|
68
84
|
/** The signal that stopped the run, on an `interrupted` record. */
|
|
69
85
|
signal?: RunInterruptSignal;
|
|
86
|
+
/** Why an `interrupted` record has no signal. */
|
|
87
|
+
reason?: RunInterruptReason;
|
|
70
88
|
outcome?: RunStatusOutcome;
|
|
89
|
+
/**
|
|
90
|
+
* Set on a `finished` record when automatic analysis starts in the same process. While it
|
|
91
|
+
* runs, the heartbeat keeps `updatedAt` fresh; `completedAt` stays when the participants ended.
|
|
92
|
+
*/
|
|
93
|
+
analysis?: RunStatusAnalysis;
|
|
71
94
|
}
|
|
72
95
|
export interface RunStatusHandle {
|
|
73
96
|
/** Resolves once the initial record has landed on disk, and never rejects: a failed write is
|
|
@@ -75,7 +98,8 @@ export interface RunStatusHandle {
|
|
|
75
98
|
* it before a route can acquire a sandbox, so a run killed after its sandbox receipt lands
|
|
76
99
|
* still has a record to classify. */
|
|
77
100
|
readonly started: Promise<void>;
|
|
78
|
-
/**
|
|
101
|
+
/** The run ended with a published bundle: state `finished`, `completedAt`, and the derived
|
|
102
|
+
* outcome. Stops the cadence.
|
|
79
103
|
* Idempotent: a second call is a no-op, so a route with several exit paths is safe. */
|
|
80
104
|
finish(outcome?: RunStatusOutcome): Promise<void>;
|
|
81
105
|
/** After finish, replace the finished record's outcome with one read from a rewritten run.json,
|
|
@@ -88,6 +112,15 @@ export interface RunStatusHandle {
|
|
|
88
112
|
interrupt(signal: RunInterruptSignal): Promise<boolean>;
|
|
89
113
|
/** True from the moment `interrupt` accepts a signal, before its write lands. */
|
|
90
114
|
readonly interrupted: boolean;
|
|
115
|
+
/** The run's scope closed without a publish: state `interrupted` with reason `no-publish`. A
|
|
116
|
+
* no-op after finish or an interrupt, so it never overwrites a record that says more. */
|
|
117
|
+
abandon(): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* After finish, automatic analysis starts: `analysis.startedAt` is written and the heartbeat
|
|
120
|
+
* runs again until the returned function records the end and its state. Before finish or after
|
|
121
|
+
* an interrupt it writes nothing, and neither does the returned function.
|
|
122
|
+
*/
|
|
123
|
+
beginAnalysis(): Promise<(state: RunAnalysisEndState) => Promise<void>>;
|
|
91
124
|
/** The host suspensions the cadence saw until now, or until it stopped. */
|
|
92
125
|
suspensions(): readonly HostSuspension[];
|
|
93
126
|
}
|
|
@@ -114,6 +147,28 @@ export type RunLiveness = "running" | "interrupted" | "finished";
|
|
|
114
147
|
* `nowMs` is passed in rather than read, so a classification is reproducible.
|
|
115
148
|
*/
|
|
116
149
|
export declare function classifyRunStatus(record: Pick<RunStatusRecord, "state" | "updatedAt">, nowMs: number, staleMs?: number): RunLiveness;
|
|
150
|
+
/** Automatic analysis as a status record shows it: running, ended, or stopped with its process. */
|
|
151
|
+
export type RunAnalysisPhase = {
|
|
152
|
+
state: "running" | "stopped";
|
|
153
|
+
startedAt: string;
|
|
154
|
+
} | {
|
|
155
|
+
state: "ended";
|
|
156
|
+
startedAt: string;
|
|
157
|
+
endedAt: string;
|
|
158
|
+
outcome?: RunAnalysisEndState;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* The record's automatic analysis. `stopped` is an analysis with no end whose heartbeat went
|
|
162
|
+
* stale: the process ended during it. Undefined when the record has none, or one this version
|
|
163
|
+
* cannot read.
|
|
164
|
+
*/
|
|
165
|
+
export declare function runAnalysisPhase(record: Pick<RunStatusRecord, "updatedAt" | "analysis">, nowMs: number): RunAnalysisPhase | undefined;
|
|
166
|
+
/**
|
|
167
|
+
* A `running` record whose heartbeat is stale and whose pid no process on this host holds: its
|
|
168
|
+
* owner is gone, so the run cannot still be writing. A live pid proves nothing either way (pids
|
|
169
|
+
* are reused), so such a record stays unconfirmed.
|
|
170
|
+
*/
|
|
171
|
+
export declare function runOwnerGone(record: Pick<RunStatusRecord, "state" | "pid" | "updatedAt">, nowMs: number, running?: (pid: number) => boolean): boolean;
|
|
117
172
|
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
118
173
|
export declare function isRunStatusRecord(value: unknown): value is RunStatusRecord;
|
|
119
174
|
export {};
|
package/dist/run/status.js
CHANGED
|
@@ -20,7 +20,7 @@ export const RUN_STATUS_FILE = "status.json";
|
|
|
20
20
|
export const RUN_STATUS_TOUCH_MS = 5_000;
|
|
21
21
|
/**
|
|
22
22
|
* A `running` record whose `updatedAt` is older than this is interrupted, not alive: the process
|
|
23
|
-
* died
|
|
23
|
+
* died before the run ended (a dropped SSH, a killed terminal, a crash). Three touch intervals of
|
|
24
24
|
* slack so an ordinary scheduling hiccup or a slow disk never mislabels a healthy run.
|
|
25
25
|
*/
|
|
26
26
|
export const RUN_STATUS_STALE_MS = RUN_STATUS_TOUCH_MS * 3;
|
|
@@ -132,6 +132,48 @@ export function beginRunStatus(runPaths, options) {
|
|
|
132
132
|
get interrupted() {
|
|
133
133
|
return interrupted;
|
|
134
134
|
},
|
|
135
|
+
async abandon() {
|
|
136
|
+
if (finished)
|
|
137
|
+
return;
|
|
138
|
+
finished = true;
|
|
139
|
+
heartbeat.stop();
|
|
140
|
+
const completedAt = iso();
|
|
141
|
+
await write({
|
|
142
|
+
...base,
|
|
143
|
+
state: "interrupted",
|
|
144
|
+
updatedAt: completedAt,
|
|
145
|
+
completedAt,
|
|
146
|
+
reason: "no-publish",
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
async beginAnalysis() {
|
|
150
|
+
const ended = async () => { };
|
|
151
|
+
if (finishedRecord === undefined || interrupted)
|
|
152
|
+
return ended;
|
|
153
|
+
const startedAt = iso();
|
|
154
|
+
finishedRecord = { ...finishedRecord, updatedAt: startedAt, analysis: { startedAt } };
|
|
155
|
+
await write(finishedRecord);
|
|
156
|
+
const analysisHeartbeat = startHeartbeat(clock, RUN_STATUS_TOUCH_MS, () => {
|
|
157
|
+
if (finishedRecord === undefined)
|
|
158
|
+
return;
|
|
159
|
+
finishedRecord = { ...finishedRecord, updatedAt: iso() };
|
|
160
|
+
void write(finishedRecord);
|
|
161
|
+
});
|
|
162
|
+
let done = false;
|
|
163
|
+
return async (state) => {
|
|
164
|
+
if (done || finishedRecord?.analysis === undefined)
|
|
165
|
+
return;
|
|
166
|
+
done = true;
|
|
167
|
+
analysisHeartbeat.stop();
|
|
168
|
+
const endedAt = iso();
|
|
169
|
+
finishedRecord = {
|
|
170
|
+
...finishedRecord,
|
|
171
|
+
updatedAt: endedAt,
|
|
172
|
+
analysis: { ...finishedRecord.analysis, endedAt, state },
|
|
173
|
+
};
|
|
174
|
+
await write(finishedRecord);
|
|
175
|
+
};
|
|
176
|
+
},
|
|
135
177
|
suspensions: () => heartbeat.suspensions(),
|
|
136
178
|
};
|
|
137
179
|
return handle;
|
|
@@ -150,6 +192,50 @@ export function classifyRunStatus(record, nowMs, staleMs = RUN_STATUS_STALE_MS)
|
|
|
150
192
|
return "interrupted";
|
|
151
193
|
return nowMs - updated <= staleMs ? "running" : "interrupted";
|
|
152
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The record's automatic analysis. `stopped` is an analysis with no end whose heartbeat went
|
|
197
|
+
* stale: the process ended during it. Undefined when the record has none, or one this version
|
|
198
|
+
* cannot read.
|
|
199
|
+
*/
|
|
200
|
+
export function runAnalysisPhase(record, nowMs) {
|
|
201
|
+
const analysis = record.analysis;
|
|
202
|
+
if (analysis === null || typeof analysis !== "object")
|
|
203
|
+
return undefined;
|
|
204
|
+
const { startedAt, endedAt, state } = analysis;
|
|
205
|
+
if (typeof startedAt !== "string" || !Number.isFinite(Date.parse(startedAt)))
|
|
206
|
+
return undefined;
|
|
207
|
+
if (typeof endedAt === "string")
|
|
208
|
+
return {
|
|
209
|
+
state: "ended",
|
|
210
|
+
startedAt,
|
|
211
|
+
endedAt,
|
|
212
|
+
...(typeof state === "string" ? { outcome: state } : {}),
|
|
213
|
+
};
|
|
214
|
+
const updated = Date.parse(record.updatedAt);
|
|
215
|
+
return Number.isFinite(updated) && nowMs - updated <= RUN_STATUS_STALE_MS
|
|
216
|
+
? { state: "running", startedAt }
|
|
217
|
+
: { state: "stopped", startedAt };
|
|
218
|
+
}
|
|
219
|
+
/** Whether `pid` names a process on this host. EPERM means one exists that this user cannot signal. */
|
|
220
|
+
function processRunning(pid) {
|
|
221
|
+
try {
|
|
222
|
+
process.kill(pid, 0);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return error.code === "EPERM";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A `running` record whose heartbeat is stale and whose pid no process on this host holds: its
|
|
231
|
+
* owner is gone, so the run cannot still be writing. A live pid proves nothing either way (pids
|
|
232
|
+
* are reused), so such a record stays unconfirmed.
|
|
233
|
+
*/
|
|
234
|
+
export function runOwnerGone(record, nowMs, running = processRunning) {
|
|
235
|
+
if (record.state !== "running" || classifyRunStatus(record, nowMs) === "running")
|
|
236
|
+
return false;
|
|
237
|
+
return record.pid !== process.pid && !running(record.pid);
|
|
238
|
+
}
|
|
153
239
|
/** Shape guard for a record read off disk. Unknown extra fields are tolerated (additive contract). */
|
|
154
240
|
export function isRunStatusRecord(value) {
|
|
155
241
|
if (value === null || typeof value !== "object")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReviewSummary } from "./bundle.js";
|
|
2
2
|
import { type RunDisplay } from "./display.js";
|
|
3
|
+
import { type RunAnalysisPhase } from "./status.js";
|
|
3
4
|
import { type VerifyResult } from "../verify/verify.js";
|
|
4
5
|
declare const RUNS_SCHEMA = "humanish.runs-result.v1";
|
|
5
6
|
export interface RunsResult {
|
|
@@ -13,6 +14,8 @@ export interface RunsResult {
|
|
|
13
14
|
path: string;
|
|
14
15
|
/** How the run reads (runDisplay in src/run/display.ts); absent when it has no readable run.json. */
|
|
15
16
|
display?: RunDisplay;
|
|
17
|
+
/** The automatic analysis its own process started after the run ended, from status.json. */
|
|
18
|
+
analysis?: RunAnalysisPhase;
|
|
16
19
|
}>;
|
|
17
20
|
latest: string | null;
|
|
18
21
|
error?: {
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -6,7 +6,7 @@ import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutpu
|
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
7
|
import { isReviewSummary, isRunBundle, isRunOutcome } from "./bundle-shape.js";
|
|
8
8
|
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
|
-
import { RUN_STATUS_FILE } from "./status.js";
|
|
9
|
+
import { isRunStatusRecord, RUN_STATUS_FILE, runAnalysisPhase, } from "./status.js";
|
|
10
10
|
import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
12
12
|
import { displayedTraces, loadStreamTraces } from "./stream-trace.js";
|
|
@@ -106,10 +106,15 @@ async function listedRunDisplay(runPaths, bundle) {
|
|
|
106
106
|
const displayed = displayedBundle(bundle);
|
|
107
107
|
if (displayed === undefined)
|
|
108
108
|
return {};
|
|
109
|
-
const status =
|
|
110
|
-
|
|
109
|
+
const status = runJsonValue(await readRunJsonIfExists(runPaths, RUN_STATUS_FILE));
|
|
110
|
+
const analysis = isRunStatusRecord(status) && status.runId === displayed.runId
|
|
111
|
+
? runAnalysisPhase(status, Date.now())
|
|
111
112
|
: undefined;
|
|
112
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
// A bundle with an outcome decides how the run ended; status.json only adds its analysis.
|
|
115
|
+
display: runDisplay(bundleDisplayFacts(displayed, displayed.outcome === undefined ? status : undefined)),
|
|
116
|
+
...(analysis === undefined ? {} : { analysis }),
|
|
117
|
+
};
|
|
113
118
|
}
|
|
114
119
|
/** The fields runDisplay reads from an unchecked run.json; none when it does not name a run. */
|
|
115
120
|
function displayedBundle(bundle) {
|
package/dist/run-study.d.ts
CHANGED
|
@@ -81,6 +81,11 @@ interface RunStudyInternals {
|
|
|
81
81
|
scorerProvenance?: RunScorerProvenance;
|
|
82
82
|
/** The local VM study's desktop, analysis gate and signal, for a local browser study. */
|
|
83
83
|
localVm?: LocalVmInput;
|
|
84
|
+
/**
|
|
85
|
+
* The run key its sandboxes carry, chosen by the process that launched this run on a remote
|
|
86
|
+
* machine (the CLI's hidden `--owner-key`). Requires `runId`.
|
|
87
|
+
*/
|
|
88
|
+
ownerKey?: string;
|
|
84
89
|
}
|
|
85
90
|
/** A run's result and the route it ran on. */
|
|
86
91
|
interface RouteOutcome<R extends StudyRoute, T> {
|
package/dist/study/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { externalCatchHealthy } from "../comms/sandbox-catch.js";
|
|
|
12
12
|
import { receivingRequiredKey } from "../comms/setup.js";
|
|
13
13
|
import { classifyCodexInstallation, codexInstallAdvice, codexVersionRecovery, } from "../actors/codex/codex-admission.js";
|
|
14
14
|
import { cli } from "../cli/invocation.js";
|
|
15
|
+
import { analysisKeyNames } from "../keys/key-resolution.js";
|
|
15
16
|
/** A ready row's message, with the schema values the launch recorded. */
|
|
16
17
|
function readyMessage(message, readiness) {
|
|
17
18
|
const additions = protocolAdditionsWarning(readiness.cliVersion, readiness.protocolAdditions);
|
|
@@ -365,7 +366,7 @@ function subjectEnvChecks(names, args) {
|
|
|
365
366
|
: "missing declared subject environment variable; provide it with --dotenv",
|
|
366
367
|
}));
|
|
367
368
|
}
|
|
368
|
-
/** The post-run analysis: the Codex account, or the OpenAI key and the
|
|
369
|
+
/** The post-run analysis: the Codex account, or the OpenAI or Gemini key and the cost limit. */
|
|
369
370
|
async function analysisCheck(analysis, args, checkAccount) {
|
|
370
371
|
if (analysis.provider === "codex") {
|
|
371
372
|
const readiness = await checkAccount();
|
|
@@ -382,6 +383,18 @@ async function analysisCheck(analysis, args, checkAccount) {
|
|
|
382
383
|
: `Codex account analysis is unavailable (${readiness.errorCode ?? "codex_unavailable"}). ${recovery} No API fallback is used; participant readiness is independent.`,
|
|
383
384
|
};
|
|
384
385
|
}
|
|
386
|
+
if (analysis.provider === "gemini") {
|
|
387
|
+
const names = analysisKeyNames("gemini");
|
|
388
|
+
const geminiKeyed = names.some(args.keyPresent);
|
|
389
|
+
return {
|
|
390
|
+
name: "post-run analysis",
|
|
391
|
+
ok: true,
|
|
392
|
+
...(geminiKeyed ? {} : { status: "note" }),
|
|
393
|
+
message: geminiKeyed
|
|
394
|
+
? `A Gemini API key is present for the separate automatic analysis request, which sends the evidence and captures to Google; model access and quota are not tested. The analysis is refused before it starts if ${admissionRule(`$${analysis.maxCostUsd}`)}; this is not a billing cap. Participant readiness is independent.`
|
|
395
|
+
: `Will be skipped: ${names.join(" and ")} are missing. The participant may run, but there will be no automatic findings report. Add it with ${cli("keys set gemini")}, or set review.analysis: false deliberately.`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
385
398
|
const keyed = args.keyPresent("OPENAI_API_KEY");
|
|
386
399
|
return {
|
|
387
400
|
name: "post-run analysis",
|
|
@@ -10,7 +10,7 @@ export declare function requiredSubjectEnv(requirements: readonly Requirement[])
|
|
|
10
10
|
/**
|
|
11
11
|
* The provider keys a live run of `plan` is known to read: its `key` and `key-one-of` names, its
|
|
12
12
|
* subject env, the external catch's token variable, ANTHROPIC_API_KEY for a Claude Code
|
|
13
|
-
* participant, and
|
|
13
|
+
* participant, and the analyst's key when automatic analysis runs on OpenAI or Gemini. `run` prints a
|
|
14
14
|
* key-source line for these only. Discovery still fills every key it finds: other readers, such as
|
|
15
15
|
* a declared scorer's host code, are not in the plan.
|
|
16
16
|
*/
|