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.
Files changed (52) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/analysis/admission.d.ts +8 -3
  3. package/dist/analysis/admission.js +7 -3
  4. package/dist/analysis/automatic-config.js +2 -1
  5. package/dist/analysis/automatic.js +2 -2
  6. package/dist/analysis/execute.d.ts +7 -10
  7. package/dist/analysis/execute.js +79 -49
  8. package/dist/analysis/models.d.ts +2 -0
  9. package/dist/analysis/models.js +15 -0
  10. package/dist/analysis/openai-wire.d.ts +1 -1
  11. package/dist/analysis/openai-wire.js +36 -26
  12. package/dist/analysis/output-limit.d.ts +12 -0
  13. package/dist/analysis/output-limit.js +34 -0
  14. package/dist/analysis/output-retry.d.ts +20 -0
  15. package/dist/analysis/output-retry.js +63 -0
  16. package/dist/analysis/provider.d.ts +1 -1
  17. package/dist/analysis/provider.js +3 -3
  18. package/dist/analysis/responses.js +1 -1
  19. package/dist/analysis/service.js +2 -2
  20. package/dist/analysis/validation.js +1 -1
  21. package/dist/cli/commands/analyze.js +1 -1
  22. package/dist/cli/commands/study-remote.d.ts +3 -1
  23. package/dist/cli/commands/study-remote.js +18 -3
  24. package/dist/cli/io.js +2 -0
  25. package/dist/cli/run-findings.js +10 -2
  26. package/dist/keys/key-resolution.js +2 -2
  27. package/dist/observer-app.html +1 -1
  28. package/dist/remote/launch.d.ts +21 -3
  29. package/dist/remote/launch.js +111 -52
  30. package/dist/remote/runner.d.ts +47 -14
  31. package/dist/remote/runner.js +199 -64
  32. package/dist/routes/computer-use/e2b-desktop/desktop.js +47 -7
  33. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +4 -2
  34. package/dist/routes/computer-use/e2b-desktop/prepare.js +4 -2
  35. package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -6
  36. package/dist/routes/computer-use/e2b-desktop/start.js +72 -34
  37. package/dist/routes/terminal/credentials.js +3 -2
  38. package/dist/routes/terminal/live-sandbox.js +2 -2
  39. package/dist/routes/terminal/runtime-auth.d.ts +0 -12
  40. package/dist/routes/terminal/runtime-auth.js +0 -23
  41. package/dist/substrates/e2b/cdp-probe.d.ts +16 -1
  42. package/dist/substrates/e2b/cdp-probe.js +67 -5
  43. package/dist/substrates/e2b/desktop-browser.js +4 -2
  44. package/dist/substrates/e2b/desktop-cdp.d.ts +9 -2
  45. package/dist/substrates/e2b/desktop-cdp.js +26 -4
  46. package/dist/substrates/e2b/egress-keys.d.ts +22 -0
  47. package/dist/substrates/e2b/egress-keys.js +59 -0
  48. package/docs/contracts/schemas.md +1 -1
  49. package/docs/contracts/study-analysis.md +75 -9
  50. package/docs/product/automatic-analysis.md +9 -6
  51. package/package.json +5 -5
  52. package/skills/humanish/SKILL.md +2 -1
@@ -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 { installHumanish, listRemoteRun, listSettledEvidence, readRemoteBytes, readRemoteFile, readRemoteRun, RUNNER_OBSERVER_PORT, runnerOf, startRemoteRun, stopRemoteRun, uploadProject, } from "./runner.js";
22
- /** How often the launcher reads the runner's run while it follows it. */
23
- const POLL_MS = 5_000;
24
- /** Reads in a row the runner may leave unanswered before the launcher gives it up. */
25
- const UNANSWERED_LIMIT = 24;
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
- const tarball = await packThisHumanish();
58
- tarballDir = tarball.dir;
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 runner = runnerOf((await acquireRunner(launch, paths)).sandbox);
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
- // Installing can take minutes; a signal stops waiting for it, and the reclaim below stops the
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: forwardedEnv(remote, launch.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
- if (tarballDir !== undefined)
112
- await rm(tarballDir, { recursive: true, force: true });
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: { allowPublicTraffic: false },
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 unanswered = 0;
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 run = await readRemoteRun(runner, runId).catch(() => undefined);
152
- unanswered = run === undefined ? unanswered + 1 : 0;
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 = unanswered >= UNANSWERED_LIMIT
199
- ? `the runner sandbox stopped answering for ${(UNANSWERED_LIMIT * POLL_MS) / 1000} s`
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
- continue;
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 names and values the runner's run receives: each of the plan's names that is set here, and of
354
- * names the run reads only the first of, only the first one set.
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 forwardedEnv(remote, env) {
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
- return Object.fromEntries(remote.envNames.flatMap((name) => (set(name) && !unread.has(name) ? [[name, env[name]]] : [])));
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
- async function packThisHumanish() {
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
- const { stdout } = await promisify(execFile)("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", dir, root], { maxBuffer: 16 * 1024 * 1024 });
378
- const [packed] = JSON.parse(stdout);
379
- return { bytes: await readFile(path.join(dir, packed.filename)), dir };
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) {
@@ -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
- * Install Node and the humanish package `tarball` (this machine's own humanish, packed), with the
18
- * provider SDKs its participants need at `peers` (name@version). Returns the installed version.
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 installHumanish(runner: Runner, tarball: Uint8Array, peers: readonly string[]): Promise<string>;
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
- * Upload each project-relative file in `files`, and every file under each directory in
23
- * `directories` that exists here, to the same place in the runner's project.
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 uploadProject(runner: Runner, cwd: string, files: readonly string[], directories: readonly string[]): Promise<number>;
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
- export declare function readRemoteRun(runner: Runner, runId: string): Promise<RemoteRunState>;
55
- /** One file of the runner's run directory: its path in the directory and its sha256. */
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 and their digests. Refuses a directory that holds
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>`, as RemoteFiles with paths relative to the directory. */
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
- /** The bytes of one file of the runner's run directory, checked against its listed digest. */
84
- export declare function readRemoteFile(runner: Runner, runId: string, file: RemoteFile): Promise<Uint8Array>;
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.