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.
- package/CHANGELOG.md +23 -0
- package/dist/actors/computer-use/actor.d.ts +2 -0
- package/dist/actors/computer-use/actor.js +1 -0
- package/dist/actors/computer-use/loop/observation.js +2 -0
- package/dist/actors/computer-use/loop/session.d.ts +3 -2
- package/dist/actors/computer-use/loop/session.js +8 -12
- package/dist/actors/computer-use/loop/types.d.ts +14 -1
- package/dist/cli/commands/runs.js +2 -2
- package/dist/cli/doctor.js +24 -0
- package/dist/evidence/browser-network.d.ts +108 -0
- package/dist/evidence/browser-network.js +516 -0
- package/dist/evidence/trace-page.d.ts +31 -1
- package/dist/evidence/trace-page.js +68 -2
- package/dist/keys/key-resolution.js +53 -2
- package/dist/observer-app.html +2 -2
- package/dist/routes/computer-use/bundle.js +1 -0
- package/dist/routes/computer-use/e2b-desktop/desktop.js +6 -0
- package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +12 -1
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +7 -2
- package/dist/routes/computer-use/e2b-desktop/network.d.ts +52 -0
- package/dist/routes/computer-use/e2b-desktop/network.js +153 -0
- package/dist/routes/computer-use/e2b-desktop/start.js +18 -1
- package/dist/routes/computer-use/e2b-desktop/state.d.ts +3 -0
- package/dist/routes/computer-use/e2b-desktop/state.js +2 -0
- package/dist/routes/computer-use/e2b-desktop/teardown.js +6 -3
- package/dist/routes/computer-use/fanout-records.js +10 -0
- package/dist/routes/computer-use/participant-desktop.d.ts +7 -2
- package/dist/routes/computer-use/participant-model.js +2 -1
- package/dist/routes/computer-use/plan.js +2 -1
- package/dist/routes/computer-use/single-bundle.d.ts +3 -0
- package/dist/routes/computer-use/single-bundle.js +4 -0
- package/dist/routes/computer-use/types.d.ts +7 -0
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/shared-world/plan.js +3 -2
- package/dist/routes/terminal/live-sandbox.d.ts +2 -0
- package/dist/routes/terminal/live-sandbox.js +8 -3
- package/dist/routes/terminal/plan.js +51 -8
- package/dist/routes/terminal/session.js +8 -1
- package/dist/run/bundle.d.ts +2 -1
- package/dist/run/cost-summary.js +9 -4
- package/dist/run/live-items.d.ts +20 -2
- package/dist/run/live-items.js +23 -14
- package/dist/run/pricing.d.ts +11 -0
- package/dist/run/pricing.js +18 -5
- package/dist/run/reclaim-outcomes.d.ts +4 -1
- package/dist/run/reclaim-outcomes.js +8 -6
- package/dist/run/reclaim.d.ts +4 -1
- package/dist/run/reclaim.js +81 -34
- package/dist/run/sandbox-creates.d.ts +7 -4
- package/dist/run/sandbox-creates.js +17 -9
- package/dist/run/sandbox-receipts.d.ts +13 -5
- package/dist/run/sandbox-receipts.js +18 -6
- package/dist/run/streams.d.ts +7 -0
- package/dist/study/composition-rules.js +11 -10
- package/dist/study/config.js +4 -1
- package/dist/study/doctor.js +1 -1
- package/dist/study/keys.d.ts +3 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/parse/comms.js +1 -20
- package/dist/study/parse/execution.js +35 -3
- package/dist/study/parse/values.d.ts +5 -0
- package/dist/study/parse/values.js +22 -0
- package/dist/study/plan-base.js +6 -1
- package/dist/study/plan-types.d.ts +5 -2
- package/dist/study/requirements.js +7 -2
- package/dist/study/routing.d.ts +1 -1
- package/dist/study/routing.js +1 -1
- package/dist/study/study-fields.d.ts +7 -1
- package/dist/study/study-fields.js +10 -0
- package/dist/study/types.d.ts +12 -1
- package/dist/study/validation.d.ts +8 -0
- package/dist/study/validation.js +31 -2
- package/dist/substrates/detached.d.ts +5 -0
- package/dist/substrates/detached.js +9 -0
- package/dist/substrates/e2b/cdp-network-capture.d.ts +23 -0
- package/dist/substrates/e2b/cdp-network-capture.js +194 -0
- package/dist/substrates/e2b/cdp-probe.d.ts +20 -5
- package/dist/substrates/e2b/cdp-probe.js +98 -34
- package/dist/substrates/e2b/desktop-cdp.d.ts +32 -2
- package/dist/substrates/e2b/desktop-cdp.js +87 -5
- package/dist/substrates/e2b/desktop-resources.d.ts +4 -2
- package/dist/substrates/e2b/desktop-resources.js +2 -2
- package/dist/substrates/e2b/peer-install.js +1 -1
- package/dist/substrates/e2b/sandbox.d.ts +3 -0
- package/dist/substrates/e2b/sandbox.js +5 -4
- package/dist/substrates/modal/image.d.ts +9 -0
- package/dist/substrates/modal/image.js +36 -0
- package/dist/substrates/modal/sandbox.d.ts +26 -0
- package/dist/substrates/modal/sandbox.js +367 -0
- package/dist/substrates/modal/sdk.d.ts +112 -0
- package/dist/substrates/modal/sdk.js +61 -0
- package/dist/verify/network.d.ts +10 -0
- package/dist/verify/network.js +162 -0
- package/dist/verify/pages.js +1 -16
- package/dist/verify/verify.js +8 -2
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +90 -3
- package/docs/contracts/schemas.md +6 -5
- package/package.json +8 -2
|
@@ -11,6 +11,7 @@ import { extractLocalActorVerdict, normalizeLocalActorTranscript, } from "../../
|
|
|
11
11
|
import { NODE_BOOTSTRAP_COMMAND, NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
12
12
|
import { observeDesktopResources, } from "../../substrates/e2b/desktop-resources.js";
|
|
13
13
|
import { acquireE2BShellSandbox } from "../../substrates/e2b/sandbox.js";
|
|
14
|
+
import { loadModalShellModule } from "../../substrates/modal/sandbox.js";
|
|
14
15
|
import { e2bAccount } from "../../substrates/e2b/connection.js";
|
|
15
16
|
import { E2BDesktopStartupError, loadE2BDesktopModule, } from "../../substrates/e2b/sdk.js";
|
|
16
17
|
import { shellQuote } from "../../substrates/shell.js";
|
|
@@ -60,7 +61,10 @@ export class LiveTerminalSandbox {
|
|
|
60
61
|
const { requestTimeoutMs, sandboxTimeoutMs } = this;
|
|
61
62
|
// Declared egress allowlist, or undefined for the historical unrestricted default.
|
|
62
63
|
const egressAllow = plan.egressAllow;
|
|
63
|
-
const
|
|
64
|
+
const provider = plan.sandboxProvider;
|
|
65
|
+
const providerName = provider === "modal" ? "Modal" : "E2B";
|
|
66
|
+
const sandboxModule = await (deps.desktopModule ??
|
|
67
|
+
(provider === "modal" ? () => loadModalShellModule(this.inputs.env) : loadE2BDesktopModule))();
|
|
64
68
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
65
69
|
// No sandbox-global env in either mode. In openai-egress, only this host-side SDK request
|
|
66
70
|
// carries the real runtime key; participant commands receive an inert placeholder. The proxy
|
|
@@ -83,6 +87,7 @@ export class LiveTerminalSandbox {
|
|
|
83
87
|
this.account = e2bAccount(sandboxModule, options);
|
|
84
88
|
const acquired = await acquireE2BShellSandbox({
|
|
85
89
|
module: sandboxModule,
|
|
90
|
+
provider,
|
|
86
91
|
options,
|
|
87
92
|
retry: {
|
|
88
93
|
// A failed first attempt may have allocated a sandbox whose id never reached this run;
|
|
@@ -103,8 +108,8 @@ export class LiveTerminalSandbox {
|
|
|
103
108
|
this.sandboxId = sandboxId;
|
|
104
109
|
this.createdAtMs = now();
|
|
105
110
|
await validatePreparedRunArtifactPaths(runPaths);
|
|
106
|
-
recordLifecycle("terminal-lab.sandbox.created",
|
|
107
|
-
const sandboxResources = await observeDesktopResources(sandbox);
|
|
111
|
+
recordLifecycle("terminal-lab.sandbox.created", `${providerName} shell sandbox ${sandboxId} created with allowlisted metadata and kill-on-timeout, and with no sandbox-wide environment variables.`);
|
|
112
|
+
const sandboxResources = await observeDesktopResources(sandbox, undefined, provider === "modal" ? "modal.create" : "e2b.getInfo");
|
|
108
113
|
this.resources = sandboxResources;
|
|
109
114
|
if ("reason" in sandboxResources) {
|
|
110
115
|
warnings.push(`Sandbox resource size unavailable (${sandboxResources.reason}); its compute cost remains unpriced.`);
|
|
@@ -7,8 +7,9 @@ import { isReasoningEffort } from "../../actors/reasoning-effort.js";
|
|
|
7
7
|
import { actorRegistry, isTerminalActorDescriptor } from "../../actors/registry.js";
|
|
8
8
|
import { isNonEmpty, planBase } from "../../study/plan-base.js";
|
|
9
9
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
10
|
-
import { desktopMediaValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
10
|
+
import { desktopMediaValidationReason, networkCaptureValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
11
11
|
import { ceilingAdvice } from "../../substrates/e2b/lifetime.js";
|
|
12
|
+
import { MODAL_MAX_SANDBOX_MS, MODAL_TOKEN_ID_ENV, MODAL_TOKEN_SECRET_ENV, } from "../../substrates/modal/sdk.js";
|
|
12
13
|
import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
13
14
|
import { terminalSandboxTimeoutMs } from "./lifetime.js";
|
|
14
15
|
import { isExactRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./runtime.js";
|
|
@@ -24,7 +25,7 @@ export function planTerminalStudy(config, input) {
|
|
|
24
25
|
});
|
|
25
26
|
if (String(config.comms?.email?.kind) === "real")
|
|
26
27
|
return refuse("HUMANISH_TERMINAL_SUBJECT_INVALID", "Real email receiving is unsupported on the terminal route. Use a supported hosted computer-use browser study.");
|
|
27
|
-
const mediaReason = desktopMediaValidationReason(config);
|
|
28
|
+
const mediaReason = desktopMediaValidationReason(config) ?? networkCaptureValidationReason(config, "terminal");
|
|
28
29
|
if (mediaReason)
|
|
29
30
|
return refuse("HUMANISH_TERMINAL_SUBJECT_INVALID", mediaReason);
|
|
30
31
|
// A library caller's config skips the parser, which refuses these first.
|
|
@@ -34,6 +35,10 @@ export function planTerminalStudy(config, input) {
|
|
|
34
35
|
const runtimeAuth = config.execution?.runtimeAuth;
|
|
35
36
|
if (runtimeAuth !== undefined && runtimeAuth !== "openai-egress" && runtimeAuth !== "openai-env")
|
|
36
37
|
return refuse("HUMANISH_TERMINAL_CREDENTIAL_DENIED", "`execution.runtimeAuth` must be openai-egress or openai-env. Leave it out for openai-egress, which keeps the OpenAI key outside the sandbox.");
|
|
38
|
+
const sandboxProvider = config.execution?.target === "modal-terminal" ? "modal" : "e2b";
|
|
39
|
+
const modal = sandboxProvider === "modal" ? modalRefusal(config) : undefined;
|
|
40
|
+
if (modal !== undefined)
|
|
41
|
+
return refuse(modal.code, modal.message);
|
|
37
42
|
const analysis = resolveAutomaticAnalysis(config.review?.analysis);
|
|
38
43
|
if (!analysis.ok)
|
|
39
44
|
return refuse("HUMANISH_STUDY_ANALYSIS_INVALID", analysis.message);
|
|
@@ -64,6 +69,7 @@ export function planTerminalStudy(config, input) {
|
|
|
64
69
|
const shared = {
|
|
65
70
|
...base,
|
|
66
71
|
route: "terminal",
|
|
72
|
+
sandboxProvider,
|
|
67
73
|
actor: descriptor.id,
|
|
68
74
|
product: { ...product, publicSurfaces: surfaces },
|
|
69
75
|
...(actor?.persona === undefined ? {} : { personaId: actor.persona }),
|
|
@@ -107,14 +113,16 @@ export function planTerminalStudy(config, input) {
|
|
|
107
113
|
// The sandbox's timeout covers the steps before the codex command, maxMinutes and the teardown
|
|
108
114
|
// buffer, and E2B refuses a sandbox past the plan's ceiling. Refuse here, with the arithmetic,
|
|
109
115
|
// rather than after the plan prints, from a provider 400 that names neither knob.
|
|
110
|
-
const ceiling =
|
|
116
|
+
const ceiling = sandboxProvider === "modal"
|
|
117
|
+
? { ok: true, ms: MODAL_MAX_SANDBOX_MS, source: "default" }
|
|
118
|
+
: input.sandboxCeiling;
|
|
111
119
|
if (!ceiling.ok)
|
|
112
120
|
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", ceiling.message, descriptor.id);
|
|
113
121
|
const productInstall = shared.product.install !== undefined;
|
|
114
122
|
const sandboxTimeoutMs = terminalSandboxTimeoutMs({ maxMinutes, productInstall });
|
|
115
123
|
if (sandboxTimeoutMs > ceiling.ms) {
|
|
116
124
|
const headroomMinutes = (sandboxTimeoutMs - maxMinutes * 60_000) / 60_000;
|
|
117
|
-
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", `caps.maxMinutes ${maxMinutes} derives a ${sandboxTimeoutMs / 60_000}m sandbox deadline, and a sandbox may not live longer than ${ceiling.ms / 60_000}m. The deadline is maxMinutes plus ${headroomMinutes}m: the Node bootstrap (${NODE_BOOTSTRAP_TIMEOUT_MS / 60_000}m), the runtime version check (${TERMINAL_RUNTIME_VERSION_TIMEOUT_MS / 60_000}m)${productInstall ? `, the product setup (${PRODUCT_SETUP_TIMEOUT_MS / 60_000}m)` : ""} and the teardown buffer (${TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS / 60_000}m). Lower caps.maxMinutes to at most ${ceiling.ms / 60_000 - headroomMinutes}.${ceilingAdvice(sandboxTimeoutMs)} No sandbox was created and the runtime key was not used.`, descriptor.id);
|
|
125
|
+
return refuse("HUMANISH_TERMINAL_CAPS_INVALID", `caps.maxMinutes ${maxMinutes} derives a ${sandboxTimeoutMs / 60_000}m sandbox deadline, and a sandbox may not live longer than ${ceiling.ms / 60_000}m. The deadline is maxMinutes plus ${headroomMinutes}m: the Node bootstrap (${NODE_BOOTSTRAP_TIMEOUT_MS / 60_000}m), the runtime version check (${TERMINAL_RUNTIME_VERSION_TIMEOUT_MS / 60_000}m)${productInstall ? `, the product setup (${PRODUCT_SETUP_TIMEOUT_MS / 60_000}m)` : ""} and the teardown buffer (${TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS / 60_000}m). Lower caps.maxMinutes to at most ${ceiling.ms / 60_000 - headroomMinutes}.${sandboxProvider === "modal" ? " Modal ends every sandbox after 24 hours." : ceilingAdvice(sandboxTimeoutMs)} No sandbox was created and the runtime key was not used.`, descriptor.id);
|
|
118
126
|
}
|
|
119
127
|
return {
|
|
120
128
|
ok: true,
|
|
@@ -122,10 +130,45 @@ export function planTerminalStudy(config, input) {
|
|
|
122
130
|
...shared,
|
|
123
131
|
dryRun: false,
|
|
124
132
|
caps: { ...caps, maxUsd, maxMinutes },
|
|
125
|
-
requirements:
|
|
126
|
-
{ kind: "key", name: "E2B_API_KEY" },
|
|
127
|
-
{ kind: "key-one-of", names: ["CODEX_API_KEY", "OPENAI_API_KEY"] },
|
|
128
|
-
],
|
|
133
|
+
requirements: liveRequirements(sandboxProvider),
|
|
129
134
|
},
|
|
130
135
|
};
|
|
131
136
|
}
|
|
137
|
+
/** The sandbox provider's keys, then the runtime key the agent's command gets. */
|
|
138
|
+
function liveRequirements(provider) {
|
|
139
|
+
return [
|
|
140
|
+
...(provider === "modal"
|
|
141
|
+
? [
|
|
142
|
+
{ kind: "key", name: MODAL_TOKEN_ID_ENV },
|
|
143
|
+
{ kind: "key", name: MODAL_TOKEN_SECRET_ENV },
|
|
144
|
+
]
|
|
145
|
+
: [{ kind: "key", name: "E2B_API_KEY" }]),
|
|
146
|
+
{ kind: "key-one-of", names: ["CODEX_API_KEY", "OPENAI_API_KEY"] },
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
/** What a terminal study on Modal cannot have that one on E2B can. */
|
|
150
|
+
function modalRefusal(config) {
|
|
151
|
+
// E2B adds the key to requests at its egress proxy for each sandbox. Modal does that only from a
|
|
152
|
+
// Secret stored in the operator's workspace, so humanish does not place the key there itself.
|
|
153
|
+
if (config.execution?.runtimeAuth !== "openai-env")
|
|
154
|
+
return {
|
|
155
|
+
code: "HUMANISH_TERMINAL_CREDENTIAL_DENIED",
|
|
156
|
+
message: "On Modal, humanish cannot keep the OpenAI key outside the sandbox: Modal adds a key to outbound requests only from a Secret stored in your Modal workspace. Set `execution.runtimeAuth: openai-env`, which passes the key to the codex command only, or use `execution.target: e2b-terminal`.",
|
|
157
|
+
};
|
|
158
|
+
const egress = modalEgressReason(config.execution?.egressAllow);
|
|
159
|
+
return egress === undefined
|
|
160
|
+
? undefined
|
|
161
|
+
: { code: "HUMANISH_TERMINAL_SUBJECT_INVALID", message: egress };
|
|
162
|
+
}
|
|
163
|
+
const CIDR = /^[0-9a-f:.]+\/\d{1,3}$/i;
|
|
164
|
+
/**
|
|
165
|
+
* Why a declared egress allowlist cannot run on Modal, or undefined when it can. Modal limits
|
|
166
|
+
* egress to listed domains or CIDR ranges; an entry that is neither, such as a bare IP address,
|
|
167
|
+
* has no Modal form.
|
|
168
|
+
*/
|
|
169
|
+
function modalEgressReason(egressAllow) {
|
|
170
|
+
const unsupported = (egressAllow ?? []).filter((entry) => !CIDR.test(entry) && !/^(\*\.)?([a-z0-9-]+\.)+[a-z0-9-]*[a-z][a-z0-9-]*$/i.test(entry));
|
|
171
|
+
return unsupported.length === 0
|
|
172
|
+
? undefined
|
|
173
|
+
: `Modal limits egress to domain names and CIDR ranges, and \`execution.egressAllow\` lists ${unsupported.map((entry) => `"${entry}"`).join(", ")}. Write a bare address as a /32 range, or use \`execution.target: e2b-terminal\`.`;
|
|
174
|
+
}
|
|
@@ -95,6 +95,7 @@ export async function runLiveTerminalSession(args) {
|
|
|
95
95
|
verdictNonce,
|
|
96
96
|
maxMinutes,
|
|
97
97
|
e2bApiKey,
|
|
98
|
+
env,
|
|
98
99
|
runPaths,
|
|
99
100
|
metadata: buildSandboxMetadata({ studyId: plan.studyId, recordId: "sim-001", runId }),
|
|
100
101
|
warnings,
|
|
@@ -160,7 +161,13 @@ async function prepareLivePrompt(args) {
|
|
|
160
161
|
// anything persists (a key has no detectable "shape" if it is an arbitrary token); redactText is
|
|
161
162
|
// the second pass for secret-shaped content. Applied pre-truncation so a cut can never split a
|
|
162
163
|
// value past the scrubber.
|
|
163
|
-
|
|
164
|
+
// The sandbox provider's credentials too: a provider error that quotes one reaches the run.
|
|
165
|
+
const secrets = new RunSecrets([
|
|
166
|
+
runtimeEnv.keyValue,
|
|
167
|
+
env.E2B_API_KEY?.trim() ?? "",
|
|
168
|
+
env.MODAL_TOKEN_ID?.trim() ?? "",
|
|
169
|
+
env.MODAL_TOKEN_SECRET?.trim() ?? "",
|
|
170
|
+
]);
|
|
164
171
|
const sanitize = (text) => redactText(secrets.scrub(text));
|
|
165
172
|
const persona = terminalPersonaRef(terminalPersona, promptDigest, secrets.scrub);
|
|
166
173
|
return {
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -441,7 +441,8 @@ export interface RunCostLine {
|
|
|
441
441
|
cpuCount: number;
|
|
442
442
|
memoryMiB: number;
|
|
443
443
|
};
|
|
444
|
-
|
|
444
|
+
/** E2B's report of the allocation, or the size humanish asked Modal for. */
|
|
445
|
+
resourceSource?: "e2b.getInfo" | "modal.create";
|
|
445
446
|
resourceUnavailableReason?: "metadata_unavailable" | "metadata_invalid" | "metadata_timeout";
|
|
446
447
|
usdPerSecond?: number;
|
|
447
448
|
};
|
package/dist/run/cost-summary.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { estimateActorCostForExecution, estimateAllocatedDesktopCost, estimateDesktopCost, round6, } from "./pricing.js";
|
|
1
|
+
import { estimateActorCostForExecution, estimateAllocatedDesktopCost, estimateDesktopCost, MODAL_SANDBOX_RESOURCE_RATE, round6, } from "./pricing.js";
|
|
2
2
|
import {} from "./bundle.js";
|
|
3
3
|
export function buildRunCostSummary(args) {
|
|
4
4
|
const breakdown = [...modelCostLines(args.participants), ...desktopCostLines(args)];
|
|
@@ -76,8 +76,11 @@ function desktopCostLines(args) {
|
|
|
76
76
|
const breakdown = [];
|
|
77
77
|
for (const usage of args.desktops ?? []) {
|
|
78
78
|
const observation = usage.observation;
|
|
79
|
-
const
|
|
80
|
-
const
|
|
79
|
+
const observed = observation && "resources" in observation ? observation : undefined;
|
|
80
|
+
const resources = observed?.resources;
|
|
81
|
+
const estimate = observed?.source === "modal.create"
|
|
82
|
+
? estimateAllocatedDesktopCost(usage.minutes, resources, MODAL_SANDBOX_RESOURCE_RATE)
|
|
83
|
+
: estimateAllocatedDesktopCost(usage.minutes, resources);
|
|
81
84
|
breakdown.push({
|
|
82
85
|
kind: "desktop-minutes",
|
|
83
86
|
...(usage.participantId === undefined ? {} : { laneId: usage.participantId }),
|
|
@@ -88,7 +91,9 @@ function desktopCostLines(args) {
|
|
|
88
91
|
desktop: {
|
|
89
92
|
minutes: estimate.minutes,
|
|
90
93
|
durationBasis: "host-acquired-to-cleanup",
|
|
91
|
-
...(
|
|
94
|
+
...(observed === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { resources: observed.resources, resourceSource: observed.source }),
|
|
92
97
|
...(observation && "reason" in observation
|
|
93
98
|
? { resourceUnavailableReason: observation.reason }
|
|
94
99
|
: {}),
|
package/dist/run/live-items.d.ts
CHANGED
|
@@ -13,11 +13,29 @@ export interface LiveItemsCursor {
|
|
|
13
13
|
items: number;
|
|
14
14
|
bytes: number;
|
|
15
15
|
}
|
|
16
|
+
/** How run.json binds a file of JSON lines: its byte count, its sha256 and its number of lines. */
|
|
17
|
+
export interface JsonLinesIndex {
|
|
18
|
+
path: string;
|
|
19
|
+
bytes: number;
|
|
20
|
+
sha256: string;
|
|
21
|
+
lines: number;
|
|
22
|
+
}
|
|
23
|
+
/** What a refusal calls one line of a file and several: `an item` and `items` for a live file. */
|
|
24
|
+
export interface LineNoun {
|
|
25
|
+
one: string;
|
|
26
|
+
many: string;
|
|
27
|
+
}
|
|
16
28
|
/**
|
|
17
|
-
* The
|
|
29
|
+
* The JSON object on each line of a file run.json binds, or why the bytes do not match the index:
|
|
18
30
|
* `bytes` must be exactly the indexed byte count, hash to the indexed sha256, and hold the indexed
|
|
19
|
-
* number of lines, each a JSON object.
|
|
31
|
+
* number of lines, each a JSON object. A refusal names the file and counts, never what it holds.
|
|
20
32
|
*/
|
|
33
|
+
export declare function boundJsonLines(bytes: Buffer, index: JsonLinesIndex, noun: LineNoun): {
|
|
34
|
+
lines: Record<string, unknown>[];
|
|
35
|
+
} | {
|
|
36
|
+
refused: string;
|
|
37
|
+
};
|
|
38
|
+
/** The items the indexed prefix of a live file holds, or why the bytes do not match the index. */
|
|
21
39
|
export declare function liveItemsFromBytes(bytes: Buffer, index: LiveItemsIndex): {
|
|
22
40
|
items: ActorTraceItem[];
|
|
23
41
|
} | {
|
package/dist/run/live-items.js
CHANGED
|
@@ -29,19 +29,24 @@ export function isLiveItemsIndex(value) {
|
|
|
29
29
|
typeof value.sha256 === "string" &&
|
|
30
30
|
/^[0-9a-f]{64}$/.test(value.sha256));
|
|
31
31
|
}
|
|
32
|
+
const ITEM = { one: "an item", many: "items" };
|
|
32
33
|
/**
|
|
33
|
-
* The
|
|
34
|
+
* The JSON object on each line of a file run.json binds, or why the bytes do not match the index:
|
|
34
35
|
* `bytes` must be exactly the indexed byte count, hash to the indexed sha256, and hold the indexed
|
|
35
|
-
* number of lines, each a JSON object.
|
|
36
|
+
* number of lines, each a JSON object. A refusal names the file and counts, never what it holds.
|
|
36
37
|
*/
|
|
37
|
-
export function
|
|
38
|
+
export function boundJsonLines(bytes, index, noun) {
|
|
38
39
|
if (bytes.length !== index.bytes)
|
|
39
40
|
return {
|
|
40
41
|
refused: `${index.path} holds ${bytes.length} bytes and run.json records ${index.bytes}`,
|
|
41
42
|
};
|
|
42
43
|
if (createHash("sha256").update(bytes).digest("hex") !== index.sha256)
|
|
43
44
|
return { refused: `${index.path} does not match the sha256 run.json records for it` };
|
|
44
|
-
return
|
|
45
|
+
return objectLinesOf(bytes, index.lines, index.path, noun);
|
|
46
|
+
}
|
|
47
|
+
/** The items the indexed prefix of a live file holds, or why the bytes do not match the index. */
|
|
48
|
+
export function liveItemsFromBytes(bytes, index) {
|
|
49
|
+
return asItems(boundJsonLines(bytes, { ...index, lines: index.items }, ITEM));
|
|
45
50
|
}
|
|
46
51
|
/**
|
|
47
52
|
* The items past `after` in the indexed prefix of a live file, from the file's bytes between
|
|
@@ -59,10 +64,14 @@ export function liveItemsAfter(bytes, index, after) {
|
|
|
59
64
|
};
|
|
60
65
|
if (bytes[0] !== 0x0a)
|
|
61
66
|
return { refused: `${index.path} has no line end before byte ${after.bytes}` };
|
|
62
|
-
return
|
|
67
|
+
return asItems(objectLinesOf(bytes.subarray(1), index.items - after.items, index.path, ITEM));
|
|
68
|
+
}
|
|
69
|
+
/** A live file's lines as trace items. */
|
|
70
|
+
function asItems(read) {
|
|
71
|
+
return "refused" in read ? read : { items: read.lines };
|
|
63
72
|
}
|
|
64
73
|
/** The JSON object on each of `count` lines in `bytes`, which must be UTF-8 and end a line. */
|
|
65
|
-
function
|
|
74
|
+
function objectLinesOf(bytes, count, file, noun) {
|
|
66
75
|
let text;
|
|
67
76
|
try {
|
|
68
77
|
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
@@ -74,21 +83,21 @@ function itemLinesOf(bytes, count, file) {
|
|
|
74
83
|
if (lines.length > 0 && lines.pop() !== "")
|
|
75
84
|
return { refused: `${file} does not end its indexed bytes at a line end` };
|
|
76
85
|
if (lines.length !== count)
|
|
77
|
-
return { refused: `${file} holds ${lines.length}
|
|
78
|
-
const
|
|
86
|
+
return { refused: `${file} holds ${lines.length} ${noun.many} and run.json records ${count}` };
|
|
87
|
+
const objects = [];
|
|
79
88
|
for (const line of lines) {
|
|
80
|
-
let
|
|
89
|
+
let value;
|
|
81
90
|
try {
|
|
82
|
-
|
|
91
|
+
value = JSON.parse(line);
|
|
83
92
|
}
|
|
84
93
|
catch {
|
|
85
94
|
return { refused: `${file} holds a line that is not JSON` };
|
|
86
95
|
}
|
|
87
|
-
if (!isRecord(
|
|
88
|
-
return { refused: `${file} holds a line that is not
|
|
89
|
-
|
|
96
|
+
if (!isRecord(value))
|
|
97
|
+
return { refused: `${file} holds a line that is not ${noun.one}` };
|
|
98
|
+
objects.push(value);
|
|
90
99
|
}
|
|
91
|
-
return {
|
|
100
|
+
return { lines: objects };
|
|
92
101
|
}
|
|
93
102
|
/** The warning for a participant whose live items were written in full on two writes in a row. */
|
|
94
103
|
function rewrittenEvent(stream, position) {
|
package/dist/run/pricing.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ export interface DesktopResourceRate {
|
|
|
50
50
|
usdPerGiBSecond: number;
|
|
51
51
|
asOf: string;
|
|
52
52
|
source: string;
|
|
53
|
+
/** The sizes the public sheet prices; a size outside them stays unpriced. Absent: any size. */
|
|
54
|
+
sheet?: {
|
|
55
|
+
cpuCounts: readonly number[];
|
|
56
|
+
minMemoryMiB: number;
|
|
57
|
+
maxMemoryMiB: number;
|
|
58
|
+
};
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* The token-derived cost estimate for one actor trace. `estimatedCostUsd: null` = declared absent
|
|
@@ -104,6 +110,11 @@ export interface DesktopCostEstimate {
|
|
|
104
110
|
usdPerSecond?: number;
|
|
105
111
|
}
|
|
106
112
|
export declare const MODEL_RATES: Record<string, ModelRate>;
|
|
113
|
+
/**
|
|
114
|
+
* Modal's Sandbox rate, per vCPU: Modal prices a physical core, $0.00003942 a second, and counts
|
|
115
|
+
* it as two vCPU. Modal prices every size the same way.
|
|
116
|
+
*/
|
|
117
|
+
export declare const MODAL_SANDBOX_RESOURCE_RATE: DesktopResourceRate;
|
|
107
118
|
/**
|
|
108
119
|
* The refusal for a spend cap on a model this table cannot price. It names the priced models,
|
|
109
120
|
* since an npm install cannot edit this file.
|
package/dist/run/pricing.js
CHANGED
|
@@ -91,6 +91,19 @@ const DESKTOP_RESOURCE_RATE = {
|
|
|
91
91
|
asOf: "2026-09-05",
|
|
92
92
|
// A source label rather than an executable URL; runtime URL redaction protects E2B streams.
|
|
93
93
|
source: "e2b.dev/pricing",
|
|
94
|
+
// The public Hobby/Pro sheet stops at 8 CPU / 8 GiB; larger allocations may have negotiated
|
|
95
|
+
// prices, so their resource evidence is kept without extrapolating a standard rate to them.
|
|
96
|
+
sheet: { cpuCounts: [1, 2, 4, 6, 8], minMemoryMiB: 512, maxMemoryMiB: 8192 },
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Modal's Sandbox rate, per vCPU: Modal prices a physical core, $0.00003942 a second, and counts
|
|
100
|
+
* it as two vCPU. Modal prices every size the same way.
|
|
101
|
+
*/
|
|
102
|
+
export const MODAL_SANDBOX_RESOURCE_RATE = {
|
|
103
|
+
usdPerCpuSecond: 0.00003942 / 2,
|
|
104
|
+
usdPerGiBSecond: 0.00000667,
|
|
105
|
+
asOf: "2026-10-19",
|
|
106
|
+
source: "modal.com/pricing",
|
|
94
107
|
};
|
|
95
108
|
// Planning assumption behind the public estimateDesktopCost helper: stock desktops observed on
|
|
96
109
|
// 2026-09-05 had 8 vCPU / 8 GiB. This is the largest CPU/RAM combination on that public sheet, not
|
|
@@ -131,11 +144,11 @@ export function estimateAllocatedDesktopCost(minutes, resources, rate = DESKTOP_
|
|
|
131
144
|
minutes: round6(minutes),
|
|
132
145
|
};
|
|
133
146
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
147
|
+
const { sheet } = rate;
|
|
148
|
+
if ((sheet !== undefined &&
|
|
149
|
+
(!sheet.cpuCounts.includes(resources.cpuCount) ||
|
|
150
|
+
resources.memoryMiB < sheet.minMemoryMiB ||
|
|
151
|
+
resources.memoryMiB > sheet.maxMemoryMiB)) ||
|
|
139
152
|
!Number.isFinite(rate.usdPerCpuSecond) ||
|
|
140
153
|
rate.usdPerCpuSecond < 0 ||
|
|
141
154
|
!Number.isFinite(rate.usdPerGiBSecond) ||
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type E2BTagSearch } from "../substrates/e2b/sandbox.js";
|
|
2
2
|
import type { E2BAccount } from "../substrates/e2b/connection.js";
|
|
3
|
+
import { type SandboxProviderId } from "./sandbox-receipts.js";
|
|
4
|
+
/** The account reclaim reaches each provider through; absent when it could not be set up. */
|
|
5
|
+
export type ProviderAccounts = Partial<Record<SandboxProviderId, E2BAccount>>;
|
|
3
6
|
import { type ContainedRefusal, type PreparedOutputRoot } from "./contained-output.js";
|
|
4
7
|
export declare const RECLAIM_RESULT_SCHEMA = "humanish.reclaim-result.v1";
|
|
5
8
|
export declare const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
@@ -50,7 +53,7 @@ export interface Target {
|
|
|
50
53
|
* kills leaves a record of each one not yet confirmed gone.
|
|
51
54
|
*/
|
|
52
55
|
export declare function targetSet(options: {
|
|
53
|
-
|
|
56
|
+
accounts: ProviderAccounts;
|
|
54
57
|
check: boolean;
|
|
55
58
|
requestTimeoutMs: number;
|
|
56
59
|
earlier: ReclaimOutcome[];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// receipt writer. src/run/reclaim.ts decides what to find; this module records what happened.
|
|
5
5
|
import { REDACTED_SANDBOX_ID, redactText, sandboxIdDigest, toErrorMessage, } from "../evidence/redaction.js";
|
|
6
6
|
import { destroyE2BSandbox, findE2BSandboxesByTags, inspectE2BSandbox, } from "../substrates/e2b/sandbox.js";
|
|
7
|
+
import { isSandboxProviderId } from "./sandbox-receipts.js";
|
|
7
8
|
import { readContainedRegularFile, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
8
9
|
import { scrubSandboxIds } from "./sandbox-ids.js";
|
|
9
10
|
import { isRecord } from "./type-guards.js";
|
|
@@ -15,7 +16,7 @@ export const RECLAIM_RECEIPT_ARTIFACT = "reclaim-receipt.json";
|
|
|
15
16
|
* kills leaves a record of each one not yet confirmed gone.
|
|
16
17
|
*/
|
|
17
18
|
export function targetSet(options) {
|
|
18
|
-
const {
|
|
19
|
+
const { accounts, check, requestTimeoutMs, earlier, persist } = options;
|
|
19
20
|
const targets = new Map();
|
|
20
21
|
const work = [];
|
|
21
22
|
const queued = [];
|
|
@@ -25,7 +26,7 @@ export function targetSet(options) {
|
|
|
25
26
|
const seen = new Set(current.map((outcome) => outcome.sandboxIdDigest));
|
|
26
27
|
return [...current, ...carried.filter((outcome) => !seen.has(outcome.sandboxIdDigest))];
|
|
27
28
|
};
|
|
28
|
-
const act = (target) => actOn(
|
|
29
|
+
const act = (target) => actOn(accounts, target, { check, requestTimeoutMs }).then((settled) => {
|
|
29
30
|
target.outcome = settled;
|
|
30
31
|
void persist.write(snapshot(), "unknown");
|
|
31
32
|
});
|
|
@@ -95,17 +96,18 @@ export async function searchTagSets(e2b, tagSets, options) {
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
/** Kill (or look up) one sandbox, and say what happened in reclaim's vocabulary. */
|
|
98
|
-
async function actOn(
|
|
99
|
+
async function actOn(accounts, target, options) {
|
|
99
100
|
const { outcome, provider, rawId } = target;
|
|
100
|
-
|
|
101
|
+
const account = isSandboxProviderId(provider) ? accounts[provider] : undefined;
|
|
102
|
+
if (account === undefined)
|
|
101
103
|
return {
|
|
102
104
|
...outcome,
|
|
103
105
|
state: "unsupported-provider",
|
|
104
106
|
detail: `provider ${JSON.stringify(provider)} is not supported by this humanish version; nothing was attempted, and that provider's own timeout is the backstop`,
|
|
105
107
|
};
|
|
106
108
|
const attempt = options.check
|
|
107
|
-
? await inspectE2BSandbox(
|
|
108
|
-
: await destroyE2BSandbox(
|
|
109
|
+
? await inspectE2BSandbox(account, rawId, options)
|
|
110
|
+
: await destroyE2BSandbox(account, rawId, options);
|
|
109
111
|
const { detail: _previous, ...rest } = outcome;
|
|
110
112
|
// A provider error that quotes the id is scrubbed; the raw id stays in sandbox-receipts.ndjson.
|
|
111
113
|
return "detail" in attempt
|
package/dist/run/reclaim.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type E2BAccount } from "../substrates/e2b/connection.js";
|
|
1
2
|
import { type E2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
2
3
|
import { type StoppedSandboxCreates } from "./sandbox-creates.js";
|
|
3
4
|
import { RECLAIM_RECEIPT_ARTIFACT, RECLAIM_RESULT_SCHEMA, type ReclaimOutcome, type ReclaimState, type ReclaimTagSearch } from "./reclaim-outcomes.js";
|
|
@@ -33,13 +34,15 @@ export interface ReclaimResult {
|
|
|
33
34
|
export interface ReclaimHooks {
|
|
34
35
|
/** Tests inject a fake @e2b/desktop module; default loads the real one. */
|
|
35
36
|
loadModule?: () => Promise<E2BDesktopModule>;
|
|
37
|
+
/** Tests inject a fake Modal account; default reads the Modal credentials from `env`. */
|
|
38
|
+
loadModalAccount?: (env: Readonly<Record<string, string | undefined>>) => Promise<E2BAccount>;
|
|
36
39
|
/**
|
|
37
40
|
* The env whose E2B_API_KEY and E2B_DOMAIN every E2B call carries. Defaults to process.env, where
|
|
38
41
|
* the CLI put the run's key before the run started.
|
|
39
42
|
*/
|
|
40
43
|
env?: Readonly<Record<string, string | undefined>>;
|
|
41
44
|
requestTimeoutMs?: number;
|
|
42
|
-
/** Ask
|
|
45
|
+
/** Ask the provider whether each sandbox still exists, and kill nothing. */
|
|
43
46
|
check?: boolean;
|
|
44
47
|
/** The signal handler's view of this run's creates: the ids they reported, and those still
|
|
45
48
|
* in flight, whose sandboxes are killed as their ids arrive. */
|