humanish 0.137.0 → 0.139.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 (63) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cli/commands/runs.js +2 -2
  3. package/dist/cli/doctor.js +24 -0
  4. package/dist/keys/key-resolution.js +53 -2
  5. package/dist/routes/computer-use/bundle-parts.d.ts +3 -0
  6. package/dist/routes/computer-use/bundle-parts.js +9 -1
  7. package/dist/routes/computer-use/bundle.js +3 -4
  8. package/dist/routes/computer-use/e2b-desktop/prepare.js +8 -4
  9. package/dist/routes/computer-use/fanout-bundle.js +3 -2
  10. package/dist/routes/computer-use/participant-deps.d.ts +1 -1
  11. package/dist/routes/computer-use/plan.js +66 -19
  12. package/dist/routes/computer-use/setup.js +14 -7
  13. package/dist/routes/computer-use/types.d.ts +3 -0
  14. package/dist/routes/terminal/live-sandbox.d.ts +2 -0
  15. package/dist/routes/terminal/live-sandbox.js +8 -3
  16. package/dist/routes/terminal/plan.js +49 -6
  17. package/dist/routes/terminal/session.js +8 -1
  18. package/dist/run/bundle-shape.js +1 -1
  19. package/dist/run/bundle.d.ts +5 -3
  20. package/dist/run/cost-summary.js +9 -4
  21. package/dist/run/feedback-shape.js +1 -0
  22. package/dist/run/pricing.d.ts +11 -0
  23. package/dist/run/pricing.js +18 -5
  24. package/dist/run/reclaim-outcomes.d.ts +4 -1
  25. package/dist/run/reclaim-outcomes.js +8 -6
  26. package/dist/run/reclaim.d.ts +4 -1
  27. package/dist/run/reclaim.js +81 -34
  28. package/dist/run/sandbox-creates.d.ts +7 -4
  29. package/dist/run/sandbox-creates.js +17 -9
  30. package/dist/run/sandbox-receipts.d.ts +13 -5
  31. package/dist/run/sandbox-receipts.js +18 -6
  32. package/dist/study/composition-rules.js +14 -12
  33. package/dist/study/doctor.js +1 -1
  34. package/dist/study/parse/execution.js +6 -2
  35. package/dist/study/plan-base.d.ts +1 -0
  36. package/dist/study/plan-base.js +2 -0
  37. package/dist/study/plan-types.d.ts +6 -1
  38. package/dist/study/requirements.js +7 -2
  39. package/dist/study/routing.d.ts +1 -1
  40. package/dist/study/routing.js +1 -1
  41. package/dist/study/types.d.ts +1 -1
  42. package/dist/study/warnings.js +3 -1
  43. package/dist/substrates/e2b/desktop-resources.d.ts +4 -2
  44. package/dist/substrates/e2b/desktop-resources.js +2 -2
  45. package/dist/substrates/e2b/peer-install.js +1 -1
  46. package/dist/substrates/e2b/sandbox.d.ts +3 -0
  47. package/dist/substrates/e2b/sandbox.js +5 -4
  48. package/dist/substrates/e2b/sdk.d.ts +5 -0
  49. package/dist/substrates/e2b/sdk.js +12 -0
  50. package/dist/substrates/modal/commands.d.ts +16 -0
  51. package/dist/substrates/modal/commands.js +174 -0
  52. package/dist/substrates/modal/desktop.d.ts +16 -0
  53. package/dist/substrates/modal/desktop.js +195 -0
  54. package/dist/substrates/modal/image.d.ts +11 -0
  55. package/dist/substrates/modal/image.js +71 -0
  56. package/dist/substrates/modal/sandbox.d.ts +30 -0
  57. package/dist/substrates/modal/sandbox.js +325 -0
  58. package/dist/substrates/modal/sdk.d.ts +112 -0
  59. package/dist/substrates/modal/sdk.js +61 -0
  60. package/docs/architecture/terminal-product-route.md +1 -1
  61. package/docs/contracts/run-bundle.md +5 -2
  62. package/docs/contracts/schemas.md +8 -7
  63. package/package.json +8 -2
@@ -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;
@@ -28,7 +28,7 @@ fail-closed cross-validation, and forward-declared warnings.
28
28
  | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
29
29
  | `subject.source` | `terminal-product` |
30
30
  | `subject.product` | `{ name, publicSurfaces[], install?, workdir?, upload? }`: the only world the agent sees |
31
- | `execution.target` | `e2b-terminal` (or absent → implied) |
31
+ | `execution.target` | `e2b-terminal` (or absent → implied), or `modal-terminal` for a Modal sandbox |
32
32
  | `execution.terminal` | `{ transport: exec-stream, stdin: disabled }` |
33
33
  | `execution.runtimeAuth` | `openai-egress` (default) or opt-in `openai-env`; names-only durable evidence |
34
34
  | `execution.runtime.version` | Optional exact `@openai/codex` version; observed before keyed execution |
@@ -703,8 +703,11 @@ Dry runs omit `cost`. A live run that spends nothing records an explicit zero: n
703
703
  `breakdown` lines, `estimatedTotalUsd: 0`, `ratesAsOf: null` and `fullyEstimated: true`.
704
704
  A local scripted run (no model request, no hosted desktop) is such a run; a scripted run on
705
705
  a provisioned clone records the clone's desktop as a `laneId: subject` line. A live bundle
