humanish 0.136.1 → 0.138.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 (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/actors/computer-use/actor.d.ts +2 -0
  3. package/dist/actors/computer-use/actor.js +1 -0
  4. package/dist/actors/computer-use/loop/observation.js +2 -0
  5. package/dist/actors/computer-use/loop/session.d.ts +3 -2
  6. package/dist/actors/computer-use/loop/session.js +8 -12
  7. package/dist/actors/computer-use/loop/types.d.ts +14 -1
  8. package/dist/cli/commands/runs.js +2 -2
  9. package/dist/cli/doctor.js +24 -0
  10. package/dist/evidence/browser-network.d.ts +108 -0
  11. package/dist/evidence/browser-network.js +516 -0
  12. package/dist/evidence/trace-page.d.ts +31 -1
  13. package/dist/evidence/trace-page.js +68 -2
  14. package/dist/keys/key-resolution.js +53 -2
  15. package/dist/observer-app.html +2 -2
  16. package/dist/routes/computer-use/bundle.js +1 -0
  17. package/dist/routes/computer-use/e2b-desktop/desktop.js +6 -0
  18. package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +12 -1
  19. package/dist/routes/computer-use/e2b-desktop/fidelity.js +7 -2
  20. package/dist/routes/computer-use/e2b-desktop/network.d.ts +52 -0
  21. package/dist/routes/computer-use/e2b-desktop/network.js +153 -0
  22. package/dist/routes/computer-use/e2b-desktop/start.js +18 -1
  23. package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -0
  24. package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
  25. package/dist/routes/computer-use/e2b-desktop/teardown.js +6 -3
  26. package/dist/routes/computer-use/fanout-records.js +10 -0
  27. package/dist/routes/computer-use/participant-desktop.d.ts +7 -2
  28. package/dist/routes/computer-use/participant-model.js +2 -1
  29. package/dist/routes/computer-use/plan.js +2 -1
  30. package/dist/routes/computer-use/single-bundle.d.ts +3 -0
  31. package/dist/routes/computer-use/single-bundle.js +4 -0
  32. package/dist/routes/computer-use/types.d.ts +7 -0
  33. package/dist/routes/scripted/plan.js +2 -2
  34. package/dist/routes/shared-world/plan.js +3 -2
  35. package/dist/routes/terminal/live-sandbox.d.ts +2 -0
  36. package/dist/routes/terminal/live-sandbox.js +8 -3
  37. package/dist/routes/terminal/plan.js +51 -8
  38. package/dist/routes/terminal/session.js +8 -1
  39. package/dist/run/bundle.d.ts +2 -1
  40. package/dist/run/cost-summary.js +9 -4
  41. package/dist/run/live-items.d.ts +20 -2
  42. package/dist/run/live-items.js +23 -14
  43. package/dist/run/pricing.d.ts +11 -0
  44. package/dist/run/pricing.js +18 -5
  45. package/dist/run/reclaim-outcomes.d.ts +4 -1
  46. package/dist/run/reclaim-outcomes.js +8 -6
  47. package/dist/run/reclaim.d.ts +4 -1
  48. package/dist/run/reclaim.js +81 -34
  49. package/dist/run/sandbox-creates.d.ts +7 -4
  50. package/dist/run/sandbox-creates.js +17 -9
  51. package/dist/run/sandbox-receipts.d.ts +13 -5
  52. package/dist/run/sandbox-receipts.js +18 -6
  53. package/dist/run/streams.d.ts +7 -0
  54. package/dist/study/composition-rules.js +11 -10
  55. package/dist/study/config.js +4 -1
  56. package/dist/study/doctor.js +1 -1
  57. package/dist/study/keys.d.ts +3 -0
  58. package/dist/study/keys.js +2 -0
  59. package/dist/study/parse/comms.js +1 -20
  60. package/dist/study/parse/execution.js +35 -3
  61. package/dist/study/parse/values.d.ts +5 -0
  62. package/dist/study/parse/values.js +22 -0
  63. package/dist/study/plan-base.js +6 -1
  64. package/dist/study/plan-types.d.ts +5 -2
  65. package/dist/study/requirements.js +7 -2
  66. package/dist/study/routing.d.ts +1 -1
  67. package/dist/study/routing.js +1 -1
  68. package/dist/study/study-fields.d.ts +7 -1
  69. package/dist/study/study-fields.js +10 -0
  70. package/dist/study/types.d.ts +12 -1
  71. package/dist/study/validation.d.ts +8 -0
  72. package/dist/study/validation.js +31 -2
  73. package/dist/substrates/detached.d.ts +5 -0
  74. package/dist/substrates/detached.js +9 -0
  75. package/dist/substrates/e2b/cdp-network-capture.d.ts +23 -0
  76. package/dist/substrates/e2b/cdp-network-capture.js +194 -0
  77. package/dist/substrates/e2b/cdp-probe.d.ts +20 -5
  78. package/dist/substrates/e2b/cdp-probe.js +98 -34
  79. package/dist/substrates/e2b/desktop-cdp.d.ts +32 -2
  80. package/dist/substrates/e2b/desktop-cdp.js +87 -5
  81. package/dist/substrates/e2b/desktop-resources.d.ts +4 -2
  82. package/dist/substrates/e2b/desktop-resources.js +2 -2
  83. package/dist/substrates/e2b/peer-install.js +1 -1
  84. package/dist/substrates/e2b/sandbox.d.ts +3 -0
  85. package/dist/substrates/e2b/sandbox.js +5 -4
  86. package/dist/substrates/modal/image.d.ts +9 -0
  87. package/dist/substrates/modal/image.js +36 -0
  88. package/dist/substrates/modal/sandbox.d.ts +26 -0
  89. package/dist/substrates/modal/sandbox.js +367 -0
  90. package/dist/substrates/modal/sdk.d.ts +112 -0
  91. package/dist/substrates/modal/sdk.js +61 -0
  92. package/dist/verify/network.d.ts +10 -0
  93. package/dist/verify/network.js +162 -0
  94. package/dist/verify/pages.js +1 -16
  95. package/dist/verify/verify.js +8 -2
  96. package/docs/architecture/terminal-product-route.md +1 -1
  97. package/docs/contracts/run-bundle.md +90 -3
  98. package/docs/contracts/schemas.md +6 -5
  99. package/package.json +8 -2
@@ -0,0 +1,26 @@
1
+ import type { E2BDesktopModule } from "../e2b/sdk.js";
2
+ import type { E2BAccount } from "../e2b/connection.js";
3
+ import { type ModalClient, type ModalConnection, type ModalImage, type ModalModule } from "./sdk.js";
4
+ /** The size and image of the sandboxes one module creates. */
5
+ export interface ModalSandboxProfile {
6
+ image: (client: ModalClient) => ModalImage;
7
+ /** Physical cores; Modal counts one core as two vCPU. */
8
+ cpu: number;
9
+ memoryMiB: number;
10
+ }
11
+ /**
12
+ * A sandbox port over one Modal account. `create` maps E2B's create options onto Modal's; `kill`,
13
+ * `getInfo` and `list` keep the E2B SDK's contracts, which release and reclaim read.
14
+ */
15
+ export declare function modalSandboxModule(client: ModalClient, connection: ModalConnection, profile: ModalSandboxProfile): E2BDesktopModule;
16
+ /**
17
+ * The calls on sandboxes that already exist, with the E2B SDK's contracts: `kill` is true when it
18
+ * ended a running sandbox and false when the sandbox was gone, `getInfo` throws an error named
19
+ * SandboxNotFoundError for a gone id, and `list` filters by every given tag. None of them creates
20
+ * anything, so `reclaim --check` stays read-only.
21
+ */
22
+ export declare function modalSandboxes(client: ModalClient, connection: ModalConnection): Required<E2BAccount>;
23
+ /** The Modal account `env` names, for release by id and reclaim. Throws when it names none. */
24
+ export declare function modalAccount(env: Readonly<Record<string, string | undefined>>, load?: () => Promise<ModalModule>): Promise<Required<E2BAccount>>;
25
+ /** A sandbox port that creates the terminal route's shell sandboxes on the Modal account `env` names. */
26
+ export declare function loadModalShellModule(env: Readonly<Record<string, string | undefined>>, load?: () => Promise<ModalModule>): Promise<E2BDesktopModule>;
@@ -0,0 +1,367 @@
1
+ import { MODAL_SANDBOX_USER, modalShellImage } from "./image.js";
2
+ import { isModalNotFoundError, loadModalModule, modalConnection, MODAL_TOKEN_ID_ENV, MODAL_TOKEN_SECRET_ENV, withoutTokens, } from "./sdk.js";
3
+ /** The Modal App every humanish sandbox is created in, inside the connection's environment. */
4
+ const MODAL_APP_NAME = "humanish";
5
+ /** How long a release waits for Modal to report a terminated sandbox finished; about 130 ms seen. */
6
+ const TERMINATE_WAIT_MS = 30_000;
7
+ /** Modal takes sandbox and command timeouts in whole seconds; a shorter one is rounded up. */
8
+ const wholeSeconds = (ms) => Math.ceil(ms / 1000) * 1000;
9
+ /** `fn`, with any error it throws stripped of the connection's tokens. */
10
+ function hidingTokens(connection, fn) {
11
+ return async (...args) => {
12
+ try {
13
+ return await fn(...args);
14
+ }
15
+ catch (error) {
16
+ throw withoutTokens(error, connection);
17
+ }
18
+ };
19
+ }
20
+ /** E2B's commands.run default, kept so a caller that sets no timeout gets the same bound. */
21
+ const DEFAULT_COMMAND_TIMEOUT_MS = 60_000;
22
+ /** Modal's own default sandbox lifetime is 5 minutes, as E2B's is. */
23
+ const DEFAULT_SANDBOX_TIMEOUT_MS = 5 * 60_000;
24
+ /** A Modal sandbox was asked for something only an E2B sandbox has. */
25
+ class ModalUnsupportedError extends Error {
26
+ constructor(what) {
27
+ super(`${what} is not available on Modal sandboxes in this version of humanish.`);
28
+ this.name = "ModalUnsupportedError";
29
+ }
30
+ }
31
+ /**
32
+ * A sandbox port over one Modal account. `create` maps E2B's create options onto Modal's; `kill`,
33
+ * `getInfo` and `list` keep the E2B SDK's contracts, which release and reclaim read.
34
+ */
35
+ export function modalSandboxModule(client, connection, profile) {
36
+ const humanishApp = appLookup(client, connection, true);
37
+ // The E2B SDK's second overload names a template first; Modal sandboxes have no templates.
38
+ async function create(templateOrOptions) {
39
+ if (typeof templateOrOptions === "string")
40
+ throw new ModalUnsupportedError("A desktop template");
41
+ const options = templateOrOptions;
42
+ const params = {
43
+ cpu: profile.cpu,
44
+ memoryMiB: profile.memoryMiB,
45
+ timeoutMs: wholeSeconds(options.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS),
46
+ runtime: "vm",
47
+ workdir: MODAL_SANDBOX_USER.home,
48
+ ...(options.envs === undefined ? {} : { env: { ...options.envs } }),
49
+ ...(options.metadata === undefined ? {} : { tags: { ...options.metadata } }),
50
+ ...modalNetwork(options.network),
51
+ };
52
+ const sandbox = await client.sandboxes.create(await humanishApp(), profile.image(client), params);
53
+ return modalSandboxHandle(sandbox, profile, connection);
54
+ }
55
+ return {
56
+ Sandbox: {
57
+ create: hidingTokens(connection, create),
58
+ ...modalSandboxes(client, connection),
59
+ },
60
+ };
61
+ }
62
+ /** The humanish App in the connection's environment; only a create may make it. */
63
+ function appLookup(client, connection, createIfMissing) {
64
+ let app;
65
+ return () => (app ??= client.apps.fromName(MODAL_APP_NAME, {
66
+ ...(connection.environment === undefined ? {} : { environment: connection.environment }),
67
+ createIfMissing,
68
+ }));
69
+ }
70
+ /** A Modal request's default deadline, as E2B's SDK sets its own. */
71
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
72
+ /** How often a release asks whether a terminated sandbox has finished. */
73
+ const TERMINATE_POLL_MS = 500;
74
+ /**
75
+ * `request`, or a rejection once `ms` pass. The SDK takes no deadline of its own, so a stalled
76
+ * RPC would otherwise hold a release or reclaim with no end.
77
+ */
78
+ async function within(request, ms, what) {
79
+ let timer;
80
+ try {
81
+ return await Promise.race([
82
+ request,
83
+ new Promise((_resolve, reject) => {
84
+ timer = setTimeout(() => reject(new Error(`Modal did not answer ${what} within ${ms} ms.`)), ms);
85
+ }),
86
+ ]);
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+ /**
93
+ * The calls on sandboxes that already exist, with the E2B SDK's contracts: `kill` is true when it
94
+ * ended a running sandbox and false when the sandbox was gone, `getInfo` throws an error named
95
+ * SandboxNotFoundError for a gone id, and `list` filters by every given tag. None of them creates
96
+ * anything, so `reclaim --check` stays read-only.
97
+ */
98
+ export function modalSandboxes(client, connection) {
99
+ const existingApp = appLookup(client, connection, false);
100
+ const deadline = (options) => options?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
101
+ /** The sandbox's exit code, null while it runs, or undefined when Modal has no such id. */
102
+ async function exitOf(sandboxId, ms) {
103
+ try {
104
+ const sandbox = await within(client.sandboxes.fromId(sandboxId), ms, "a sandbox lookup");
105
+ return await within(sandbox.poll(), ms, "a sandbox poll");
106
+ }
107
+ catch (error) {
108
+ if (isModalNotFoundError(error))
109
+ return undefined;
110
+ throw error;
111
+ }
112
+ }
113
+ async function kill(sandboxId, options) {
114
+ const ms = deadline(options);
115
+ if ((await exitOf(sandboxId, ms)) !== null)
116
+ return false;
117
+ const sandbox = await within(client.sandboxes.fromId(sandboxId), ms, "a sandbox lookup");
118
+ await within(sandbox.terminate(), ms, "a terminate");
119
+ // terminate returns before the sandbox stops. Polling, unlike the SDK's wait, ends when this
120
+ // loop does; a sandbox still running at the end leaves the kill unconfirmed.
121
+ for (const end = Date.now() + TERMINATE_WAIT_MS; Date.now() < end;) {
122
+ if ((await exitOf(sandboxId, ms)) !== null)
123
+ return true;
124
+ await new Promise((resolve) => setTimeout(resolve, TERMINATE_POLL_MS));
125
+ }
126
+ throw new Error(`Modal had not reported the sandbox finished ${TERMINATE_WAIT_MS / 1000} s after it was terminated.`);
127
+ }
128
+ async function getInfo(sandboxId, options) {
129
+ if ((await exitOf(sandboxId, deadline(options))) !== null)
130
+ throw sandboxNotFound(sandboxId);
131
+ return { sandboxId, state: "running" };
132
+ }
133
+ /** One page: Modal filters by tags server-side, and a run tags a few dozen sandboxes at most. */
134
+ async function listed(tags, ms) {
135
+ let app;
136
+ try {
137
+ app = await within(existingApp(), ms, "an app lookup");
138
+ }
139
+ catch (error) {
140
+ // No humanish App in this environment: no sandbox of any run is there.
141
+ if (isModalNotFoundError(error))
142
+ return [];
143
+ throw error;
144
+ }
145
+ const found = [];
146
+ const search = async () => {
147
+ for await (const sandbox of client.sandboxes.list({
148
+ appId: app.appId,
149
+ ...(tags === undefined ? {} : { tags }),
150
+ ...(connection.environment === undefined ? {} : { environment: connection.environment }),
151
+ }))
152
+ found.push({
153
+ sandboxId: sandbox.sandboxId,
154
+ metadata: await sandbox.getTags(),
155
+ state: "running",
156
+ });
157
+ };
158
+ await within(search(), ms, "a sandbox list");
159
+ return found;
160
+ }
161
+ return {
162
+ kill: hidingTokens(connection, kill),
163
+ getInfo: hidingTokens(connection, getInfo),
164
+ list: (options) => {
165
+ let read = false;
166
+ return {
167
+ get hasNext() {
168
+ return !read;
169
+ },
170
+ nextItems: hidingTokens(connection, () => {
171
+ read = true;
172
+ return listed(options?.query?.metadata, deadline(options));
173
+ }),
174
+ };
175
+ },
176
+ };
177
+ }
178
+ function sandboxNotFound(sandboxId) {
179
+ const error = new Error(`Modal has no running sandbox ${sandboxId}.`);
180
+ error.name = "SandboxNotFoundError";
181
+ return error;
182
+ }
183
+ const CIDR = /^[0-9a-f:.]+\/\d{1,3}$/i;
184
+ /**
185
+ * E2B's egress shapes that Modal can express: unrestricted, or deny-all with an allowlist of hosts
186
+ * and CIDRs. Header transforms and other deny lists have no Modal form, so plan validation refuses
187
+ * them first and this throws if one gets through.
188
+ */
189
+ function modalNetwork(network) {
190
+ if (network === undefined)
191
+ return {};
192
+ if (network.rules !== undefined && Object.keys(network.rules).length > 0)
193
+ throw new ModalUnsupportedError("An outbound header transform");
194
+ const allow = network.allowOut ?? [];
195
+ const deny = network.denyOut ?? [];
196
+ if (deny.length === 0 && allow.length === 0)
197
+ return {};
198
+ if (deny.length !== 1 || deny[0] !== "0.0.0.0/0")
199
+ throw new ModalUnsupportedError("An egress deny list other than 0.0.0.0/0");
200
+ if (allow.length === 0)
201
+ return { blockNetwork: true };
202
+ const cidrs = allow.filter((entry) => CIDR.test(entry));
203
+ const domains = allow.filter((entry) => !CIDR.test(entry));
204
+ return {
205
+ ...(cidrs.length === 0 ? {} : { outboundCidrAllowlist: cidrs }),
206
+ ...(domains.length === 0 ? {} : { outboundDomainAllowlist: domains }),
207
+ };
208
+ }
209
+ /** The argv that runs `command` in a login shell as the sandbox user, in their home. */
210
+ function asSandboxUser(command) {
211
+ const { uid } = MODAL_SANDBOX_USER;
212
+ return [
213
+ "setpriv",
214
+ `--reuid=${uid}`,
215
+ `--regid=${uid}`,
216
+ "--init-groups",
217
+ "--",
218
+ "bash",
219
+ "-lc",
220
+ command,
221
+ ];
222
+ }
223
+ const USER_ENV = {
224
+ HOME: MODAL_SANDBOX_USER.home,
225
+ USER: MODAL_SANDBOX_USER.name,
226
+ LOGNAME: MODAL_SANDBOX_USER.name,
227
+ };
228
+ /** The E2B SDK throws this shape on a non-zero exit; src/substrates/command-failure.ts reads it. */
229
+ class ModalCommandExitError extends Error {
230
+ exitCode;
231
+ stdout;
232
+ stderr;
233
+ constructor(exitCode, stdout, stderr) {
234
+ super(`Command exited with code ${exitCode}${stderr ? ` and error:\n${stderr}` : ""}`);
235
+ this.exitCode = exitCode;
236
+ this.stdout = stdout;
237
+ this.stderr = stderr;
238
+ this.name = "CommandExitError";
239
+ }
240
+ get error() {
241
+ return this.stderr;
242
+ }
243
+ }
244
+ async function collect(stream, onData) {
245
+ const decoder = new TextDecoder();
246
+ let text = "";
247
+ for await (const chunk of stream) {
248
+ const data = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
249
+ text += data;
250
+ if (onData !== undefined && data !== "")
251
+ await onData(data);
252
+ }
253
+ return text;
254
+ }
255
+ async function run(sandbox, command, options = {}) {
256
+ if (options.background === true || options.stdin === true)
257
+ throw new ModalUnsupportedError("A background command");
258
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
259
+ let stdout;
260
+ let stderr;
261
+ let exitCode;
262
+ try {
263
+ const child = await sandbox.exec(asSandboxUser(command), {
264
+ env: { ...USER_ENV, ...options.envs },
265
+ ...(options.cwd === undefined ? {} : { workdir: options.cwd }),
266
+ ...(timeoutMs > 0 ? { timeoutMs: wholeSeconds(timeoutMs) } : {}),
267
+ });
268
+ // E2B starts a command with no stdin; a Modal exec keeps the pipe open, and a reader such as
269
+ // codex exec would wait on it until its timeout.
270
+ await child.closeStdin();
271
+ [stdout, stderr, exitCode] = await Promise.all([
272
+ collect(child.stdout, options.onStdout),
273
+ collect(child.stderr, options.onStderr),
274
+ child.wait(),
275
+ ]);
276
+ }
277
+ catch (error) {
278
+ // Modal ends a command at its timeout and reports it as a deadline; E2B names it TimeoutError.
279
+ if (error instanceof Error && /deadline exceeded/i.test(error.message)) {
280
+ const timeout = new Error(`Command did not finish within ${timeoutMs} ms.`, { cause: error });
281
+ timeout.name = "TimeoutError";
282
+ throw timeout;
283
+ }
284
+ throw error;
285
+ }
286
+ if (exitCode !== 0)
287
+ throw new ModalCommandExitError(exitCode, stdout, stderr);
288
+ return { exitCode, stdout, stderr };
289
+ }
290
+ /** Relative paths are under the sandbox user's home, as on E2B. */
291
+ function sandboxPath(path) {
292
+ return path.startsWith("/") ? path : `${MODAL_SANDBOX_USER.home}/${path}`;
293
+ }
294
+ async function writeFile(sandbox, path, data) {
295
+ const target = sandboxPath(path);
296
+ const child = await sandbox.exec(asSandboxUser('mkdir -p -- "$(dirname -- "$0")" && cat > "$0"').concat(target), { mode: "binary", env: USER_ENV });
297
+ await child.stdin.writeBytes(typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data));
298
+ // closeStdin ends input at offset 0, which Modal ignores once bytes were written.
299
+ await child.stdin.close();
300
+ const [stderr, exitCode] = await Promise.all([child.stderr.readBytes(), child.wait()]);
301
+ if (exitCode !== 0)
302
+ throw new ModalCommandExitError(exitCode, "", new TextDecoder().decode(stderr));
303
+ return { path: target };
304
+ }
305
+ function modalSandboxHandle(sandbox, profile, connection) {
306
+ const desktopOnly = (what) => () => Promise.reject(new ModalUnsupportedError(what));
307
+ return {
308
+ sandboxId: sandbox.sandboxId,
309
+ // Modal reports no allocation for a sandbox; the create's request is what it bills.
310
+ getInfo: () => Promise.resolve({ cpuCount: profile.cpu * 2, memoryMB: profile.memoryMiB }),
311
+ commands: {
312
+ run: hidingTokens(connection, (command, options) => run(sandbox, command, options)),
313
+ },
314
+ files: {
315
+ write: hidingTokens(connection, (path, data) => writeFile(sandbox, path, data)),
316
+ getInfo: hidingTokens(connection, async (path) => {
317
+ const result = await run(sandbox, `stat -c %s -- '${sandboxPath(path).replaceAll("'", "'\\''")}'`);
318
+ return { size: Number(result.stdout?.trim()) };
319
+ }),
320
+ },
321
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
322
+ launch: desktopOnly("A desktop application launch"),
323
+ screenshot: desktopOnly("A desktop screenshot"),
324
+ leftClick: desktopOnly("Desktop input"),
325
+ rightClick: desktopOnly("Desktop input"),
326
+ middleClick: desktopOnly("Desktop input"),
327
+ doubleClick: desktopOnly("Desktop input"),
328
+ moveMouse: desktopOnly("Desktop input"),
329
+ scroll: desktopOnly("Desktop input"),
330
+ write: desktopOnly("Desktop input"),
331
+ press: desktopOnly("Desktop input"),
332
+ drag: desktopOnly("Desktop input"),
333
+ stream: {
334
+ getAuthKey: () => {
335
+ throw new ModalUnsupportedError("A desktop stream");
336
+ },
337
+ getUrl: () => {
338
+ throw new ModalUnsupportedError("A desktop stream");
339
+ },
340
+ start: desktopOnly("A desktop stream"),
341
+ },
342
+ };
343
+ }
344
+ /** The Modal account `env` names, for release by id and reclaim. Throws when it names none. */
345
+ export async function modalAccount(env, load = loadModalModule) {
346
+ const connection = requireModalConnection(env);
347
+ const { ModalClient: Client } = await load();
348
+ return modalSandboxes(new Client(connection), connection);
349
+ }
350
+ /** The terminal route's shell: Codex needs little CPU, so it runs smaller than E2B's 8 vCPU desktop. */
351
+ const MODAL_SHELL_PROFILE = {
352
+ image: modalShellImage,
353
+ cpu: 2,
354
+ memoryMiB: 4096,
355
+ };
356
+ /** A sandbox port that creates the terminal route's shell sandboxes on the Modal account `env` names. */
357
+ export async function loadModalShellModule(env, load = loadModalModule) {
358
+ const connection = requireModalConnection(env);
359
+ const { ModalClient: Client } = await load();
360
+ return modalSandboxModule(new Client(connection), connection, MODAL_SHELL_PROFILE);
361
+ }
362
+ function requireModalConnection(env) {
363
+ const connection = modalConnection(env);
364
+ if (connection === undefined)
365
+ throw new Error(`No Modal credentials: set ${MODAL_TOKEN_ID_ENV} and ${MODAL_TOKEN_SECRET_ENV}, or run \`modal token new\`.`);
366
+ return connection;
367
+ }
@@ -0,0 +1,112 @@
1
+ /** Which Modal workspace and environment a call reaches. */
2
+ export interface ModalConnection {
3
+ tokenId: string;
4
+ tokenSecret: string;
5
+ /** Modal's own default environment is used when unset. */
6
+ environment?: string;
7
+ }
8
+ /** The variables a Modal connection is read from, as Modal's own CLI and SDK name them. */
9
+ export declare const MODAL_TOKEN_ID_ENV = "MODAL_TOKEN_ID";
10
+ export declare const MODAL_TOKEN_SECRET_ENV = "MODAL_TOKEN_SECRET";
11
+ /**
12
+ * The connection an env names, or undefined when either token is missing. The SDK would otherwise
13
+ * fall back to process.env and ~/.modal.toml, which a library caller's env may not match, and a
14
+ * release could then reach another workspace than the create.
15
+ */
16
+ export declare function modalConnection(env: Readonly<Record<string, string | undefined>>): ModalConnection | undefined;
17
+ export interface ModalReadStream extends AsyncIterable<string | Uint8Array> {
18
+ readText(): Promise<string>;
19
+ readBytes(): Promise<Uint8Array>;
20
+ }
21
+ export interface ModalProcess {
22
+ stdout: ModalReadStream;
23
+ stderr: ModalReadStream;
24
+ stdin: {
25
+ writeText(text: string): Promise<void>;
26
+ writeBytes(bytes: Uint8Array): Promise<void>;
27
+ /** End of input after what was written. */
28
+ close(): Promise<void>;
29
+ };
30
+ /** End of input at offset 0: it ends input only before anything was written. */
31
+ closeStdin(): Promise<void>;
32
+ wait(): Promise<number>;
33
+ }
34
+ export interface ModalExecParams {
35
+ mode?: "text" | "binary";
36
+ stdout?: "pipe" | "ignore";
37
+ stderr?: "pipe" | "ignore";
38
+ workdir?: string;
39
+ timeoutMs?: number;
40
+ env?: Record<string, string>;
41
+ }
42
+ export interface ModalSandbox {
43
+ readonly sandboxId: string;
44
+ exec(command: string[], params?: ModalExecParams): Promise<ModalProcess>;
45
+ terminate(): Promise<void>;
46
+ /** The exit code once the sandbox has finished, null while it runs. */
47
+ poll(): Promise<number | null>;
48
+ /** Resolves with the exit code once the sandbox has finished. */
49
+ wait(): Promise<number>;
50
+ getTags(): Promise<Record<string, string>>;
51
+ tunnels(timeoutMs?: number): Promise<Record<number, {
52
+ url: string;
53
+ }>>;
54
+ }
55
+ export interface ModalSandboxCreateParams {
56
+ cpu?: number;
57
+ memoryMiB?: number;
58
+ timeoutMs?: number;
59
+ workdir?: string;
60
+ env?: Record<string, string>;
61
+ encryptedPorts?: number[];
62
+ blockNetwork?: boolean;
63
+ outboundCidrAllowlist?: string[];
64
+ outboundDomainAllowlist?: string[];
65
+ runtime?: "gvisor" | "vm";
66
+ tags?: Record<string, string>;
67
+ }
68
+ export interface ModalImage {
69
+ dockerfileCommands(commands: string[]): ModalImage;
70
+ }
71
+ export interface ModalClient {
72
+ apps: {
73
+ fromName(name: string, params?: {
74
+ environment?: string;
75
+ createIfMissing?: boolean;
76
+ }): Promise<{
77
+ appId: string;
78
+ }>;
79
+ };
80
+ images: {
81
+ fromRegistry(tag: string): ModalImage;
82
+ };
83
+ sandboxes: {
84
+ create(app: {
85
+ appId: string;
86
+ }, image: ModalImage, params?: ModalSandboxCreateParams): Promise<ModalSandbox>;
87
+ fromId(sandboxId: string): Promise<ModalSandbox>;
88
+ list(params?: {
89
+ appId?: string;
90
+ tags?: Record<string, string>;
91
+ environment?: string;
92
+ }): AsyncIterable<ModalSandbox>;
93
+ };
94
+ close?(): void;
95
+ }
96
+ export interface ModalModule {
97
+ ModalClient: new (params: ModalConnection) => ModalClient;
98
+ }
99
+ export declare function loadModalModule(): Promise<ModalModule>;
100
+ /**
101
+ * `error` with the connection's tokens replaced in its message and stack. The SDK can quote a
102
+ * token, for instance when gRPC refuses one with a stray zero-width character as metadata, and a
103
+ * provider error reaches run warnings and reclaim receipts.
104
+ */
105
+ export declare function withoutTokens(error: unknown, connection: ModalConnection): unknown;
106
+ /**
107
+ * Whether `error` is Modal's answer that no sandbox has this id: a gRPC NOT_FOUND (code 5), which
108
+ * the SDK raises from the first call on a handle `fromId` returned for an unknown id.
109
+ */
110
+ export declare function isModalNotFoundError(error: unknown): boolean;
111
+ /** Modal ends a sandbox after 24 hours on every plan. */
112
+ export declare const MODAL_MAX_SANDBOX_MS: number;
@@ -0,0 +1,61 @@
1
+ // The optional `modal` peer: a lazy loader and the structural subset of its JavaScript SDK that
2
+ // humanish calls. Like @e2b/desktop it stays out of the published tarball, and these interfaces
3
+ // keep its types out of humanish's own declarations.
4
+ import { desktopPeerAdvice, humanishInstall } from "../e2b/peer-install.js";
5
+ /** The variables a Modal connection is read from, as Modal's own CLI and SDK name them. */
6
+ export const MODAL_TOKEN_ID_ENV = "MODAL_TOKEN_ID";
7
+ export const MODAL_TOKEN_SECRET_ENV = "MODAL_TOKEN_SECRET";
8
+ const MODAL_ENVIRONMENT_ENV = "MODAL_ENVIRONMENT";
9
+ /**
10
+ * The connection an env names, or undefined when either token is missing. The SDK would otherwise
11
+ * fall back to process.env and ~/.modal.toml, which a library caller's env may not match, and a
12
+ * release could then reach another workspace than the create.
13
+ */
14
+ export function modalConnection(env) {
15
+ const tokenId = env[MODAL_TOKEN_ID_ENV]?.trim();
16
+ const tokenSecret = env[MODAL_TOKEN_SECRET_ENV]?.trim();
17
+ if (!tokenId || !tokenSecret)
18
+ return undefined;
19
+ const environment = env[MODAL_ENVIRONMENT_ENV]?.trim();
20
+ return { tokenId, tokenSecret, ...(environment ? { environment } : {}) };
21
+ }
22
+ export async function loadModalModule() {
23
+ try {
24
+ return (await import("modal"));
25
+ }
26
+ catch (error) {
27
+ const value = error;
28
+ if (value.code === "ERR_MODULE_NOT_FOUND" && value.message?.includes("'modal'") === true) {
29
+ const { where, advice } = desktopPeerAdvice(humanishInstall(), "modal");
30
+ throw new Error(`Running on Modal requires the optional peer modal${where}. ${advice}`);
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+ /**
36
+ * `error` with the connection's tokens replaced in its message and stack. The SDK can quote a
37
+ * token, for instance when gRPC refuses one with a stray zero-width character as metadata, and a
38
+ * provider error reaches run warnings and reclaim receipts.
39
+ */
40
+ export function withoutTokens(error, connection) {
41
+ if (!(error instanceof Error))
42
+ return error;
43
+ const tokens = [connection.tokenId, connection.tokenSecret];
44
+ const hide = (text) => tokens.reduce((hidden, token) => hidden.replaceAll(token, "[REDACTED_MODAL_TOKEN]"), text);
45
+ error.message = hide(error.message);
46
+ if (error.stack !== undefined)
47
+ error.stack = hide(error.stack);
48
+ return error;
49
+ }
50
+ /**
51
+ * Whether `error` is Modal's answer that no sandbox has this id: a gRPC NOT_FOUND (code 5), which
52
+ * the SDK raises from the first call on a handle `fromId` returned for an unknown id.
53
+ */
54
+ export function isModalNotFoundError(error) {
55
+ if (error === null || typeof error !== "object")
56
+ return false;
57
+ const value = error;
58
+ return value.name === "NotFoundError" || value.code === 5;
59
+ }
60
+ /** Modal ends a sandbox after 24 hours on every plan. */
61
+ export const MODAL_MAX_SANDBOX_MS = 24 * 60 * 60_000;
@@ -0,0 +1,10 @@
1
+ import type { RunBundle } from "../run/bundle.js";
2
+ import type { PreparedRunArtifactPaths } from "../run/paths.js";
3
+ /**
4
+ * One finding per problem with the run's browser network logs: a stream index without the
5
+ * schema's shape or naming a file other than network/<its participant id>.ndjson, a file that is
6
+ * missing, unreadable or does not match its index's byte count, sha256 and record count, a record
7
+ * outside the schema or on an origin its index does not list, and a file under network/ that no
8
+ * index names. Each finding names the file and the line, never what the line holds.
9
+ */
10
+ export declare function networkFindings(bundle: RunBundle, runPaths: PreparedRunArtifactPaths): Promise<string[]>;