humanish 0.141.0 → 0.143.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 +21 -0
- package/README.md +3 -1
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/automatic-config.d.ts +17 -3
- package/dist/analysis/automatic-config.js +56 -12
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +6 -5
- package/dist/analysis/execute.js +27 -26
- package/dist/analysis/gemini-wire.d.ts +9 -0
- package/dist/analysis/gemini-wire.js +166 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +1 -0
- package/dist/analysis/job.js +1 -0
- package/dist/analysis/models.d.ts +12 -0
- package/dist/analysis/models.js +47 -0
- package/dist/analysis/openai-wire.d.ts +6 -0
- package/dist/analysis/openai-wire.js +102 -0
- package/dist/analysis/provider.d.ts +19 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.js +11 -3
- package/dist/analysis/types.d.ts +12 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +17 -15
- package/dist/cli/commands/analyze.js +11 -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/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +26 -2
- package/dist/cli/commands/study-route-scripted.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +1 -0
- package/dist/cli/commands/study-route-terminal.js +1 -0
- 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 +9 -1
- package/dist/cli/findings.js +62 -17
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +2 -1
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- 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-app.html +1 -1
- package/dist/remote/launch.d.ts +56 -0
- package/dist/remote/launch.js +365 -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 +84 -0
- package/dist/remote/runner.js +241 -0
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- 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/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run.d.ts +13 -0
- package/dist/run/run.js +18 -7
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- 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 +1 -1
- 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 +24 -6
- 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/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +46 -46
- package/docs/contracts/study-analysis.md +85 -4
- package/docs/product/automatic-analysis.md +42 -8
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +18 -1
package/dist/run/run-cost.js
CHANGED
|
@@ -77,8 +77,13 @@ function estimateText(usd, complete) {
|
|
|
77
77
|
function analysesNoun(requests) {
|
|
78
78
|
return requests === 1 ? "analysis" : `${requests} analyses`;
|
|
79
79
|
}
|
|
80
|
+
const BILLED_BY = {
|
|
81
|
+
openai: "OpenAI API key",
|
|
82
|
+
gemini: "Gemini API key",
|
|
83
|
+
codex: "Codex account",
|
|
84
|
+
};
|
|
80
85
|
function billedBy(providers) {
|
|
81
|
-
const names = providers.map((provider) => provider
|
|
86
|
+
const names = providers.map((provider) => BILLED_BY[provider]);
|
|
82
87
|
return names.length === 0 ? "" : ` (${names.join(" and ")})`;
|
|
83
88
|
}
|
|
84
89
|
/**
|
package/dist/run/run.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ interface StartRunOptions {
|
|
|
11
11
|
cwd: string;
|
|
12
12
|
/** Caller-supplied id (`--run-id`, library `runId`); `mintRunId` runs when it is absent. */
|
|
13
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;
|
|
14
20
|
mintRunId: () => string;
|
|
15
21
|
mode: "dry-run" | "live";
|
|
16
22
|
study?: RunStudyProvenance | undefined;
|
|
@@ -44,6 +50,8 @@ interface StartRunPlan {
|
|
|
44
50
|
interface StartRunInput {
|
|
45
51
|
/** Caller-supplied id (`--run-id`, library `runId`); one is minted from the prefix when absent. */
|
|
46
52
|
readonly runId?: string | undefined;
|
|
53
|
+
/** The run key its sandboxes carry; see StartRunOptions.ownerKey. */
|
|
54
|
+
readonly ownerKey?: string | undefined;
|
|
47
55
|
/** Open the Observer page once it renders. */
|
|
48
56
|
readonly open?: boolean | undefined;
|
|
49
57
|
/**
|
|
@@ -215,4 +223,9 @@ export declare function runScope<T>(fn: (scope: RunScope) => Promise<T>, hooks?:
|
|
|
215
223
|
result: T;
|
|
216
224
|
finished: FinishedRun | undefined;
|
|
217
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>;
|
|
218
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 {
|
|
@@ -203,13 +204,7 @@ function runPublisher(args) {
|
|
|
203
204
|
return written;
|
|
204
205
|
};
|
|
205
206
|
const writePointer = async () => {
|
|
206
|
-
|
|
207
|
-
schema: "humanish.latest-run.v1",
|
|
208
|
-
runId,
|
|
209
|
-
path: paths.relativeRunRoot,
|
|
210
|
-
updatedAt: new Date(now()).toISOString(),
|
|
211
|
-
};
|
|
212
|
-
await writePreparedRunLatestPointer(paths, json(pointer), "utf8");
|
|
207
|
+
await pointLatestRunAt(paths, runId, now);
|
|
213
208
|
pointerWritten = true;
|
|
214
209
|
};
|
|
215
210
|
// Each running participant's items so far, appended to its live file as the flushes arrive.
|
|
@@ -326,6 +321,8 @@ export async function runScope(fn, hooks = {}) {
|
|
|
326
321
|
const created = await createRunArtifactPaths(options.cwd, runId);
|
|
327
322
|
if (!created.ok)
|
|
328
323
|
return created;
|
|
324
|
+
if (options.ownerKey !== undefined)
|
|
325
|
+
await pinRunOwnerKey(created.paths, options.ownerKey);
|
|
329
326
|
// beginRunStatus reads only the mode, the study provenance, the sandboxes field and the
|
|
330
327
|
// clock from the run's options.
|
|
331
328
|
const runStatus = beginRunStatus(created.paths, { ...options, runId });
|
|
@@ -435,11 +432,25 @@ export async function runScope(fn, hooks = {}) {
|
|
|
435
432
|
}
|
|
436
433
|
return { result: outcome.result, finished };
|
|
437
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
|
+
}
|
|
438
448
|
/** Start a recorded route's run through `start`, the scope's own form, then capture its source. */
|
|
439
449
|
async function startRecordedRun(start, plan, input, route) {
|
|
440
450
|
const started = await start({
|
|
441
451
|
cwd: route.cwd,
|
|
442
452
|
runId: input.runId,
|
|
453
|
+
ownerKey: input.ownerKey,
|
|
443
454
|
mintRunId: () => mintRunId(route.prefix),
|
|
444
455
|
mode: plan.dryRun ? "dry-run" : "live",
|
|
445
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-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
|
*/
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// key (`key-one-of`) stays with buildRuntimeAuth at run time, which also picks its placement.
|
|
7
7
|
import { detectLocalAgents } from "../actors/local-agent/cli.js";
|
|
8
8
|
import { localAgentRefusal } from "../actors/local-agent/readiness.js";
|
|
9
|
-
import { describeMissingKeys } from "../keys/key-resolution.js";
|
|
10
|
-
import {
|
|
9
|
+
import { analysisKeyNames, describeMissingKeys } from "../keys/key-resolution.js";
|
|
10
|
+
import { participantRate, unpricedCapMessage } from "../run/pricing.js";
|
|
11
11
|
/** Provider keys in the order the routes name them in a refusal. */
|
|
12
12
|
const KEY_ORDER = [
|
|
13
13
|
"OPENAI_API_KEY",
|
|
@@ -45,7 +45,7 @@ export function requiredSubjectEnv(requirements) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The provider keys a live run of `plan` is known to read: its `key` and `key-one-of` names, its
|
|
47
47
|
* subject env, the external catch's token variable, ANTHROPIC_API_KEY for a Claude Code
|
|
48
|
-
* participant, and
|
|
48
|
+
* participant, and the analyst's key when automatic analysis runs on OpenAI or Gemini. `run` prints a
|
|
49
49
|
* key-source line for these only. Discovery still fills every key it finds: other readers, such as
|
|
50
50
|
* a declared scorer's host code, are not in the plan.
|
|
51
51
|
*/
|
|
@@ -63,8 +63,10 @@ export function keyNamesOf(plan) {
|
|
|
63
63
|
const catchToken = plan.residual.comms?.email?.external?.authTokenEnv;
|
|
64
64
|
if (catchToken !== undefined)
|
|
65
65
|
names.add(catchToken);
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const analyst = plan.analysis?.config.provider;
|
|
67
|
+
if (plan.analysis !== undefined && analyst !== "codex")
|
|
68
|
+
for (const name of analysisKeyNames(analyst ?? "openai"))
|
|
69
|
+
names.add(name);
|
|
68
70
|
return names;
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
@@ -140,7 +142,7 @@ export function unpricedCapCheck(args) {
|
|
|
140
142
|
const { caps, model } = args;
|
|
141
143
|
if (caps.maxUsd === undefined && caps.maxTotalUsd === undefined)
|
|
142
144
|
return undefined;
|
|
143
|
-
if (
|
|
145
|
+
if (participantRate(model))
|
|
144
146
|
return undefined;
|
|
145
147
|
return { code: args.code, message: unpricedCapMessage(model) };
|
|
146
148
|
}
|
|
@@ -10,6 +10,7 @@ export function computerUseInput(options, deps, emit) {
|
|
|
10
10
|
...(options.open === undefined ? {} : { open: options.open }),
|
|
11
11
|
...(options.onObserverReady === undefined ? {} : { onObserverReady: options.onObserverReady }),
|
|
12
12
|
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
13
|
+
...(options.ownerKey === undefined ? {} : { ownerKey: options.ownerKey }),
|
|
13
14
|
...(options.rerun === undefined ? {} : { rerun: options.rerun }),
|
|
14
15
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
15
16
|
...(options.prepareDesktop === undefined ? {} : { prepareDesktop: options.prepareDesktop }),
|
|
@@ -28,6 +29,7 @@ export function scriptedInput(options, deps, emit) {
|
|
|
28
29
|
cwd: options.cwd,
|
|
29
30
|
...(options.open === undefined ? {} : { open: options.open }),
|
|
30
31
|
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
32
|
+
...(options.ownerKey === undefined ? {} : { ownerKey: options.ownerKey }),
|
|
31
33
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
32
34
|
...(options.prepareDesktop === undefined ? {} : { prepareDesktop: options.prepareDesktop }),
|
|
33
35
|
deps,
|
|
@@ -40,6 +42,7 @@ export function terminalInput(options, deps, emit) {
|
|
|
40
42
|
cwd: options.cwd,
|
|
41
43
|
...(options.open === undefined ? {} : { open: options.open }),
|
|
42
44
|
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
45
|
+
...(options.ownerKey === undefined ? {} : { ownerKey: options.ownerKey }),
|
|
43
46
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
44
47
|
deps,
|
|
45
48
|
...scorerOf(options, terminalRouteScorer),
|
|
@@ -52,6 +55,7 @@ export function sharedWorldInput(options, deps, emit) {
|
|
|
52
55
|
...(options.open === undefined ? {} : { open: options.open }),
|
|
53
56
|
...(options.onObserverReady === undefined ? {} : { onObserverReady: options.onObserverReady }),
|
|
54
57
|
...(options.runId === undefined ? {} : { runId: options.runId }),
|
|
58
|
+
...(options.ownerKey === undefined ? {} : { ownerKey: options.ownerKey }),
|
|
55
59
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
56
60
|
...(options.prepareDesktop === undefined ? {} : { prepareDesktop: options.prepareDesktop }),
|
|
57
61
|
...observersOf(options, emit),
|
|
@@ -54,7 +54,7 @@ export type StudyEvent = {
|
|
|
54
54
|
/**
|
|
55
55
|
* The literal values to scrub from an onEvent warning: the provider keys and the declared subject
|
|
56
56
|
* env from every env the run could read (the `env` option and process.env), and the
|
|
57
|
-
* analysis API
|
|
57
|
+
* analysis API keys, OpenAI's and Gemini's. A callback can hold any of them, and its warning is appended after the route
|
|
58
58
|
* sanitized its own.
|
|
59
59
|
*/
|
|
60
60
|
export declare function knownSecrets(config: StudyConfig, options: InternalRunStudyOptions, forwardedEnv: Readonly<Record<string, string | undefined>> | undefined): RunSecrets;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// What a run reports through RunStudyOptions.onEvent, and how a callback failure becomes a run
|
|
2
2
|
// warning. normalizeRunStudyOptions (run-study-options.ts) builds the emitter and hands it to the
|
|
3
3
|
// route hook bags it maps; the event values are built here.
|
|
4
|
+
import { analysisKeyNames } from "../keys/key-resolution.js";
|
|
4
5
|
import { redactText, toErrorMessage } from "../evidence/redaction.js";
|
|
5
6
|
import { RunSecrets } from "../run/secrets.js";
|
|
6
7
|
/**
|
|
7
8
|
* The literal values to scrub from an onEvent warning: the provider keys and the declared subject
|
|
8
9
|
* env from every env the run could read (the `env` option and process.env), and the
|
|
9
|
-
* analysis API
|
|
10
|
+
* analysis API keys, OpenAI's and Gemini's. A callback can hold any of them, and its warning is appended after the route
|
|
10
11
|
* sanitized its own.
|
|
11
12
|
*/
|
|
12
13
|
export function knownSecrets(config, options, forwardedEnv) {
|
|
13
14
|
const sources = [forwardedEnv, options.env, process.env];
|
|
14
|
-
const names = [
|
|
15
|
+
const names = [
|
|
16
|
+
"OPENAI_API_KEY",
|
|
17
|
+
"E2B_API_KEY",
|
|
18
|
+
"CODEX_API_KEY",
|
|
19
|
+
...analysisKeyNames("gemini"),
|
|
20
|
+
...(config.subject.env ?? []),
|
|
21
|
+
];
|
|
15
22
|
const secrets = new RunSecrets([]);
|
|
16
23
|
for (const env of sources)
|
|
17
24
|
for (const name of names)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// E2B's base sandbox, the `base` template with no desktop, behind the same sandbox port as the
|
|
2
|
+
// desktop, so acquire(), receipts, release and reclaim work on it unchanged. A sandbox that runs
|
|
3
|
+
// humanish itself needs commands, files and one authenticated port; the desktop template would
|
|
4
|
+
// start an X session nobody uses and bill its 8 vCPU.
|
|
5
|
+
//
|
|
6
|
+
// @e2b/desktop re-exports the e2b SDK but replaces its Sandbox with the desktop subclass, and a
|
|
7
|
+
// strict install does not let humanish import `e2b`, which is only @e2b/desktop's dependency. The
|
|
8
|
+
// base class is the desktop class's parent.
|
|
9
|
+
import { guardDesktopSandboxCreate, importE2BDesktop, } from "./sdk.js";
|
|
10
|
+
import { shellOnlySurface } from "./shell-only.js";
|
|
11
|
+
/** The header E2B's proxy reads a sandbox's traffic access token from (spike, 2026-10-21). */
|
|
12
|
+
const TRAFFIC_TOKEN_HEADER = "e2b-traffic-access-token";
|
|
13
|
+
class E2BDesktopOnlyError extends Error {
|
|
14
|
+
constructor(what) {
|
|
15
|
+
super(`${what} needs a desktop sandbox; this E2B sandbox runs the base template.`);
|
|
16
|
+
this.name = "E2BDesktopOnlyError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** A sandbox port that creates and connects to E2B base-template sandboxes. */
|
|
20
|
+
export async function loadE2BBaseModule() {
|
|
21
|
+
return guardDesktopSandboxCreate({ Sandbox: baseSandbox(await importE2BDesktop()) });
|
|
22
|
+
}
|
|
23
|
+
function baseSandbox(desktop) {
|
|
24
|
+
const Base = Object.getPrototypeOf(desktop.Sandbox);
|
|
25
|
+
if (typeof Base.create !== "function")
|
|
26
|
+
throw new Error("The installed @e2b/desktop does not extend the e2b SDK's Sandbox; update @e2b/desktop.");
|
|
27
|
+
class BaseSandbox extends Base {
|
|
28
|
+
constructor(...args) {
|
|
29
|
+
super(...args);
|
|
30
|
+
Object.assign(this, shellOnlySurface((what) => new E2BDesktopOnlyError(what)));
|
|
31
|
+
}
|
|
32
|
+
async portAccess(port) {
|
|
33
|
+
const token = this.trafficAccessToken;
|
|
34
|
+
if (token === undefined || token === "")
|
|
35
|
+
throw new Error("This E2B sandbox accepts public traffic, so it has no traffic access token. Create it with network.allowPublicTraffic set to false.");
|
|
36
|
+
return { url: `https://${this.getHost(port)}`, headers: { [TRAFFIC_TOKEN_HEADER]: token } };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return BaseSandbox;
|
|
40
|
+
}
|
|
@@ -81,4 +81,4 @@ extraChromiumFlags?: readonly string[], environment?: Readonly<Record<string, st
|
|
|
81
81
|
* from it run in that zone.
|
|
82
82
|
*/
|
|
83
83
|
export declare function openDesktopTerminal(desktop: E2BDesktopSandbox, requestTimeoutMs: number, workdir: string | undefined, timeZone?: string): Promise<void>;
|
|
84
|
-
export declare function startDesktopStream(desktop: E2BDesktopSandbox, browserWindowId: string | undefined): Promise<void>;
|
|
84
|
+
export declare function startDesktopStream(desktop: E2BDesktopSandbox, browserWindowId: string | undefined, requestTimeoutMs: number): Promise<void>;
|
|
@@ -402,15 +402,33 @@ export async function openDesktopTerminal(desktop, requestTimeoutMs, workdir, ti
|
|
|
402
402
|
throw new Error("desktop-cli participant could not open a terminal on this desktop template");
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
-
export async function startDesktopStream(desktop, browserWindowId) {
|
|
405
|
+
export async function startDesktopStream(desktop, browserWindowId, requestTimeoutMs) {
|
|
406
406
|
if (!browserWindowId) {
|
|
407
407
|
await desktop.stream.start({ requireAuth: true });
|
|
408
|
-
return;
|
|
409
408
|
}
|
|
410
|
-
|
|
411
|
-
|
|
409
|
+
else {
|
|
410
|
+
try {
|
|
411
|
+
await desktop.stream.start({ requireAuth: true, windowId: browserWindowId });
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
await desktop.stream.start({ requireAuth: true });
|
|
415
|
+
}
|
|
412
416
|
}
|
|
413
|
-
|
|
414
|
-
|
|
417
|
+
await makeStreamViewOnly(desktop, requestTimeoutMs);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Switches the stream's VNC server to view-only and confirms it. Both providers start x11vnc
|
|
421
|
+
* without `-viewonly`, and the URL's `view_only` is a noVNC client setting, so anyone holding the
|
|
422
|
+
* URL could otherwise type into the participant's desktop. x11vnc takes the change over its X
|
|
423
|
+
* property channel and answers the query with `ans=viewonly:1`; anything else throws, and the
|
|
424
|
+
* caller treats the stream as unavailable.
|
|
425
|
+
*/
|
|
426
|
+
async function makeStreamViewOnly(desktop, requestTimeoutMs) {
|
|
427
|
+
const shell = e2bShell(desktop);
|
|
428
|
+
const options = { timeoutMs: 20_000, requestTimeoutMs };
|
|
429
|
+
await shell.run("x11vnc -display :0 -sync -R viewonly", options);
|
|
430
|
+
const query = await shell.run("x11vnc -display :0 -Q viewonly", options);
|
|
431
|
+
if (!/^ans=viewonly:1$/m.test(query.stdout)) {
|
|
432
|
+
throw new Error("the live stream could not be made view-only");
|
|
415
433
|
}
|
|
416
434
|
}
|
|
@@ -30,6 +30,12 @@ export interface E2BDesktopModule {
|
|
|
30
30
|
* (2.38 and later) returns a paginator.
|
|
31
31
|
*/
|
|
32
32
|
list?(options?: E2BSandboxListOptions): E2BSandboxPaginator;
|
|
33
|
+
/**
|
|
34
|
+
* A handle on a sandbox that exists, by exact id, with the same calls a create returns. Throws
|
|
35
|
+
* a SandboxNotFoundError-shaped error when the id is gone. A process that did not create the
|
|
36
|
+
* sandbox uses it to run commands there and reach its ports.
|
|
37
|
+
*/
|
|
38
|
+
connect?(sandboxId: string, options?: E2BRequestOptions): Promise<E2BDesktopSandbox>;
|
|
33
39
|
};
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
@@ -84,6 +90,17 @@ export interface E2BNetworkOptions {
|
|
|
84
90
|
headers?: Record<string, string>;
|
|
85
91
|
};
|
|
86
92
|
}[]>;
|
|
93
|
+
/**
|
|
94
|
+
* False makes every URL `getHost` gives answer only requests that carry the sandbox's traffic
|
|
95
|
+
* access token. E2B's own default is true.
|
|
96
|
+
*/
|
|
97
|
+
allowPublicTraffic?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/** A URL that reaches a port inside a sandbox, and the headers the provider requires on each request. */
|
|
100
|
+
export interface SandboxPortAccess {
|
|
101
|
+
url: string;
|
|
102
|
+
/** Credentials: never persist or log them. */
|
|
103
|
+
headers: Record<string, string>;
|
|
87
104
|
}
|
|
88
105
|
/**
|
|
89
106
|
* A create names its account. e2bAccount (connection.ts) gives every later call on the sandbox the
|
|
@@ -172,6 +189,11 @@ export interface E2BDesktopSandbox {
|
|
|
172
189
|
* the concurrent backend fails closed when it is absent rather than calling a missing method.
|
|
173
190
|
*/
|
|
174
191
|
getHost?(port: number): string;
|
|
192
|
+
/**
|
|
193
|
+
* The URL and credential headers that reach `port`, for a sandbox whose provider authenticates
|
|
194
|
+
* port traffic: an E2B sandbox created with `allowPublicTraffic: false`, or any Modal sandbox.
|
|
195
|
+
*/
|
|
196
|
+
portAccess?(port: number): Promise<SandboxPortAccess>;
|
|
175
197
|
screenshot(format?: "bytes"): Promise<Uint8Array>;
|
|
176
198
|
wait(ms: number): Promise<void>;
|
|
177
199
|
leftClick(x?: number, y?: number): Promise<void>;
|
|
@@ -202,6 +224,8 @@ export interface E2BDesktopSandbox {
|
|
|
202
224
|
};
|
|
203
225
|
}
|
|
204
226
|
export declare function loadE2BDesktopModule(): Promise<E2BDesktopModule>;
|
|
227
|
+
/** The optional @e2b/desktop peer as it is, or an error that says how to install it. */
|
|
228
|
+
export declare function importE2BDesktop(): Promise<E2BDesktopModule>;
|
|
205
229
|
export declare const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10000;
|
|
206
230
|
/**
|
|
207
231
|
* Call `observer` with the sandbox id as soon as the guarded SDK constructs the handle for a
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
import { protectDesktopScreenshotCleanup } from "./desktop-screenshot-cleanup.js";
|
|
9
9
|
import { desktopPeerAdvice, humanishInstall } from "./peer-install.js";
|
|
10
10
|
export async function loadE2BDesktopModule() {
|
|
11
|
+
return guardDesktopSandboxCreate(await importE2BDesktop());
|
|
12
|
+
}
|
|
13
|
+
/** The optional @e2b/desktop peer as it is, or an error that says how to install it. */
|
|
14
|
+
export async function importE2BDesktop() {
|
|
11
15
|
try {
|
|
12
|
-
return
|
|
16
|
+
return (await import("@e2b/desktop"));
|
|
13
17
|
}
|
|
14
18
|
catch (error) {
|
|
15
19
|
if (isMissingE2BDesktopDependency(error)) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { E2BDesktopSandbox } from "./sdk.js";
|
|
2
|
+
type DesktopSurface = Pick<E2BDesktopSandbox, "wait" | "launch" | "screenshot" | "leftClick" | "rightClick" | "middleClick" | "doubleClick" | "moveMouse" | "scroll" | "write" | "press" | "drag" | "stream">;
|
|
3
|
+
/** Desktop calls that reject with `unsupported(what)`; `wait` still waits. */
|
|
4
|
+
export declare function shellOnlySurface(unsupported: (what: string) => Error): DesktopSurface;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Desktop calls that reject with `unsupported(what)`; `wait` still waits. */
|
|
2
|
+
export function shellOnlySurface(unsupported) {
|
|
3
|
+
const reject = (what) => () => Promise.reject(unsupported(what));
|
|
4
|
+
return {
|
|
5
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
6
|
+
launch: reject("A desktop application launch"),
|
|
7
|
+
screenshot: reject("A desktop screenshot"),
|
|
8
|
+
leftClick: reject("Desktop input"),
|
|
9
|
+
rightClick: reject("Desktop input"),
|
|
10
|
+
middleClick: reject("Desktop input"),
|
|
11
|
+
doubleClick: reject("Desktop input"),
|
|
12
|
+
moveMouse: reject("Desktop input"),
|
|
13
|
+
scroll: reject("Desktop input"),
|
|
14
|
+
write: reject("Desktop input"),
|
|
15
|
+
press: reject("Desktop input"),
|
|
16
|
+
drag: reject("Desktop input"),
|
|
17
|
+
stream: {
|
|
18
|
+
getAuthKey: () => {
|
|
19
|
+
throw unsupported("A desktop stream");
|
|
20
|
+
},
|
|
21
|
+
getUrl: () => {
|
|
22
|
+
throw unsupported("A desktop stream");
|
|
23
|
+
},
|
|
24
|
+
start: reject("A desktop stream"),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|