706
- without `cost` has an unmeasured spend. A live terminal run's `cost` prices the E2B terminal
707
- sandbox as a `desktop-minutes` line from its acquired-to-cleanup span and `e2b.getInfo` size.
706
+ without `cost` has an unmeasured spend. A live terminal run's `cost` prices its shell sandbox
707
+ as a `desktop-minutes` line from its acquired-to-cleanup span and size: E2B's report
708
+ (`resourceSource: e2b.getInfo`), or the size humanish asked Modal for (`modal.create`), priced at
709
+ Modal's sandbox rate. A computer-use run on `execution.target: modal-desktop` records each
710
+ desktop's `providerResources[].provider` as `modal-desktop` and prices it the same way.
708
711
  Its Codex `model-tokens` line prices the agent's tokens from the model passed as `--model`
709
712
  (`actor.model`, else `gpt-5.6-sol`). Codex reports usage per turn, summed over the turn's
710
713
  requests, so the line prices every token at the base tier and carries
@@ -336,10 +336,10 @@ The rest of this section names keys by their v3 spelling:
336
336
  - `subject.appUrl`: a loopback http(s) URL a computer-use actor drives, or (on the
337
337
  external-public shared-world route) the non-loopback public deployment used as the
338
338
  shared plane (with `publicTarget` + `allowPublicTargets`);
339
- - `execution`: where it runs (`local`, `e2b-desktop`, or `e2b-terminal`), plus
340
- desktop device/resolution and timeouts. app-url subjects pair `e2b-desktop`
341
- or `local` with a computer-use actor, or `local` (or absent) with a
342
- scripted-browser actor; terminal-product subjects pair `e2b-terminal` (or absent → implied)
339
+ - `execution`: where it runs (`local`, `e2b-desktop`, `e2b-terminal`, `modal-desktop` or
340
+ `modal-terminal`), plus desktop device/resolution and timeouts. app-url subjects pair
341
+ `e2b-desktop`, `modal-desktop` or `local` with a computer-use actor, or `local` (or absent) with a
342
+ scripted-browser actor; terminal-product subjects pair `e2b-terminal` or `modal-terminal` (or absent → `e2b-terminal`)
343
343
  with a registered terminal actor. clone and local-tree subjects require an
344
344
  explicit `e2b-desktop`: any other target, or none, fails to parse, and the
345
345
  computer-use and scripted-browser routes refuse it when a library caller skips
@@ -1297,7 +1297,7 @@ items: []
1297
1297
  Reserved: `humanish.substrate.v1` is named here for layering intent but has
1298
1298
  never shipped; no code emits or validates it. Substrate truth today lives
1299
1299
  inside run bundles (per-stream transport and status) and study execution config
1300
- (`execution.target: local | e2b-desktop | e2b-terminal`). Do not emit this schema.
1300
+ (`execution.target: local | e2b-desktop | e2b-terminal | modal-desktop | modal-terminal`). Do not emit this schema.
1301
1301
 
1302
1302
  ## Serve Result And Reserved Control-Plane Namespace
1303
1303
 
@@ -1578,12 +1578,13 @@ Three new `.v1` schema tags ship, all additive and optional so
1578
1578
  lines remain readable.
1579
1579
  A desktop line's optional `desktop` object records `minutes`,
1580
1580
  `durationBasis: host-acquired-to-cleanup`, observed `resources` (`cpuCount`,
1581
- `memoryMiB`), `resourceSource: e2b.getInfo`, and `usdPerSecond` when priceable.
1581
+ `memoryMiB`), `resourceSource` (`e2b.getInfo`, or `modal.create` for the size humanish asked
1582
+ Modal for), and `usdPerSecond` when priceable.
1582
1583
  Failed metadata reads record `resourceUnavailableReason`; no resource guess is used.
1583
1584
  Resource metadata and missing/unsupported rates produce a null line. A kept or
1584
1585
  unconfirmed allocation adds `desktop_lifetime_incomplete` as a second null line.
1585
1586
  Live terminal-product runs emit the same summary: one `desktop-minutes` line for
1586
- the E2B shell sandbox (its acquired-to-cleanup span and `e2b.getInfo` size) and
1587
+ the shell sandbox (its acquired-to-cleanup span and its `e2b.getInfo` or `modal.create` size) and
1587
1588
  one `model-tokens` line for the Codex participant, priced from the model the route passed
1588
1589
  to Codex (`gpt-5.6-sol` unless the study declares one) with `basis:
1589
1590
  aggregated_turns_base_rate`, or `null` with `no_token_usage`. Older terminal bundles record
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.137.0",
3
+ "version": "0.139.0",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",
@@ -109,6 +109,7 @@
109
109
  "parse5": "^8.0.1",
110
110
  "playwright-core": "^1.63.0",
111
111
  "pngjs": "^7.0.0",
112
+ "smol-toml": "^1.9.0",
112
113
  "undici": "^8.11.2",
113
114
  "yaml": "^2.9.1",
114
115
  "zod": "^4.6.5"
@@ -120,6 +121,7 @@
120
121
  "axe-core": "4.14.0",
121
122
  "fast-check": "catalog:",
122
123
  "knip": "6.41.0",
124
+ "modal": "^0.11.1",
123
125
  "oxc-parser": "catalog:",
124
126
  "oxfmt": "0.73.0",
125
127
  "oxlint": "1.88.0",
@@ -130,11 +132,15 @@
130
132
  "vitest": "catalog:"
131
133
  },
132
134
  "peerDependencies": {
133
- "@e2b/desktop": "^2.3.2"
135
+ "@e2b/desktop": "^2.3.2",
136
+ "modal": "^0.11.1"
134
137
  },
135
138
  "peerDependenciesMeta": {
136
139
  "@e2b/desktop": {
137
140
  "optional": true
141
+ },
142
+ "modal": {
143
+ "optional": true
138
144
  }
139
145
  },
140
146
  "engines": {