humanish 0.144.0 → 0.146.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/CHANGELOG.md +22 -0
- package/dist/analysis/admission.d.ts +8 -3
- package/dist/analysis/admission.js +7 -3
- package/dist/analysis/automatic-config.js +2 -1
- package/dist/analysis/automatic.js +2 -2
- package/dist/analysis/execute.d.ts +7 -10
- package/dist/analysis/execute.js +79 -49
- package/dist/analysis/models.d.ts +2 -0
- package/dist/analysis/models.js +15 -0
- package/dist/analysis/openai-wire.d.ts +1 -1
- package/dist/analysis/openai-wire.js +36 -26
- package/dist/analysis/output-limit.d.ts +12 -0
- package/dist/analysis/output-limit.js +34 -0
- package/dist/analysis/output-retry.d.ts +20 -0
- package/dist/analysis/output-retry.js +63 -0
- package/dist/analysis/provider.d.ts +1 -1
- package/dist/analysis/provider.js +3 -3
- package/dist/analysis/responses.js +1 -1
- package/dist/analysis/service.js +2 -2
- package/dist/analysis/validation.js +1 -1
- package/dist/cli/commands/analyze.js +1 -1
- package/dist/cli/commands/study-remote.d.ts +3 -1
- package/dist/cli/commands/study-remote.js +18 -3
- package/dist/cli/io.js +2 -0
- package/dist/cli/run-findings.js +10 -2
- package/dist/keys/key-resolution.js +2 -2
- package/dist/observer-app.html +1 -1
- package/dist/remote/launch.d.ts +21 -3
- package/dist/remote/launch.js +111 -52
- package/dist/remote/runner.d.ts +47 -14
- package/dist/remote/runner.js +199 -64
- package/dist/routes/computer-use/e2b-desktop/desktop.js +47 -7
- package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +4 -2
- package/dist/routes/computer-use/e2b-desktop/prepare.js +4 -2
- package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -6
- package/dist/routes/computer-use/e2b-desktop/start.js +72 -34
- package/dist/routes/terminal/credentials.js +3 -2
- package/dist/routes/terminal/live-sandbox.js +2 -2
- package/dist/routes/terminal/runtime-auth.d.ts +0 -12
- package/dist/routes/terminal/runtime-auth.js +0 -23
- package/dist/substrates/e2b/cdp-probe.d.ts +16 -1
- package/dist/substrates/e2b/cdp-probe.js +67 -5
- package/dist/substrates/e2b/desktop-browser.js +4 -2
- package/dist/substrates/e2b/desktop-cdp.d.ts +9 -2
- package/dist/substrates/e2b/desktop-cdp.js +26 -4
- package/dist/substrates/e2b/egress-keys.d.ts +22 -0
- package/dist/substrates/e2b/egress-keys.js +59 -0
- package/docs/contracts/schemas.md +1 -1
- package/docs/contracts/study-analysis.md +75 -9
- package/docs/product/automatic-analysis.md +9 -6
- package/package.json +5 -5
- package/skills/humanish/SKILL.md +2 -1
package/dist/remote/launch.js
CHANGED
|
@@ -15,14 +15,17 @@ import { pointLatestRunAt } from "../run/run.js";
|
|
|
15
15
|
import { appendSandboxOwner, SANDBOX_RECEIPTS_ARTIFACT } from "../run/sandbox-receipts.js";
|
|
16
16
|
import { loadE2BBaseModule } from "../substrates/e2b/base-module.js";
|
|
17
17
|
import { e2bConnection } from "../substrates/e2b/connection.js";
|
|
18
|
+
import { E2B_SYSTEM_CA_BUNDLE, holdKeysInEgress } from "../substrates/e2b/egress-keys.js";
|
|
18
19
|
import { acquireE2BShellSandbox } from "../substrates/e2b/sandbox.js";
|
|
19
20
|
import { loadModalShellModule } from "../substrates/modal/sandbox.js";
|
|
20
21
|
import { serveObserverProxy } from "./observer-proxy.js";
|
|
21
|
-
import {
|
|
22
|
-
/**
|
|
23
|
-
const
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
22
|
+
import { listRemoteRun, listSettledEvidence, readProject, readRemoteBytes, readRemoteFiles, RUNNER_OBSERVER_PORT, runnerOf, setUpRunner, startRemoteRun, stopRemoteRun, waitForRemoteRun, } from "./runner.js";
|
|
23
|
+
/** The longest one read of the runner's run waits there for a change. */
|
|
24
|
+
const WAIT_MS = 5_000;
|
|
25
|
+
/** How long the launcher waits after a read the runner did not answer before it reads again. */
|
|
26
|
+
const RETRY_MS = 5_000;
|
|
27
|
+
/** How long the runner may leave reads unanswered before the launcher gives it up. */
|
|
28
|
+
const UNANSWERED_LIMIT_MS = 120_000;
|
|
26
29
|
/** How long the run has to write its status record after it is started. */
|
|
27
30
|
const START_DEADLINE_MS = 3 * 60_000;
|
|
28
31
|
/** How often the launcher copies the run's settled evidence while it goes on. */
|
|
@@ -51,21 +54,33 @@ export async function launchRemoteRun(launch) {
|
|
|
51
54
|
// Installed before the first sandbox exists, so a signal never ends this process while one is
|
|
52
55
|
// being created without a cleanup.
|
|
53
56
|
const signals = watchSignals();
|
|
54
|
-
let tarballDir;
|
|
55
57
|
let proxy;
|
|
58
|
+
let tarball;
|
|
56
59
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
// Read and checked before any sandbox exists, so a refused file costs nothing.
|
|
61
|
+
const project = await readProject(cwd, remote.uploads, remote.uploadDirectories);
|
|
62
|
+
// Packed while the runner is created and Node installs in it.
|
|
63
|
+
tarball = packThisHumanish();
|
|
64
|
+
tarball.catch(() => undefined);
|
|
59
65
|
if (signals.requested !== undefined)
|
|
60
66
|
return { runId, exitCode: signals.exitCode, error: "stopped before the runner was created" };
|
|
61
67
|
const where = remote.provider === "modal" ? "a Modal sandbox" : "an E2B sandbox";
|
|
62
68
|
say(`Running this study on ${where} as run ${runId}; creating the runner.`);
|
|
63
|
-
const
|
|
69
|
+
const keys = runnerKeys(remote, launch.env);
|
|
70
|
+
const runner = runnerOf((await acquireRunner(launch, paths, keys.network)).sandbox);
|
|
64
71
|
if (signals.requested !== undefined)
|
|
65
72
|
return await endRun(launch, runId, {
|
|
66
73
|
exitCode: signals.exitCode,
|
|
67
74
|
error: "stopped before the run started",
|
|
68
75
|
});
|
|
76
|
+
// Setting up can take minutes; a signal stops waiting for it, and the reclaim below stops the
|
|
77
|
+
// runner, and the setup with it. A setup left behind that way fails once its runner is gone.
|
|
78
|
+
const setup = setUpRunner(runner, {
|
|
79
|
+
tarball,
|
|
80
|
+
dependencies: [...(await ownDependencies()), await peerSpec(remote.provider)],
|
|
81
|
+
project,
|
|
82
|
+
});
|
|
83
|
+
setup.catch(() => undefined);
|
|
69
84
|
await writeRemoteRecord(paths, {
|
|
70
85
|
runId,
|
|
71
86
|
provider: remote.provider,
|
|
@@ -73,29 +88,18 @@ export async function launchRemoteRun(launch) {
|
|
|
73
88
|
});
|
|
74
89
|
// Bound before the run starts, so a taken --port fails before any participant exists.
|
|
75
90
|
proxy = await serveObserverProxy(await runner.sandbox.portAccess(RUNNER_OBSERVER_PORT), launch.port);
|
|
76
|
-
|
|
77
|
-
// runner, and the install with it.
|
|
78
|
-
const installing = installHumanish(runner, tarball.bytes, [await peerSpec(remote.provider)]);
|
|
79
|
-
// An install left behind by a signal fails once its runner is gone; that is expected.
|
|
80
|
-
installing.catch(() => undefined);
|
|
81
|
-
const version = await Promise.race([installing, signals.received.then(() => "")]);
|
|
82
|
-
if (signals.requested !== undefined)
|
|
83
|
-
return await endRun(launch, runId, {
|
|
84
|
-
exitCode: signals.exitCode,
|
|
85
|
-
error: "stopped before the run started",
|
|
86
|
-
});
|
|
87
|
-
say(`Installed humanish ${version} in the runner; uploading the study.`);
|
|
88
|
-
await uploadProject(runner, cwd, remote.uploads, remote.uploadDirectories);
|
|
91
|
+
const version = await Promise.race([setup, signals.received.then(() => "")]);
|
|
89
92
|
if (signals.requested !== undefined)
|
|
90
93
|
return await endRun(launch, runId, {
|
|
91
94
|
exitCode: signals.exitCode,
|
|
92
95
|
error: "stopped before the run started",
|
|
93
96
|
});
|
|
97
|
+
say(`Installed humanish ${version} in the runner and uploaded the study; starting its run.`);
|
|
94
98
|
await startRemoteRun(runner, {
|
|
95
99
|
study: launch.study,
|
|
96
100
|
runId,
|
|
97
101
|
ownerKey,
|
|
98
|
-
env:
|
|
102
|
+
env: keys.env,
|
|
99
103
|
});
|
|
100
104
|
return await followRemoteRun({ launch, runner, paths, runId, signals, proxyUrl: proxy.url });
|
|
101
105
|
}
|
|
@@ -108,11 +112,11 @@ export async function launchRemoteRun(launch) {
|
|
|
108
112
|
finally {
|
|
109
113
|
signals.dispose();
|
|
110
114
|
await proxy?.close();
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// A pack still going removes its directory when it ends; this process waits for that.
|
|
116
|
+
await tarball?.catch(() => undefined);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
|
-
async function acquireRunner(launch, paths) {
|
|
119
|
+
async function acquireRunner(launch, paths, network) {
|
|
116
120
|
const { provider, runnerMs } = launch.remote;
|
|
117
121
|
const module = provider === "modal" ? await loadModalShellModule(launch.env) : await loadE2BBaseModule();
|
|
118
122
|
// A Modal module takes its connection from launch.env and ignores these two.
|
|
@@ -125,7 +129,7 @@ async function acquireRunner(launch, paths) {
|
|
|
125
129
|
...account,
|
|
126
130
|
timeoutMs: runnerMs,
|
|
127
131
|
metadata: { participantId: "runner" },
|
|
128
|
-
network
|
|
132
|
+
network,
|
|
129
133
|
},
|
|
130
134
|
receipt: { root: paths, participantId: "runner" },
|
|
131
135
|
});
|
|
@@ -145,11 +149,25 @@ export async function followRemoteRun(args) {
|
|
|
145
149
|
let syncAt = Date.now() + SYNC_EVERY_MS;
|
|
146
150
|
let state;
|
|
147
151
|
let stopAt;
|
|
148
|
-
let
|
|
152
|
+
let answeredAt = Date.now();
|
|
153
|
+
// The run was started just before, so it is taken as alive until a read says otherwise.
|
|
154
|
+
let alive = true;
|
|
149
155
|
const startBy = Date.now() + START_DEADLINE_MS;
|
|
150
156
|
for (;;) {
|
|
151
|
-
const
|
|
152
|
-
|
|
157
|
+
const read = await readRun(runner, runId, { state, alive }, signals);
|
|
158
|
+
// A signal ends the read, and is handled below without one.
|
|
159
|
+
const run = read === "signal" ? undefined : read;
|
|
160
|
+
if (read === undefined) {
|
|
161
|
+
if (Date.now() - answeredAt < UNANSWERED_LIMIT_MS)
|
|
162
|
+
await Promise.race([
|
|
163
|
+
new Promise((resolve) => setTimeout(resolve, RETRY_MS)),
|
|
164
|
+
signals.requested === undefined ? signals.received : new Promise(() => undefined),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
else if (run !== undefined) {
|
|
168
|
+
answeredAt = Date.now();
|
|
169
|
+
alive = run.alive;
|
|
170
|
+
}
|
|
153
171
|
if (run?.state !== undefined && state === undefined) {
|
|
154
172
|
say(`Run ${runId} is running. Watch it live at ${args.proxyUrl}`);
|
|
155
173
|
say("Closing this terminal leaves the study running; Ctrl-C stops it.");
|
|
@@ -195,8 +213,8 @@ export async function followRemoteRun(args) {
|
|
|
195
213
|
});
|
|
196
214
|
await stopRemoteRun(runner).catch(() => undefined);
|
|
197
215
|
}
|
|
198
|
-
const ended =
|
|
199
|
-
? `the runner sandbox stopped answering for ${
|
|
216
|
+
const ended = Date.now() - answeredAt >= UNANSWERED_LIMIT_MS
|
|
217
|
+
? `the runner sandbox stopped answering for ${UNANSWERED_LIMIT_MS / 1000} s`
|
|
200
218
|
: run !== undefined && state !== undefined && !run.alive
|
|
201
219
|
? stopAt === undefined
|
|
202
220
|
? `the run ended without a result: ${run.errTail || "no output"}`
|
|
@@ -222,9 +240,18 @@ export async function followRemoteRun(args) {
|
|
|
222
240
|
// Best effort: what this misses, the copy at the end takes.
|
|
223
241
|
await copySettledEvidence(runner, paths, runId, copied).catch(() => undefined);
|
|
224
242
|
}
|
|
225
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
226
243
|
}
|
|
227
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* One read of the runner's run, waiting there for a change. The first signal ends the wait here,
|
|
247
|
+
* and the read left in the runner ends on its own. Undefined when the runner did not answer.
|
|
248
|
+
*/
|
|
249
|
+
async function readRun(runner, runId, seen, signals) {
|
|
250
|
+
const read = waitForRemoteRun(runner, runId, seen, WAIT_MS).catch(() => undefined);
|
|
251
|
+
if (signals.requested !== undefined)
|
|
252
|
+
return await read;
|
|
253
|
+
return await Promise.race([read, signals.received.then(() => "signal")]);
|
|
254
|
+
}
|
|
228
255
|
/**
|
|
229
256
|
* Copy every file of the runner's run directory into this machine's run directory, each checked
|
|
230
257
|
* against the digest the runner listed. The sandbox journal keeps this machine's lines (the
|
|
@@ -233,24 +260,24 @@ export async function followRemoteRun(args) {
|
|
|
233
260
|
async function copyRunBack(runner, paths, runId) {
|
|
234
261
|
const files = await listRemoteRun(runner, runId);
|
|
235
262
|
let alreadyHere = 0;
|
|
263
|
+
const wanted = [];
|
|
236
264
|
for (const file of files) {
|
|
237
265
|
if (file.path === "owner-key")
|
|
238
266
|
continue;
|
|
239
267
|
// Already here with the same bytes, copied while the run went on or by an earlier attempt.
|
|
240
268
|
if (file.path !== SANDBOX_RECEIPTS_ARTIFACT &&
|
|
241
|
-
(await localDigest(paths, file.path)) === file.sha256)
|
|
269
|
+
(await localDigest(paths, file.path)) === file.sha256)
|
|
242
270
|
alreadyHere += 1;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const bytes = await readRemoteFile(runner, runId, file);
|
|
246
|
-
if (file.path === SANDBOX_RECEIPTS_ARTIFACT) {
|
|
247
|
-
const local = await readContainedRegularFile(paths, file.path, 16 * 1024 * 1024);
|
|
248
|
-
const mine = local.status === "read" ? Buffer.from(local.bytes).toString("utf8") : "";
|
|
249
|
-
await writeContainedOutputFile(paths, file.path, joinJournals(mine, Buffer.from(bytes).toString("utf8")), "utf8");
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
await writeContainedOutputFile(paths, file.path, bytes);
|
|
271
|
+
else
|
|
272
|
+
wanted.push(file);
|
|
253
273
|
}
|
|
274
|
+
await readRemoteFiles(runner, runId, wanted, async (file, bytes) => {
|
|
275
|
+
if (file.path !== SANDBOX_RECEIPTS_ARTIFACT)
|
|
276
|
+
return await writeContainedOutputFile(paths, file.path, bytes);
|
|
277
|
+
const local = await readContainedRegularFile(paths, file.path, 16 * 1024 * 1024);
|
|
278
|
+
const mine = local.status === "read" ? Buffer.from(local.bytes).toString("utf8") : "";
|
|
279
|
+
await writeContainedOutputFile(paths, file.path, joinJournals(mine, Buffer.from(bytes).toString("utf8")), "utf8");
|
|
280
|
+
});
|
|
254
281
|
await pointLatestRunAt(paths, runId);
|
|
255
282
|
return { files: files.length, alreadyHere };
|
|
256
283
|
}
|
|
@@ -350,13 +377,30 @@ export function watchSignals() {
|
|
|
350
377
|
};
|
|
351
378
|
}
|
|
352
379
|
/**
|
|
353
|
-
* The
|
|
354
|
-
*
|
|
380
|
+
* The keys the runner's run receives: each of the plan's names that is set here, and of names the
|
|
381
|
+
* run reads only the first of, only the first one set. On E2B, each key that travels as a header
|
|
382
|
+
* to a fixed host stays in an E2B header transform, and the run gets a placeholder and the system
|
|
383
|
+
* CA bundle that holds E2B's proxy CA, which Node does not read by default. Modal has no such
|
|
384
|
+
* transform, so a Modal runner receives every key as an environment variable.
|
|
355
385
|
*/
|
|
356
|
-
export function
|
|
386
|
+
export function runnerKeys(remote, env) {
|
|
357
387
|
const set = (name) => (env[name]?.trim() ?? "") !== "";
|
|
358
388
|
const unread = new Set(remote.alternatives.flatMap((names) => names.filter(set).slice(1)));
|
|
359
|
-
|
|
389
|
+
const forwarded = Object.fromEntries(remote.envNames.flatMap((name) => (set(name) && !unread.has(name) ? [[name, env[name]]] : [])));
|
|
390
|
+
const routing = { allowPublicTraffic: false };
|
|
391
|
+
if (remote.provider === "modal")
|
|
392
|
+
return { env: forwarded, network: routing, held: [], sent: Object.keys(forwarded).sort() };
|
|
393
|
+
const held = holdKeysInEgress(forwarded, { domain: forwarded.E2B_DOMAIN, existing: routing });
|
|
394
|
+
return {
|
|
395
|
+
env: held.held.length === 0
|
|
396
|
+
? held.env
|
|
397
|
+
: { ...held.env, NODE_EXTRA_CA_CERTS: E2B_SYSTEM_CA_BUNDLE },
|
|
398
|
+
network: held.network,
|
|
399
|
+
held: held.held,
|
|
400
|
+
sent: Object.keys(forwarded)
|
|
401
|
+
.filter((name) => !held.held.includes(name))
|
|
402
|
+
.sort(),
|
|
403
|
+
};
|
|
360
404
|
}
|
|
361
405
|
/** Whether this humanish runs from a built package, which a runner can install. */
|
|
362
406
|
export function runsFromPackage() {
|
|
@@ -368,15 +412,30 @@ export function runsFromPackage() {
|
|
|
368
412
|
* This machine's humanish as an npm tarball, so the runner runs exactly the same code. A run from
|
|
369
413
|
* source has no built package to pack.
|
|
370
414
|
*/
|
|
371
|
-
|
|
415
|
+
function packThisHumanish() {
|
|
372
416
|
const here = fileURLToPath(import.meta.url);
|
|
373
417
|
const root = path.resolve(path.dirname(here), "..", "..");
|
|
418
|
+
// Thrown here, not from the promise, so it refuses the run before any sandbox exists.
|
|
374
419
|
if (!runsFromPackage())
|
|
375
420
|
throw new Error("A remote run installs this humanish in the runner, and a run from source has no built package. Run `pnpm build`, then `node dist/cli.js`, or use an installed humanish.");
|
|
421
|
+
return pack(root);
|
|
422
|
+
}
|
|
423
|
+
async function pack(root) {
|
|
376
424
|
const dir = await mkdtemp(path.join(tmpdir(), "humanish-pack-"));
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
425
|
+
try {
|
|
426
|
+
const { stdout } = await promisify(execFile)("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", dir, root], { maxBuffer: 16 * 1024 * 1024 });
|
|
427
|
+
const [packed] = JSON.parse(stdout);
|
|
428
|
+
return await readFile(path.join(dir, packed.filename));
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
await rm(dir, { recursive: true, force: true });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** The npm specs this humanish's package.json depends on, as name@range. */
|
|
435
|
+
async function ownDependencies() {
|
|
436
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
437
|
+
const manifest = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
438
|
+
return Object.entries(manifest.dependencies ?? {}).map(([name, range]) => `${name}@${range}`);
|
|
380
439
|
}
|
|
381
440
|
/** The provider SDK the runner's participants need, at the version this machine has. */
|
|
382
441
|
async function peerSpec(provider) {
|
package/dist/remote/runner.d.ts
CHANGED
|
@@ -13,16 +13,36 @@ export interface Runner {
|
|
|
13
13
|
readonly shell: Shell;
|
|
14
14
|
}
|
|
15
15
|
export declare function runnerOf(sandbox: E2BDesktopSandbox): Runner;
|
|
16
|
+
/** A study file read here, for the same place in the runner's project. */
|
|
17
|
+
export interface ProjectFile {
|
|
18
|
+
/** Relative to the project, with `/` separators. */
|
|
19
|
+
path: string;
|
|
20
|
+
bytes: Uint8Array;
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Read each project-relative file in `files`, and every file under each directory in `directories`
|
|
24
|
+
* that exists here, for upload to the runner. Read before any sandbox exists, so a refusal costs
|
|
25
|
+
* nothing.
|
|
19
26
|
*/
|
|
20
|
-
export declare function
|
|
27
|
+
export declare function readProject(cwd: string, files: readonly string[], directories: readonly string[]): Promise<ProjectFile[]>;
|
|
28
|
+
/** What a runner is set up with. */
|
|
29
|
+
export interface RunnerSetup {
|
|
30
|
+
/** This machine's humanish as an npm tarball; it may still be packing when setup starts. */
|
|
31
|
+
tarball: Promise<Uint8Array>;
|
|
32
|
+
/**
|
|
33
|
+
* The npm specs its package depends on, from its own package.json, and the provider SDK its
|
|
34
|
+
* participants need, as name@version.
|
|
35
|
+
*/
|
|
36
|
+
dependencies: readonly string[];
|
|
37
|
+
project: readonly ProjectFile[];
|
|
38
|
+
}
|
|
21
39
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
40
|
+
* Install Node, this humanish's dependencies and this humanish in the runner, and upload the
|
|
41
|
+
* study's files. Node's download starts at once and the dependencies install as soon as it is
|
|
42
|
+
* there, while the tarball packs and uploads beside them; the tarball is then unpacked in place,
|
|
43
|
+
* so no install waits for the pack. Returns the installed version.
|
|
24
44
|
*/
|
|
25
|
-
export declare function
|
|
45
|
+
export declare function setUpRunner(runner: Runner, setup: RunnerSetup): Promise<string>;
|
|
26
46
|
/** What the runner's run is started with. */
|
|
27
47
|
export interface RemoteRunStart {
|
|
28
48
|
/** The study file, relative to the project. */
|
|
@@ -51,19 +71,28 @@ export interface RemoteRunState {
|
|
|
51
71
|
/** The last lines the run wrote to stderr. */
|
|
52
72
|
errTail: string;
|
|
53
73
|
}
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
/**
|
|
75
|
+
* How the runner's run is going. The runner checks its run every 0.2 s for up to `waitMs` and
|
|
76
|
+
* answers as soon as it differs from `seen`: a new state, a result, or its process ending. One
|
|
77
|
+
* command answers each wait, so the launcher learns of a change within a check of it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function waitForRemoteRun(runner: Runner, runId: string, seen: {
|
|
80
|
+
state: string | undefined;
|
|
81
|
+
alive: boolean;
|
|
82
|
+
}, waitMs: number): Promise<RemoteRunState>;
|
|
83
|
+
/** One file of the runner's run directory: its path in the directory, its size and its sha256. */
|
|
56
84
|
export interface RemoteFile {
|
|
57
85
|
path: string;
|
|
86
|
+
size: number;
|
|
58
87
|
sha256: string;
|
|
59
88
|
}
|
|
60
89
|
/**
|
|
61
|
-
* The regular files of the runner's run directory
|
|
62
|
-
* a link, which a run never writes.
|
|
90
|
+
* The regular files of the runner's run directory, with their sizes and digests. Refuses a
|
|
91
|
+
* directory that holds a link, which a run never writes.
|
|
63
92
|
*/
|
|
64
93
|
export declare function listRemoteRun(runner: Runner, runId: string): Promise<RemoteFile[]>;
|
|
65
|
-
/** sha256sum lines, `<hex> ./<path>`,
|
|
66
|
-
export declare function parseDigests(output: string): RemoteFile[];
|
|
94
|
+
/** sha256sum lines, `<hex> ./<path>`, with paths relative to the directory. */
|
|
95
|
+
export declare function parseDigests(output: string): Omit<RemoteFile, "size">[];
|
|
67
96
|
/**
|
|
68
97
|
* The screenshots and recordings of the runner's run that have not changed for `settledSeconds`,
|
|
69
98
|
* with their sizes. A run writes each of them once, so one that has settled can be copied while
|
|
@@ -80,8 +109,12 @@ export declare function parseSizes(output: string): {
|
|
|
80
109
|
}[];
|
|
81
110
|
/** The bytes of one file of the runner's run directory, as they are now. */
|
|
82
111
|
export declare function readRemoteBytes(runner: Runner, runId: string, file: string): Promise<Uint8Array>;
|
|
83
|
-
/**
|
|
84
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Read `files` of the runner's run directory and hand each to `take`, checked against its listed
|
|
114
|
+
* digest. The files go in batches, each joined into one runner file and read back in one request,
|
|
115
|
+
* so a batch costs two round trips whatever its number of files.
|
|
116
|
+
*/
|
|
117
|
+
export declare function readRemoteFiles(runner: Runner, runId: string, files: readonly RemoteFile[], take: (file: RemoteFile, bytes: Uint8Array) => Promise<void>): Promise<void>;
|
|
85
118
|
/**
|
|
86
119
|
* Ask the runner's run to stop as a signal would, so it reclaims its participants and ends. The
|
|
87
120
|
* `[h]` keeps the pattern from matching the shell that runs it.
|