humanish 0.138.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.
- package/CHANGELOG.md +11 -0
- package/dist/routes/computer-use/bundle-parts.d.ts +3 -0
- package/dist/routes/computer-use/bundle-parts.js +9 -1
- package/dist/routes/computer-use/bundle.js +3 -4
- package/dist/routes/computer-use/e2b-desktop/prepare.js +8 -4
- package/dist/routes/computer-use/fanout-bundle.js +3 -2
- package/dist/routes/computer-use/participant-deps.d.ts +1 -1
- package/dist/routes/computer-use/plan.js +66 -19
- package/dist/routes/computer-use/setup.js +14 -7
- package/dist/routes/computer-use/types.d.ts +3 -0
- package/dist/run/bundle-shape.js +1 -1
- package/dist/run/bundle.d.ts +3 -2
- package/dist/run/feedback-shape.js +1 -0
- package/dist/study/composition-rules.js +3 -2
- package/dist/study/parse/execution.js +2 -1
- package/dist/study/plan-base.d.ts +1 -0
- package/dist/study/plan-base.js +2 -0
- package/dist/study/plan-types.d.ts +2 -0
- package/dist/study/types.d.ts +1 -1
- package/dist/study/warnings.js +3 -1
- package/dist/substrates/e2b/sdk.d.ts +5 -0
- package/dist/substrates/e2b/sdk.js +12 -0
- package/dist/substrates/modal/commands.d.ts +16 -0
- package/dist/substrates/modal/commands.js +174 -0
- package/dist/substrates/modal/desktop.d.ts +16 -0
- package/dist/substrates/modal/desktop.js +195 -0
- package/dist/substrates/modal/image.d.ts +2 -0
- package/dist/substrates/modal/image.js +36 -1
- package/dist/substrates/modal/sandbox.d.ts +4 -0
- package/dist/substrates/modal/sandbox.js +68 -110
- package/docs/contracts/run-bundle.md +2 -1
- package/docs/contracts/schemas.md +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,17 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.139.0: Computer-use studies can run on Modal desktops (2026-10-19)
|
|
12
|
+
|
|
13
|
+
humanish 0.139.0 can run a computer-use study's desktops on Modal. Set
|
|
14
|
+
`execution.target: modal-desktop` on an `app-url` study and each participant gets its own Modal
|
|
15
|
+
sandbox running the same desktop the E2B route uses: Xvfb, XFCE, xdotool input, screenshots, a
|
|
16
|
+
live noVNC stream, Chromium and recordings. The same study file runs on E2B or Modal
|
|
17
|
+
with only the target changed. Clone and local-tree subjects, shared worlds, E2B templates and
|
|
18
|
+
camera or speech media stay on E2B.
|
|
19
|
+
|
|
20
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.139.0)
|
|
21
|
+
|
|
11
22
|
## 0.138.0: A terminal study can run on Modal (2026-10-19)
|
|
12
23
|
|
|
13
24
|
humanish 0.138.0 can run a terminal study's shell on Modal as well as E2B. Set
|
|
@@ -15,7 +15,10 @@ export declare function describeSubjectState(state: RunSubjectProvenance["state"
|
|
|
15
15
|
/** Human-readable provenance line for the single-participant subject.provenance event of a clone
|
|
16
16
|
* or local-tree subject: claims "cloned/packed and served" only when it actually happened. */
|
|
17
17
|
export declare function subjectProvenanceMessage(provenance: RunSubjectProvenance, publicAppUrl: string, dryRun: boolean, hasSession: boolean): string;
|
|
18
|
+
/** The label a run records for the desktop `execution.target` names. */
|
|
19
|
+
export declare function desktopSubstrate(target: string | undefined): "local-desktop" | "modal-desktop" | "e2b-desktop";
|
|
18
20
|
export declare function providerResourcesForOutcome(args: {
|
|
21
|
+
provider: "e2b-desktop" | "modal-desktop";
|
|
19
22
|
outcome: ParticipantRunOutcome | undefined;
|
|
20
23
|
createdAt: string;
|
|
21
24
|
ids: ParticipantIds;
|
|
@@ -73,6 +73,14 @@ export function subjectProvenanceMessage(provenance, publicAppUrl, dryRun, hasSe
|
|
|
73
73
|
}
|
|
74
74
|
return "Subject local-tree packing attempted; archive digest unresolved (provisioning failed before resolution)";
|
|
75
75
|
}
|
|
76
|
+
/** The label a run records for the desktop `execution.target` names. */
|
|
77
|
+
export function desktopSubstrate(target) {
|
|
78
|
+
return target === "local"
|
|
79
|
+
? "local-desktop"
|
|
80
|
+
: target === "modal-desktop"
|
|
81
|
+
? "modal-desktop"
|
|
82
|
+
: "e2b-desktop";
|
|
83
|
+
}
|
|
76
84
|
export function providerResourcesForOutcome(args) {
|
|
77
85
|
if (args.outcome?.sandboxId === undefined) {
|
|
78
86
|
return [];
|
|
@@ -80,7 +88,7 @@ export function providerResourcesForOutcome(args) {
|
|
|
80
88
|
return [
|
|
81
89
|
{
|
|
82
90
|
schema: "humanish.provider-resource.v1",
|
|
83
|
-
provider:
|
|
91
|
+
provider: args.provider,
|
|
84
92
|
kind: "sandbox",
|
|
85
93
|
id: args.outcome.sandboxId,
|
|
86
94
|
owner: "humanish",
|
|
@@ -6,7 +6,7 @@ import { judgeOneParticipant, judgeParticipants } from "../../run/judge.js";
|
|
|
6
6
|
import { renderReviewMarkdown } from "../../run/review-markdown.js";
|
|
7
7
|
import { desktopSpanToMinutes } from "../../run/cost-summary.js";
|
|
8
8
|
import { e2bDesktopTemplate } from "../../substrates/e2b/sandbox.js";
|
|
9
|
-
import { providerResourcesForOutcome } from "./bundle-parts.js";
|
|
9
|
+
import { desktopSubstrate, providerResourcesForOutcome } from "./bundle-parts.js";
|
|
10
10
|
import { participantFactsOf } from "./participant-facts.js";
|
|
11
11
|
import { buildCuaFanoutBundle } from "./fanout-bundle.js";
|
|
12
12
|
import { buildSingleParticipantBundle } from "./single-bundle.js";
|
|
@@ -96,9 +96,7 @@ function singleParticipantArgs(base, state) {
|
|
|
96
96
|
desktopRoute: base.plan.runner.desktop !== "in-process",
|
|
97
97
|
substrate: base.plan.runner.desktop === "in-process"
|
|
98
98
|
? "local-filesystem"
|
|
99
|
-
: plan.residual.execution?.target
|
|
100
|
-
? "local-desktop"
|
|
101
|
-
: "e2b-desktop",
|
|
99
|
+
: desktopSubstrate(plan.residual.execution?.target),
|
|
102
100
|
...(outcome?.desktopGeometry === undefined ? {} : { desktopGeometry: outcome.desktopGeometry }),
|
|
103
101
|
...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
|
|
104
102
|
...(outcome?.networkLog === undefined ? {} : { network: outcome.networkLog }),
|
|
@@ -127,6 +125,7 @@ function singleParticipantArgs(base, state) {
|
|
|
127
125
|
...(desktopTemplate === undefined ? {} : { desktopTemplate }),
|
|
128
126
|
...(outcome?.desktopBrowser === undefined ? {} : { desktopBrowser: outcome.desktopBrowser }),
|
|
129
127
|
providerResources: providerResourcesForOutcome({
|
|
128
|
+
provider: base.plan.residual.execution?.target === "modal-desktop" ? "modal-desktop" : "e2b-desktop",
|
|
130
129
|
outcome,
|
|
131
130
|
createdAt: base.run.createdAt,
|
|
132
131
|
ids: spec,
|
|
@@ -11,6 +11,7 @@ import { observeDesktopResources } from "../../../substrates/e2b/desktop-resourc
|
|
|
11
11
|
import { acquireE2BDesktopSandbox, e2bDesktopTemplate, withOneRetryOnTransientE2BError, } from "../../../substrates/e2b/sandbox.js";
|
|
12
12
|
import { loadE2BDesktopModule } from "../../../substrates/e2b/sdk.js";
|
|
13
13
|
import { e2bShell } from "../../../substrates/e2b/shell.js";
|
|
14
|
+
import { loadModalDesktopModule } from "../../../substrates/modal/sandbox.js";
|
|
14
15
|
import { attachReceivingInbox, participantCommsEnv, startCommsCatch } from "./comms.js";
|
|
15
16
|
import { participantSubjectEnv } from "../types.js";
|
|
16
17
|
import { plural } from "../../../run/text.js";
|
|
@@ -30,11 +31,14 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
30
31
|
// Off-app comms: the base-URL env is injected at sandbox create, so the app reads it at
|
|
31
32
|
// boot; the catch starts right after create.
|
|
32
33
|
const commsEnv = participantCommsEnv(ctx.comms);
|
|
33
|
-
const
|
|
34
|
+
const provider = deps.sandboxProvider ?? "e2b";
|
|
35
|
+
const desktopModule = await (deps.desktopModule ??
|
|
36
|
+
(provider === "modal" ? () => loadModalDesktopModule(env) : loadE2BDesktopModule))();
|
|
34
37
|
// An explicit template wins. Speech gets the versioned media image; ordinary
|
|
35
|
-
// browser studies retain the SDK default desktop.
|
|
38
|
+
// browser studies retain the SDK default desktop. A Modal desktop has one image.
|
|
36
39
|
const acquired = await acquireE2BDesktopSandbox({
|
|
37
40
|
module: desktopModule,
|
|
41
|
+
provider,
|
|
38
42
|
options: {
|
|
39
43
|
apiKey: deps.e2bApiKey,
|
|
40
44
|
requestTimeoutMs: deps.requestTimeoutMs,
|
|
@@ -67,7 +71,7 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
67
71
|
dpi: 96,
|
|
68
72
|
lifecycle: { onTimeout: "kill" },
|
|
69
73
|
},
|
|
70
|
-
template: e2bDesktopTemplate(residual),
|
|
74
|
+
template: provider === "modal" ? undefined : e2bDesktopTemplate(residual),
|
|
71
75
|
retry: {
|
|
72
76
|
// The default loader reclaims an acquired handle before retrying failed desktop startup.
|
|
73
77
|
// Its error names the cleanup outcome; pre-construction allocation failures remain unowned.
|
|
@@ -92,7 +96,7 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
92
96
|
state.sandboxId = acquired.allocation.resourceId;
|
|
93
97
|
// The billed span starts the instant the sandbox exists.
|
|
94
98
|
state.sandboxCreatedAtMs = deps.now();
|
|
95
|
-
const desktopResources = await observeDesktopResources(desktop);
|
|
99
|
+
const desktopResources = await observeDesktopResources(desktop, undefined, provider === "modal" ? "modal.create" : "e2b.getInfo");
|
|
96
100
|
state.desktopResources = desktopResources;
|
|
97
101
|
if ("reason" in desktopResources) {
|
|
98
102
|
warnings.push(`Desktop resource size unavailable (${desktopResources.reason}); compute cost remains unpriced.`);
|
|
@@ -5,7 +5,7 @@ import { cuaGoalSource } from "../../actors/goal-source.js";
|
|
|
5
5
|
import { redactEncodedText } from "../../evidence/encoded-text.js";
|
|
6
6
|
import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
|
|
7
7
|
import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, skippedSentence, withCuaReviewProvenance, } from "../../run/outcomes.js";
|
|
8
|
-
import { providerResourcesForOutcome, publicSafeAppUrlLabel, streakGaps } from "./bundle-parts.js";
|
|
8
|
+
import { desktopSubstrate, providerResourcesForOutcome, publicSafeAppUrlLabel, streakGaps, } from "./bundle-parts.js";
|
|
9
9
|
import { subjectName } from "../../run/subject-name.js";
|
|
10
10
|
import { participantFeedbackCandidates } from "./participant-feedback.js";
|
|
11
11
|
import { participantFactsOf } from "./participant-facts.js";
|
|
@@ -179,7 +179,7 @@ function fanoutFeedbackCandidates(args) {
|
|
|
179
179
|
runId: args.run.runId,
|
|
180
180
|
scenarioId: `cua-${plan.studyId}`,
|
|
181
181
|
adapterId: plan.studyId,
|
|
182
|
-
substrate: plan.residual.execution?.target
|
|
182
|
+
substrate: desktopSubstrate(plan.residual.execution?.target),
|
|
183
183
|
participants: specs.map((spec, index) => {
|
|
184
184
|
const outcome = outcomes?.[index];
|
|
185
185
|
return {
|
|
@@ -241,6 +241,7 @@ export function buildCuaFanoutBundle(args) {
|
|
|
241
241
|
const desktopTemplate = e2bDesktopTemplate(plan.residual);
|
|
242
242
|
const desktopBrowser = fanoutDesktopBrowser(args);
|
|
243
243
|
const providerResources = (outcomes ?? []).flatMap((outcome) => providerResourcesForOutcome({
|
|
244
|
+
provider: plan.residual.execution?.target === "modal-desktop" ? "modal-desktop" : "e2b-desktop",
|
|
244
245
|
outcome,
|
|
245
246
|
createdAt: args.run.createdAt,
|
|
246
247
|
ids: outcome.spec,
|
|
@@ -36,7 +36,7 @@ interface DesktopRunFacts {
|
|
|
36
36
|
}
|
|
37
37
|
/** The fields one desktop route sets and the other does not. Undefined leaves a field out. */
|
|
38
38
|
type DesktopRouteOverrides = {
|
|
39
|
-
[K in "createDesktop" | "createProvider" | "inProcessExecutor" | "localTreeArchiveBuffer" | "desktopFiles" | "externalComms" | "receiving" | "screenMismatchPolicy"]?: CuaParticipantDeps[K] | undefined;
|
|
39
|
+
[K in "createDesktop" | "createProvider" | "inProcessExecutor" | "localTreeArchiveBuffer" | "desktopFiles" | "externalComms" | "receiving" | "screenMismatchPolicy" | "sandboxProvider"]?: CuaParticipantDeps[K] | undefined;
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* One run's participant deps. Call it once per run: the run budget it creates is the one ledger
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// in its order and with its codes, then the plan the run uses. What stays in the route reads
|
|
3
3
|
// external state: committed personas, a rerun's source run, keys, the local agent, subject env
|
|
4
4
|
// values, spend-cap prices and the comms catch.
|
|
5
|
+
import { MODAL_MAX_SANDBOX_MS } from "../../substrates/modal/sdk.js";
|
|
5
6
|
import { DESKTOP_FILES_ROUTE_REASON } from "../../study/desktop-files.js";
|
|
6
7
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
7
8
|
import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
@@ -206,12 +207,14 @@ function startsOf(config, runner, limits, ceilingMs) {
|
|
|
206
207
|
const declared = config.execution?.concurrency;
|
|
207
208
|
const n = runner.participants.length;
|
|
208
209
|
const hosted = runner.desktop === "e2b-desktop";
|
|
210
|
+
// The E2B plan's limit binds only E2B desktops; Modal's container limit is far above a study's.
|
|
211
|
+
const e2bHosted = hosted && runner.provider === "e2b";
|
|
209
212
|
const atOnce = participantsAtOnce({
|
|
210
213
|
concurrency: boundedConcurrency(declared, n),
|
|
211
214
|
declared: declared !== undefined,
|
|
212
215
|
participants: n,
|
|
213
216
|
}, {
|
|
214
|
-
...(
|
|
217
|
+
...(e2bHosted ? { sandboxes: limits.concurrentSandboxes } : {}),
|
|
215
218
|
cap: limits.concurrencyCap,
|
|
216
219
|
});
|
|
217
220
|
if (!atOnce.ok)
|
|
@@ -265,6 +268,54 @@ function timeZoneRunnerReason(desktop, participants) {
|
|
|
265
268
|
? `${declared} This study runs on the local browser runtime. Remove actor.clock and participants[].clock, or set execution.target: e2b-desktop.`
|
|
266
269
|
: `${declared} This study drives your own browser through RunStudyOptions.inProcess. Remove actor.clock and participants[].clock, and set the zone in that browser yourself, or run on a hosted desktop.`;
|
|
267
270
|
}
|
|
271
|
+
/** Who hosts the study's desktops and the longest sandbox they allow, or why it cannot run. */
|
|
272
|
+
function hostingOf(config, e2bCeiling) {
|
|
273
|
+
if (config.execution?.target !== "modal-desktop")
|
|
274
|
+
return e2bCeiling.ok
|
|
275
|
+
? { provider: "e2b", ceiling: e2bCeiling }
|
|
276
|
+
: { refusal: e2bCeiling.message };
|
|
277
|
+
const refusal = modalDesktopRefusal(config);
|
|
278
|
+
return refusal === undefined
|
|
279
|
+
? { provider: "modal", ceiling: { ms: MODAL_MAX_SANDBOX_MS } }
|
|
280
|
+
: { refusal };
|
|
281
|
+
}
|
|
282
|
+
/** The app-url subject the study names, and the subject a hosted desktop provisions. */
|
|
283
|
+
function subjectsOf(config) {
|
|
284
|
+
const appUrl = config.subject.appUrl ?? "";
|
|
285
|
+
const declaredServeUrl = config.subject.serve?.url;
|
|
286
|
+
const serveUrl = declaredServeUrl === undefined ? {} : { serveUrl: declaredServeUrl };
|
|
287
|
+
const appUrlSubject = {
|
|
288
|
+
kind: "app-url",
|
|
289
|
+
appUrl,
|
|
290
|
+
publicTargets: config.policies?.allowPublicTargets === true,
|
|
291
|
+
...serveUrl,
|
|
292
|
+
};
|
|
293
|
+
const product = config.subject.product;
|
|
294
|
+
const hosted = config.subject.source === "desktop-cli" && product !== undefined
|
|
295
|
+
? { kind: "desktop-cli", product, ...serveUrl }
|
|
296
|
+
: (provisionedSubject(config) ?? appUrlSubject);
|
|
297
|
+
return { appUrl, appUrlSubject, hosted };
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* What a study on a Modal desktop cannot have that one on an E2B desktop can, or undefined. The
|
|
301
|
+
* Modal desktop runs an app-url subject in Chromium; provisioned subjects, a terminal subject,
|
|
302
|
+
* E2B templates and the speech template are E2B's.
|
|
303
|
+
*/
|
|
304
|
+
function modalDesktopRefusal(config) {
|
|
305
|
+
const desktop = config.execution?.desktop;
|
|
306
|
+
if (config.subject.source !== "app-url")
|
|
307
|
+
return `A Modal desktop runs app-url subjects only, and this study's subject is ${config.subject.source}. Set execution.target: e2b-desktop.`;
|
|
308
|
+
if (config.route === "shared-world")
|
|
309
|
+
return "A shared-world study runs on E2B desktops only. Set execution.target: e2b-desktop.";
|
|
310
|
+
if (desktop?.template !== undefined)
|
|
311
|
+
return "execution.desktop.template names an E2B template, which a Modal desktop cannot use. Remove it, or set execution.target: e2b-desktop.";
|
|
312
|
+
if (desktop?.media !== undefined)
|
|
313
|
+
return "execution.desktop.media needs the E2B speech or camera template, which a Modal desktop does not have. Remove it, or set execution.target: e2b-desktop.";
|
|
314
|
+
const browser = desktop?.browser;
|
|
315
|
+
if (browser !== undefined && browser !== "default" && browser !== "chromium")
|
|
316
|
+
return `A Modal desktop has Chromium only, and execution.desktop.browser is ${browser}. Set it to chromium or default, or set execution.target: e2b-desktop.`;
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
268
319
|
/** What the participants declare that this runner cannot do, with its code, or undefined. */
|
|
269
320
|
function runnerRefusal(desktop, participants) {
|
|
270
321
|
const files = desktopFilesRunnerReason(desktop, participants);
|
|
@@ -304,9 +355,10 @@ export function planComputerUseStudy(config, input) {
|
|
|
304
355
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
305
356
|
return refuse("in-scope", "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
306
357
|
const actor = descriptor.id;
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID",
|
|
358
|
+
const hosting = hostingOf(config, input.sandboxCeiling);
|
|
359
|
+
if ("refusal" in hosting)
|
|
360
|
+
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", hosting.refusal, actor);
|
|
361
|
+
const { provider, ceiling } = hosting;
|
|
310
362
|
const rejection = cuaStudyRejection(config, hasRunSession, driving, declaredSubjectRoute(config, driving), ceiling.ms);
|
|
311
363
|
if (rejection)
|
|
312
364
|
return refuse("in-scope", rejection.code, rejection.message, actor);
|
|
@@ -335,19 +387,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
335
387
|
return refuse("after-personas", "HUMANISH_COMPUTER_USE_FANOUT_INVALID", "Fan-out to more than one participant is not supported on the in-process route (RunStudyOptions.inProcess): fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Run a single in-process participant, or fan out on the E2B route.", actor);
|
|
336
388
|
if (first === undefined)
|
|
337
389
|
throw new Error("computerUseParticipants returned no participant");
|
|
338
|
-
const appUrl = config
|
|
339
|
-
const declaredServeUrl = config.subject.serve?.url;
|
|
340
|
-
const serveUrl = declaredServeUrl === undefined ? {} : { serveUrl: declaredServeUrl };
|
|
341
|
-
const appUrlSubject = {
|
|
342
|
-
kind: "app-url",
|
|
343
|
-
appUrl,
|
|
344
|
-
publicTargets: config.policies?.allowPublicTargets === true,
|
|
345
|
-
...serveUrl,
|
|
346
|
-
};
|
|
347
|
-
const product = config.subject.product;
|
|
348
|
-
const hosted = source === "desktop-cli" && product !== undefined
|
|
349
|
-
? { kind: "desktop-cli", product, ...serveUrl }
|
|
350
|
-
: (provisionedSubject(config) ?? appUrlSubject);
|
|
390
|
+
const { appUrl, appUrlSubject, hosted } = subjectsOf(config);
|
|
351
391
|
const brain = brainOf(config, driving.createProvider);
|
|
352
392
|
const runner = driving.inProcess
|
|
353
393
|
? {
|
|
@@ -358,7 +398,13 @@ export function planComputerUseStudy(config, input) {
|
|
|
358
398
|
}
|
|
359
399
|
: isLocalBrowserStudy(config)
|
|
360
400
|
? { desktop: "local-vm", brain, participants: [first, ...rest], subject: appUrlSubject }
|
|
361
|
-
: {
|
|
401
|
+
: {
|
|
402
|
+
desktop: "e2b-desktop",
|
|
403
|
+
provider,
|
|
404
|
+
brain,
|
|
405
|
+
participants: [first, ...rest],
|
|
406
|
+
subject: hosted,
|
|
407
|
+
};
|
|
362
408
|
const unsupported = runnerRefusal(runner.desktop, participants);
|
|
363
409
|
if (unsupported)
|
|
364
410
|
return refuse("in-scope", unsupported.code, unsupported.message, actor);
|
|
@@ -382,7 +428,8 @@ export function planComputerUseStudy(config, input) {
|
|
|
382
428
|
requirements: base.dryRun || runner.desktop === "in-process"
|
|
383
429
|
? []
|
|
384
430
|
: desktopRequirements(config, {
|
|
385
|
-
e2b: runner.desktop === "e2b-desktop",
|
|
431
|
+
e2b: runner.desktop === "e2b-desktop" && runner.provider === "e2b",
|
|
432
|
+
modal: runner.desktop === "e2b-desktop" && runner.provider === "modal",
|
|
386
433
|
brain,
|
|
387
434
|
localVm: runner.desktop === "local-vm",
|
|
388
435
|
externalCatch: !provisioned,
|
|
@@ -125,13 +125,8 @@ export async function admitCuaRun(plan, input, config) {
|
|
|
125
125
|
// Read keys once into locals (names only; values never logged or persisted).
|
|
126
126
|
const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
|
|
127
127
|
const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
const secrets = new RunSecrets([
|
|
131
|
-
openaiApiKey,
|
|
132
|
-
e2bApiKey,
|
|
133
|
-
...subjectEnvNames.map((name) => env[name] ?? ""),
|
|
134
|
-
]);
|
|
128
|
+
// Every known key and subject value, scrubbed literally, in dry and in-process runs too.
|
|
129
|
+
const secrets = new RunSecrets([openaiApiKey, e2bApiKey, ...otherValues(env, subjectEnvNames)]);
|
|
135
130
|
sanitizeParticipantRuns(participantRuns, secrets.scrub);
|
|
136
131
|
const desktopFiles = await readStudyDesktopFiles(input.cwd, plan, participantRuns, secrets.scrub);
|
|
137
132
|
if (!desktopFiles.ok)
|
|
@@ -342,6 +337,7 @@ function cuaParticipantDeps(plan, input, admitted, run) {
|
|
|
342
337
|
prepareDesktop: input.prepareDesktop,
|
|
343
338
|
}, {
|
|
344
339
|
createDesktop: input.localVm?.desktop,
|
|
340
|
+
sandboxProvider: plan.runner.desktop === "e2b-desktop" ? plan.runner.provider : undefined,
|
|
345
341
|
...callerDriving(input, config),
|
|
346
342
|
localTreeArchiveBuffer: admitted.localTreeArchiveBuffer,
|
|
347
343
|
desktopFiles: admitted.desktopFiles.size === 0 ? undefined : admitted.desktopFiles,
|
|
@@ -373,3 +369,14 @@ function subjectPhaseReporter(input) {
|
|
|
373
369
|
input.emit?.(phaseEvent(event, { kind: "participant", participant }));
|
|
374
370
|
};
|
|
375
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* The Modal token pair's values, which a provider error can quote into run warnings, and the
|
|
374
|
+
* subject env values the study names.
|
|
375
|
+
*/
|
|
376
|
+
function otherValues(env, subjectEnv) {
|
|
377
|
+
return [
|
|
378
|
+
env.MODAL_TOKEN_ID?.trim() ?? "",
|
|
379
|
+
env.MODAL_TOKEN_SECRET?.trim() ?? "",
|
|
380
|
+
...subjectEnv.map((name) => env[name] ?? ""),
|
|
381
|
+
];
|
|
382
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SandboxProviderId } from "../../run/sandbox-receipts.js";
|
|
1
2
|
import type { Verdict } from "../../run/judge.js";
|
|
2
3
|
import type { HostSuspensionReading } from "../../run/host-suspension.js";
|
|
3
4
|
import type { LocalCapacity } from "../../substrates/local/capacity.js";
|
|
@@ -344,6 +345,8 @@ export interface E2BDesktopDeps {
|
|
|
344
345
|
now: () => number;
|
|
345
346
|
/** Loads the E2B SDK for the participant's desktop. Defaults to loadE2BDesktopModule. */
|
|
346
347
|
desktopModule?: () => Promise<E2BDesktopModule>;
|
|
348
|
+
/** Who hosts the participant's desktop; E2B when unset. */
|
|
349
|
+
sandboxProvider?: SandboxProviderId;
|
|
347
350
|
/** The caller's prepareDesktop, called with the participant as its target. */
|
|
348
351
|
prepareDesktop?: NonNullable<RunStudyHomes["prepareDesktop"]>;
|
|
349
352
|
/** Clock and sleep for detached provisioning steps. */
|
package/dist/run/bundle-shape.js
CHANGED
|
@@ -93,7 +93,7 @@ export function isRunOutcome(value) {
|
|
|
93
93
|
function isRunProviderResource(value) {
|
|
94
94
|
return (isRecord(value) &&
|
|
95
95
|
value.schema === "humanish.provider-resource.v1" &&
|
|
96
|
-
value.provider === "e2b-desktop" &&
|
|
96
|
+
(value.provider === "e2b-desktop" || value.provider === "modal-desktop") &&
|
|
97
97
|
value.kind === "sandbox" &&
|
|
98
98
|
typeof value.id === "string" &&
|
|
99
99
|
value.id.trim().length > 0 &&
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface RunFeedbackCandidate {
|
|
|
53
53
|
scenario_id: string;
|
|
54
54
|
persona_id: string;
|
|
55
55
|
actor: "codex-tui" | "codex-exec" | "codex-app-server" | "computer-use" | "synthetic-dry-run" | "unknown";
|
|
56
|
-
substrate: "e2b-desktop" | "local-desktop" | "e2b-terminal" | "local-filesystem" | "codex-app-server" | "unknown";
|
|
56
|
+
substrate: "e2b-desktop" | "modal-desktop" | "local-desktop" | "e2b-terminal" | "local-filesystem" | "codex-app-server" | "unknown";
|
|
57
57
|
failure_owner: "harness" | "target-app" | "actor" | "environment" | "unknown";
|
|
58
58
|
summary: string;
|
|
59
59
|
expected: string;
|
|
@@ -479,7 +479,8 @@ export interface RunCostSummary {
|
|
|
479
479
|
}
|
|
480
480
|
export interface RunProviderResource {
|
|
481
481
|
schema: "humanish.provider-resource.v1";
|
|
482
|
-
provider:
|
|
482
|
+
/** The hosted desktop's provider: E2B, or Modal for `execution.target: modal-desktop`. */
|
|
483
|
+
provider: "e2b-desktop" | "modal-desktop";
|
|
483
484
|
kind: "sandbox";
|
|
484
485
|
/**
|
|
485
486
|
* "[redacted-sandbox-id]" in runs from 0.110 on; earlier runs recorded the raw id here. The raw id
|
|
@@ -111,8 +111,9 @@ function appUrlValidationReason(config) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
else {
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const target = config.execution?.target;
|
|
115
|
+
if (target !== "e2b-desktop" && target !== "modal-desktop" && target !== "local") {
|
|
116
|
+
return "app-url computer-use subjects require `execution.target: local`, `e2b-desktop` or `modal-desktop`.";
|
|
116
117
|
}
|
|
117
118
|
if (!actorResolvesToComputerUse(type)) {
|
|
118
119
|
return `actor.type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
|
|
@@ -17,8 +17,9 @@ export function parseExecution(raw) {
|
|
|
17
17
|
if (target !== "local" &&
|
|
18
18
|
target !== "e2b-desktop" &&
|
|
19
19
|
target !== "e2b-terminal" &&
|
|
20
|
+
target !== "modal-desktop" &&
|
|
20
21
|
target !== "modal-terminal") {
|
|
21
|
-
return invalid("`execution.target` must be local, e2b-desktop, e2b-terminal, or modal-terminal.");
|
|
22
|
+
return invalid("`execution.target` must be local, e2b-desktop, e2b-terminal, modal-desktop, or modal-terminal.");
|
|
22
23
|
}
|
|
23
24
|
execution.target = target;
|
|
24
25
|
}
|
|
@@ -7,6 +7,7 @@ export declare function provisionedSubject(config: StudyConfig): ProvisionedSubj
|
|
|
7
7
|
/** Keys, env and local tools a live run checks right before it acquires anything. */
|
|
8
8
|
export declare function desktopRequirements(config: StudyConfig, args: {
|
|
9
9
|
e2b: boolean;
|
|
10
|
+
modal?: boolean;
|
|
10
11
|
brain: Brain;
|
|
11
12
|
localVm: boolean;
|
|
12
13
|
externalCatch: boolean;
|
package/dist/study/plan-base.js
CHANGED
|
@@ -73,6 +73,8 @@ export function desktopRequirements(config, args) {
|
|
|
73
73
|
const requirements = [];
|
|
74
74
|
if (args.e2b)
|
|
75
75
|
requirements.push({ kind: "key", name: "E2B_API_KEY" });
|
|
76
|
+
if (args.modal === true)
|
|
77
|
+
requirements.push({ kind: "key", name: "MODAL_TOKEN_ID" }, { kind: "key", name: "MODAL_TOKEN_SECRET" });
|
|
76
78
|
if (args.localVm)
|
|
77
79
|
requirements.push({ kind: "local-vm" });
|
|
78
80
|
if (args.brain.kind === "openai")
|
|
@@ -133,6 +133,8 @@ export interface AppUrlSubject {
|
|
|
133
133
|
*/
|
|
134
134
|
export type ComputerUseRunner = {
|
|
135
135
|
readonly desktop: "e2b-desktop";
|
|
136
|
+
/** Who hosts the desktop: E2B, or Modal for `execution.target: modal-desktop`. */
|
|
137
|
+
readonly provider: SandboxProviderId;
|
|
136
138
|
readonly brain: Brain;
|
|
137
139
|
readonly participants: NonEmpty<ComputerUseParticipant>;
|
|
138
140
|
readonly subject: AppUrlSubject | ProvisionedSubject | {
|
package/dist/study/types.d.ts
CHANGED
|
@@ -467,7 +467,7 @@ export interface StudyActor {
|
|
|
467
467
|
*/
|
|
468
468
|
dwell?: DwellWindow;
|
|
469
469
|
}
|
|
470
|
-
type StudyExecutionTarget = "local" | "e2b-desktop" | "e2b-terminal" | "modal-terminal";
|
|
470
|
+
type StudyExecutionTarget = "local" | "e2b-desktop" | "e2b-terminal" | "modal-desktop" | "modal-terminal";
|
|
471
471
|
/** Terminal transport: the captured non-interactive exec stream (stdin disabled). It is not an
|
|
472
472
|
* interactive duplex PTY; labeling captured exec output "pty" would be a claim/mechanism
|
|
473
473
|
* mismatch, so this route uses "exec-stream". */
|
package/dist/study/warnings.js
CHANGED
|
@@ -262,7 +262,9 @@ function routesOf(config) {
|
|
|
262
262
|
terminal: isTerminalProductComposition(config),
|
|
263
263
|
shared,
|
|
264
264
|
externalPublic: shared && config.subject.source === "app-url",
|
|
265
|
-
hostedCuaBrowser: config.execution?.target === "e2b-desktop"
|
|
265
|
+
hostedCuaBrowser: (config.execution?.target === "e2b-desktop" ||
|
|
266
|
+
config.execution?.target === "modal-desktop") &&
|
|
267
|
+
cua,
|
|
266
268
|
createsE2BDesktop: (cua || (scripted && config.subject.source === "clone")) &&
|
|
267
269
|
config.execution?.target === "e2b-desktop",
|
|
268
270
|
};
|
|
@@ -211,6 +211,11 @@ export declare const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10000;
|
|
|
211
211
|
* the id.
|
|
212
212
|
*/
|
|
213
213
|
export declare function observeDesktopAllocation(options: E2BDesktopCreateOptions, observer: (sandboxId: string) => void): void;
|
|
214
|
+
/**
|
|
215
|
+
* Tell the observer registered for `options` that its sandbox exists, for a module that starts its
|
|
216
|
+
* own desktop after create, as the Modal one does. An observer that throws does not fail the create.
|
|
217
|
+
*/
|
|
218
|
+
export declare function reportDesktopAllocation(options: E2BDesktopCreateOptions, sandboxId: string): void;
|
|
214
219
|
type DesktopCreateCleanup = "killed" | "already_gone" | "unconfirmed";
|
|
215
220
|
/** Startup failed after this call acquired a handle. No credentials/options are included here. */
|
|
216
221
|
export declare class E2BDesktopStartupError extends Error {
|
|
@@ -31,6 +31,18 @@ const allocationObservers = new WeakMap();
|
|
|
31
31
|
export function observeDesktopAllocation(options, observer) {
|
|
32
32
|
allocationObservers.set(options, observer);
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Tell the observer registered for `options` that its sandbox exists, for a module that starts its
|
|
36
|
+
* own desktop after create, as the Modal one does. An observer that throws does not fail the create.
|
|
37
|
+
*/
|
|
38
|
+
export function reportDesktopAllocation(options, sandboxId) {
|
|
39
|
+
try {
|
|
40
|
+
allocationObservers.get(options)?.(sandboxId);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// The resolved handle reports the id as well.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
34
46
|
/** Startup failed after this call acquired a handle. No credentials/options are included here. */
|
|
35
47
|
export class E2BDesktopStartupError extends Error {
|
|
36
48
|
cleanup;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ReadableStream } from "node:stream/web";
|
|
2
|
+
import type { E2BCommandResult, E2BCommandRunOptions } from "../e2b/sdk.js";
|
|
3
|
+
import type { ModalSandbox } from "./sdk.js";
|
|
4
|
+
/** Modal takes sandbox and command timeouts in whole seconds; a shorter one is rounded up. */
|
|
5
|
+
export declare const wholeSeconds: (ms: number) => number;
|
|
6
|
+
/** Run `command` as the sandbox user, to completion or, with `background`, returning a handle. */
|
|
7
|
+
export declare function runCommand(sandbox: ModalSandbox, command: string, options?: E2BCommandRunOptions): Promise<E2BCommandResult>;
|
|
8
|
+
/** Relative paths are under the sandbox user's home, as on E2B. */
|
|
9
|
+
export declare function sandboxPath(path: string): string;
|
|
10
|
+
export declare function writeFile(sandbox: ModalSandbox, path: string, data: string | ArrayBuffer): Promise<{
|
|
11
|
+
path: string;
|
|
12
|
+
}>;
|
|
13
|
+
/** A file's bytes as a stream, read as the sandbox user. */
|
|
14
|
+
export declare function readFile(sandbox: ModalSandbox, path: string): Promise<ReadableStream<Uint8Array>>;
|
|
15
|
+
/** A file's bytes, whole. */
|
|
16
|
+
export declare function readBytes(sandbox: ModalSandbox, path: string): Promise<Uint8Array>;
|