humanish 0.115.0 → 0.116.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 +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
package/dist/study/warnings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cli } from "../cli/invocation.js";
|
|
2
2
|
import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
|
|
3
|
-
import { declaredParticipantIds } from "./plan-participants.js";
|
|
3
|
+
import { computerUseParticipants, declaredParticipantIds, sharedWorldParticipants, } from "./plan-participants.js";
|
|
4
4
|
import { addressedRecipients } from "./parse/comms.js";
|
|
5
5
|
import { participantList, participantInstruction, declaredParticipantCount, } from "./study-fields.js";
|
|
6
6
|
// The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
|
|
@@ -269,12 +269,11 @@ export function inertFieldPaths(config) {
|
|
|
269
269
|
/**
|
|
270
270
|
* The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
|
|
271
271
|
* terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
|
|
272
|
-
* study author would trust a list that blocks nothing.
|
|
273
|
-
* it, and the run records it in its bundle.
|
|
272
|
+
* study author would trust a list that blocks nothing.
|
|
274
273
|
* Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
|
|
275
274
|
* route.
|
|
276
275
|
*/
|
|
277
|
-
|
|
276
|
+
function egressAllowIgnoredWarning(config) {
|
|
278
277
|
if (config.execution?.egressAllow === undefined || config.route === "terminal")
|
|
279
278
|
return undefined;
|
|
280
279
|
return `\`execution.egressAllow\` is ignored on route: ${config.route}. Only route: terminal creates its sandbox with that outbound allowlist, and this route's browser desktops can reach any site. Remove the field: the first minor release on or after 2026-11-04 refuses it on every route except terminal.`;
|
|
@@ -289,9 +288,6 @@ export function forwardDeclaredWarnings(config) {
|
|
|
289
288
|
: [
|
|
290
289
|
`These fields are set, but no route reads them yet, so they have no effect: ${inert.join(", ")}.`,
|
|
291
290
|
];
|
|
292
|
-
const egressIgnored = egressAllowIgnoredWarning(config);
|
|
293
|
-
if (egressIgnored !== undefined)
|
|
294
|
-
warnings.push(egressIgnored);
|
|
295
291
|
// A declared cap below the participant count is legal but loud: the roster promises N live actors
|
|
296
292
|
// and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
|
|
297
293
|
// waves is otherwise indistinguishable from the all-live run the author meant.
|
|
@@ -312,7 +308,20 @@ export function forwardDeclaredWarnings(config) {
|
|
|
312
308
|
warnings.push(`comms.email covers ${covered.size} of ${participantIds.length} participants. These get no inbox and are never told one exists: ${uncovered.join(", ")}. Add a recipient with an address for each one whose flow needs email.`);
|
|
313
309
|
}
|
|
314
310
|
}
|
|
315
|
-
return [...warnings, ...
|
|
311
|
+
return [...warnings, ...recordedStudyWarnings(config)];
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The warnings about a study's own fields that a run records in its bundle as `study.warning`
|
|
315
|
+
* events: `execution.egressAllow` off the terminal route, and participant text that reads like a
|
|
316
|
+
* script. parseStudy reports them too, after its other warnings, so `study check` and every run
|
|
317
|
+
* print them.
|
|
318
|
+
*/
|
|
319
|
+
export function recordedStudyWarnings(config) {
|
|
320
|
+
const egressIgnored = egressAllowIgnoredWarning(config);
|
|
321
|
+
return [
|
|
322
|
+
...(egressIgnored === undefined ? [] : [egressIgnored]),
|
|
323
|
+
...scriptedMissionWarnings(config),
|
|
324
|
+
];
|
|
316
325
|
}
|
|
317
326
|
const UI_ACTION = /\b(?:click|tap|press|type\b[^.!?\n]*?\binto|select\b[^.!?\n]*?\bfrom)\b/gi;
|
|
318
327
|
function normalizedUrl(value) {
|
|
@@ -343,7 +352,7 @@ function actionChain(text) {
|
|
|
343
352
|
/\bthen\b/i.test(text.slice((actions[0]?.index ?? 0) + (actions[0]?.[0].length ?? 0)))));
|
|
344
353
|
}
|
|
345
354
|
/** Authored participant text is checked before runtime instructions or researcher criteria join it. */
|
|
346
|
-
|
|
355
|
+
function scriptedMissionWarnings(config) {
|
|
347
356
|
if (config.route !== "computer-use" && config.route !== "shared-world")
|
|
348
357
|
return [];
|
|
349
358
|
const fields = [
|
|
@@ -374,20 +383,32 @@ export function scriptedMissionWarnings(config) {
|
|
|
374
383
|
];
|
|
375
384
|
});
|
|
376
385
|
}
|
|
377
|
-
/**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
/**
|
|
387
|
+
* A warning for each participant whose persona has no background, which gives a participant a
|
|
388
|
+
* reason to act beyond the assigned task. `participants` are the records the planner builds: a
|
|
389
|
+
* computer-use or shared-world plan's participants, or `plannedParticipants` before a plan exists.
|
|
390
|
+
*/
|
|
391
|
+
export function personaBackgroundWarnings(studyId, participants, personas) {
|
|
392
|
+
return participants.flatMap(({ id, personaId }) => {
|
|
384
393
|
if (personaId && personas.get(personaId)?.background)
|
|
385
394
|
return [];
|
|
386
395
|
const reason = personaId
|
|
387
396
|
? `persona ${personaId} has no readable background`
|
|
388
397
|
: "no persona is assigned";
|
|
389
398
|
return [
|
|
390
|
-
`Participant ${id} has no persona background because ${reason}. Add a short, fictional background describing their experience and situation. Run ${cli(`study show ${
|
|
399
|
+
`Participant ${id} has no persona background because ${reason}. Add a short, fictional background describing their experience and situation. Run ${cli(`study show ${studyId} --json`)} to see what the participant receives.`,
|
|
391
400
|
];
|
|
392
401
|
});
|
|
393
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* The participant records planStudy builds for a study's declared participants, for `study check`
|
|
405
|
+
* and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
|
|
406
|
+
* `--count` is not applied.
|
|
407
|
+
*/
|
|
408
|
+
export function plannedParticipants(config) {
|
|
409
|
+
if (config.route === "computer-use")
|
|
410
|
+
return computerUseParticipants(config);
|
|
411
|
+
if (config.route === "shared-world")
|
|
412
|
+
return sharedWorldParticipants(config).participants;
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { E2BConnection, E2BDesktopModule } from "./sdk.js";
|
|
2
|
+
/** The connection an env names: E2B_API_KEY and E2B_DOMAIN, trimmed, each only when set. */
|
|
3
|
+
export declare function e2bConnection(env: Readonly<Record<string, string | undefined>>): E2BConnection;
|
|
4
|
+
/** The calls humanish makes on sandboxes that already exist. */
|
|
5
|
+
export type E2BAccount = Pick<E2BDesktopModule["Sandbox"], "kill" | "getInfo" | "list">;
|
|
6
|
+
/**
|
|
7
|
+
* The kill, getInfo and list of `module`, each carrying `connection`'s key and domain over the
|
|
8
|
+
* call's own options. `connection` may be a create's options, so the release of a sandbox reaches
|
|
9
|
+
* the account that created it. Each method is bound now: a hook that later replaces one on the
|
|
10
|
+
* shared module does not change this account. A method the installed SDK lacks stays absent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function e2bAccount(module: E2BDesktopModule, connection: E2BConnection): E2BAccount;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The connection an env names: E2B_API_KEY and E2B_DOMAIN, trimmed, each only when set. */
|
|
2
|
+
export function e2bConnection(env) {
|
|
3
|
+
return named({ apiKey: env.E2B_API_KEY?.trim(), domain: env.E2B_DOMAIN?.trim() });
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The kill, getInfo and list of `module`, each carrying `connection`'s key and domain over the
|
|
7
|
+
* call's own options. `connection` may be a create's options, so the release of a sandbox reaches
|
|
8
|
+
* the account that created it. Each method is bound now: a hook that later replaces one on the
|
|
9
|
+
* shared module does not change this account. A method the installed SDK lacks stays absent.
|
|
10
|
+
*/
|
|
11
|
+
export function e2bAccount(module, connection) {
|
|
12
|
+
const sdk = module.Sandbox;
|
|
13
|
+
const reached = named(connection);
|
|
14
|
+
const account = {};
|
|
15
|
+
if (typeof sdk.kill === "function") {
|
|
16
|
+
const kill = sdk.kill.bind(sdk);
|
|
17
|
+
account.kill = (sandboxId, options) => kill(sandboxId, { ...options, ...reached });
|
|
18
|
+
}
|
|
19
|
+
if (typeof sdk.getInfo === "function") {
|
|
20
|
+
const getInfo = sdk.getInfo.bind(sdk);
|
|
21
|
+
account.getInfo = (sandboxId, options) => getInfo(sandboxId, { ...options, ...reached });
|
|
22
|
+
}
|
|
23
|
+
if (typeof sdk.list === "function") {
|
|
24
|
+
const list = sdk.list.bind(sdk);
|
|
25
|
+
account.list = (options) => list({ ...options, ...reached });
|
|
26
|
+
}
|
|
27
|
+
return account;
|
|
28
|
+
}
|
|
29
|
+
/** Only the key and the domain, and only when non-empty: the SDK reads an empty one as unset. */
|
|
30
|
+
function named(values) {
|
|
31
|
+
return {
|
|
32
|
+
...(values.apiKey ? { apiKey: values.apiKey } : {}),
|
|
33
|
+
...(values.domain ? { domain: values.domain } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { CuaExecutor, CuaObservation } from "../../actors/computer-use/loop
|
|
|
6
6
|
* executor awaits every call, which is correct for both sync and async returns.
|
|
7
7
|
*/
|
|
8
8
|
export interface E2BDesktopLike {
|
|
9
|
-
/**
|
|
9
|
+
/** The sandbox's command surface: keypresses, held modifiers and typed text run xdotool here. */
|
|
10
10
|
commands?: {
|
|
11
11
|
run(command: string, options?: {
|
|
12
12
|
requestTimeoutMs?: number;
|
|
@@ -48,8 +48,6 @@ export interface E2BDesktopLike {
|
|
|
48
48
|
scroll(direction?: "up" | "down", amount?: number): Promise<void> | void;
|
|
49
49
|
/** Write text at the current cursor position (the SDK's typing method). */
|
|
50
50
|
write(text: string): Promise<void> | void;
|
|
51
|
-
/** Press a key or chord (the SDK's key method); accepts the keys array. */
|
|
52
|
-
press(key: string | string[]): Promise<void> | void;
|
|
53
51
|
/** Drag from one coordinate tuple to another. */
|
|
54
52
|
drag(from: [number, number], to: [number, number]): Promise<void> | void;
|
|
55
53
|
/** Wait for the given number of milliseconds. */
|
|
@@ -2,11 +2,11 @@ import { shellQuote } from "../shell.js";
|
|
|
2
2
|
import { perceptualSignature } from "../../evidence/frame-signature.js";
|
|
3
3
|
import { commandFailureInfo } from "../command-failure.js";
|
|
4
4
|
import { ComputerUseExecutorError } from "../../actors/computer-use/executor-error.js";
|
|
5
|
-
import { xdotoolHeldModifiers } from "../../guest/desktop-keys.js";
|
|
5
|
+
import { xdotoolChord, xdotoolHeldModifiers } from "../../guest/desktop-keys.js";
|
|
6
6
|
const DEFAULT_WAIT_MS = 500;
|
|
7
7
|
const DEFAULT_SCROLL_AMOUNT_PER_TICK = 100;
|
|
8
8
|
const TYPE_COMMAND_TIMEOUT_MS = 15_000;
|
|
9
|
-
const
|
|
9
|
+
const KEY_COMMAND_TIMEOUT_MS = 15_000;
|
|
10
10
|
const CURSOR_READ_TIMEOUT_MS = 500;
|
|
11
11
|
/**
|
|
12
12
|
* The stock image's xdotool waits ~15s on a synchronized move to its current position.
|
|
@@ -234,11 +234,15 @@ export function createE2BDesktopExecutor(desktop, options = {}) {
|
|
|
234
234
|
}
|
|
235
235
|
return;
|
|
236
236
|
}
|
|
237
|
-
case "keypress":
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
237
|
+
case "keypress": {
|
|
238
|
+
// One chord, so ctrl and a are pressed together. A name the table cannot map is
|
|
239
|
+
// refused here, before anything reaches the desktop.
|
|
240
|
+
const chord = xdotoolChord(action.keys);
|
|
241
|
+
if (!desktop.commands)
|
|
242
|
+
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
243
|
+
await runKeyCommand(desktop.commands, `key --clearmodifiers ${chord}`);
|
|
241
244
|
return;
|
|
245
|
+
}
|
|
242
246
|
case "drag": {
|
|
243
247
|
// The SDK drag takes two endpoints, not an N-point path: drag from the
|
|
244
248
|
// first to the last point. 0 points is a safe no-op; 1 point has no
|
|
@@ -279,11 +283,10 @@ async function withHeldModifiers(desktop, chord, run) {
|
|
|
279
283
|
const commands = desktop.commands;
|
|
280
284
|
if (!commands)
|
|
281
285
|
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
282
|
-
const options = { requestTimeoutMs: HELD_KEYS_TIMEOUT_MS, timeoutMs: HELD_KEYS_TIMEOUT_MS };
|
|
283
286
|
let failed = false;
|
|
284
287
|
let failure;
|
|
285
288
|
try {
|
|
286
|
-
await commands
|
|
289
|
+
await runKeyCommand(commands, `keydown ${chord}`);
|
|
287
290
|
await run();
|
|
288
291
|
}
|
|
289
292
|
catch (error) {
|
|
@@ -292,7 +295,7 @@ async function withHeldModifiers(desktop, chord, run) {
|
|
|
292
295
|
}
|
|
293
296
|
// A keydown that failed may still have pressed some keys, so the release always runs.
|
|
294
297
|
try {
|
|
295
|
-
await commands
|
|
298
|
+
await runKeyCommand(commands, `keyup ${chord}`);
|
|
296
299
|
}
|
|
297
300
|
catch {
|
|
298
301
|
throw new ComputerUseExecutorError("execution_failed", "outcome_uncertain");
|
|
@@ -300,3 +303,33 @@ async function withHeldModifiers(desktop, chord, run) {
|
|
|
300
303
|
if (failed)
|
|
301
304
|
throw failure;
|
|
302
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* A key command xdotool did not complete. The loop reads `exitCode` and `stderr` the way it reads
|
|
308
|
+
* the SDK's CommandExitError, so it records the action as a failed desktop command and skips it.
|
|
309
|
+
*/
|
|
310
|
+
class ComputerUseKeyError extends Error {
|
|
311
|
+
exitCode;
|
|
312
|
+
stderr;
|
|
313
|
+
constructor(exitCode, stderr) {
|
|
314
|
+
super(`key command failed (exit ${exitCode})`);
|
|
315
|
+
this.name = "ComputerUseKeyError";
|
|
316
|
+
this.exitCode = exitCode;
|
|
317
|
+
this.stderr = stderr;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Run an xdotool key command: `key`, `keydown` or `keyup` with a chord from the key table, whose
|
|
322
|
+
* names are plain shell words, so the command and its stderr hold no participant text. The SDK
|
|
323
|
+
* throws its own error on a non-zero exit. xdotool skips a name it cannot resolve with a "No such
|
|
324
|
+
* key name" warning and still exits 0, so that warning fails the command too.
|
|
325
|
+
*/
|
|
326
|
+
async function runKeyCommand(commands, command) {
|
|
327
|
+
const result = await commands.run(`xdotool ${command}`, {
|
|
328
|
+
requestTimeoutMs: KEY_COMMAND_TIMEOUT_MS,
|
|
329
|
+
timeoutMs: KEY_COMMAND_TIMEOUT_MS,
|
|
330
|
+
});
|
|
331
|
+
const exitCode = result.exitCode ?? 0;
|
|
332
|
+
const stderr = result.stderr ?? "";
|
|
333
|
+
if (exitCode !== 0 || stderr.includes("No such key name"))
|
|
334
|
+
throw new ComputerUseKeyError(exitCode, stderr);
|
|
335
|
+
}
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* raw provider 400 after a plan has already printed.
|
|
2
|
+
* The longest lifetime humanish asks E2B for, in milliseconds, and whether it is the default or
|
|
3
|
+
* the setting; or why the setting cannot be read.
|
|
5
4
|
*/
|
|
6
|
-
export
|
|
5
|
+
export type SandboxCeiling = {
|
|
6
|
+
readonly ok: true;
|
|
7
|
+
readonly ms: number;
|
|
8
|
+
readonly source: "default" | "setting";
|
|
9
|
+
} | {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The sandbox ceiling: HUMANISH_E2B_MAX_SANDBOX_MINUTES when set, else 60 minutes. E2B refuses a
|
|
15
|
+
* longer sandbox with a 400 ("Timeout cannot be greater than 1 hours" on Hobby), and neither its
|
|
16
|
+
* API nor its SDK says which plan a key is on, so the operator states it. A derived deadline has
|
|
17
|
+
* to stay under the ceiling, and saying so at plan time beats a provider 400 after the plan printed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sandboxCeiling(env: Readonly<Record<string, string | undefined>>): SandboxCeiling;
|
|
20
|
+
/** The ceiling in use and where it came from, or why the setting cannot be read (doctor's row). */
|
|
21
|
+
export declare function describeSandboxCeiling(ceiling: SandboxCeiling): string;
|
|
22
|
+
/**
|
|
23
|
+
* The sentence a plan refusal adds when a `deadlineMs` sandbox deadline passes the ceiling: the
|
|
24
|
+
* setting value that would admit it, when an E2B plan allows that long.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ceilingAdvice(deadlineMs: number): string;
|
|
7
27
|
/** Server-side reclamation buffer past a participant's own wall-clock stop. */
|
|
8
28
|
export declare const SANDBOX_TIMEOUT_BUFFER_MS: number;
|
|
9
29
|
/** Room a provisioned subject adds to the sandbox deadline for clone, install, build, start and probe. */
|
|
@@ -2,12 +2,49 @@
|
|
|
2
2
|
// deadline when they set a sandbox's server-side timeout. A dead host process can never orphan a
|
|
3
3
|
// sandbox past that timeout, so every route derives it from these values.
|
|
4
4
|
import { readPositiveInt } from "../../study/parse/values.js";
|
|
5
|
+
/** The setting that tells humanish how long the operator's E2B plan lets a sandbox live. */
|
|
6
|
+
const SANDBOX_CEILING_SETTING = "HUMANISH_E2B_MAX_SANDBOX_MINUTES";
|
|
7
|
+
/** E2B's Hobby plan ends a sandbox after 1 hour, so a key on any plan can use this ceiling. */
|
|
8
|
+
const DEFAULT_CEILING_MINUTES = 60;
|
|
9
|
+
/** E2B's Pro plan allows 24 hours, the longest lifetime E2B documents. */
|
|
10
|
+
const LARGEST_CEILING_MINUTES = 24 * 60;
|
|
5
11
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
12
|
+
* The sandbox ceiling: HUMANISH_E2B_MAX_SANDBOX_MINUTES when set, else 60 minutes. E2B refuses a
|
|
13
|
+
* longer sandbox with a 400 ("Timeout cannot be greater than 1 hours" on Hobby), and neither its
|
|
14
|
+
* API nor its SDK says which plan a key is on, so the operator states it. A derived deadline has
|
|
15
|
+
* to stay under the ceiling, and saying so at plan time beats a provider 400 after the plan printed.
|
|
9
16
|
*/
|
|
10
|
-
export
|
|
17
|
+
export function sandboxCeiling(env) {
|
|
18
|
+
const declared = env[SANDBOX_CEILING_SETTING]?.trim() ?? "";
|
|
19
|
+
if (declared === "")
|
|
20
|
+
return { ok: true, ms: DEFAULT_CEILING_MINUTES * 60_000, source: "default" };
|
|
21
|
+
const minutes = /^\d+$/.test(declared) ? Number.parseInt(declared, 10) : Number.NaN;
|
|
22
|
+
if (!(minutes >= 1 && minutes <= LARGEST_CEILING_MINUTES))
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
message: `${SANDBOX_CEILING_SETTING} must be a whole number of minutes from 1 to ${LARGEST_CEILING_MINUTES} (got "${declared.slice(0, 40)}"). Set it to the sandbox lifetime your E2B plan allows: ${DEFAULT_CEILING_MINUTES} on Hobby, ${LARGEST_CEILING_MINUTES} on Pro. Unset, it is ${DEFAULT_CEILING_MINUTES}.`,
|
|
26
|
+
};
|
|
27
|
+
return { ok: true, ms: minutes * 60_000, source: "setting" };
|
|
28
|
+
}
|
|
29
|
+
/** The ceiling in use and where it came from, or why the setting cannot be read (doctor's row). */
|
|
30
|
+
export function describeSandboxCeiling(ceiling) {
|
|
31
|
+
if (!ceiling.ok)
|
|
32
|
+
return ceiling.message;
|
|
33
|
+
const minutes = ceiling.ms / 60_000;
|
|
34
|
+
return ceiling.source === "setting"
|
|
35
|
+
? `${minutes} minutes, from ${SANDBOX_CEILING_SETTING}. A study whose sandbox deadline is longer is refused before any sandbox is created, and E2B refuses a lifetime past the plan's own limit.`
|
|
36
|
+
: `${minutes} minutes, E2B's Hobby limit. On a plan with longer sandboxes (Pro allows ${LARGEST_CEILING_MINUTES} minutes), set ${SANDBOX_CEILING_SETTING} to that plan's limit.`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The sentence a plan refusal adds when a `deadlineMs` sandbox deadline passes the ceiling: the
|
|
40
|
+
* setting value that would admit it, when an E2B plan allows that long.
|
|
41
|
+
*/
|
|
42
|
+
export function ceilingAdvice(deadlineMs) {
|
|
43
|
+
const needed = Math.ceil(deadlineMs / 60_000);
|
|
44
|
+
return needed <= LARGEST_CEILING_MINUTES
|
|
45
|
+
? ` If your E2B plan allows longer sandboxes (Pro allows ${LARGEST_CEILING_MINUTES} minutes), set ${SANDBOX_CEILING_SETTING} to ${needed} or more.`
|
|
46
|
+
: ` No E2B plan humanish knows allows a sandbox past ${LARGEST_CEILING_MINUTES} minutes.`;
|
|
47
|
+
}
|
|
11
48
|
/** Server-side reclamation buffer past a participant's own wall-clock stop. */
|
|
12
49
|
export const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
|
|
13
50
|
/** Room a provisioned subject adds to the sandbox deadline for clone, install, build, start and probe. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PreparedOutputRoot } from "../../run/contained-output.js";
|
|
2
2
|
import { type DesktopReleaseResult, type OwnedDesktopAllocation } from "../desktop-session.js";
|
|
3
3
|
import type { StudyConfig } from "../../study/types.js";
|
|
4
|
+
import { type E2BAccount } from "./connection.js";
|
|
4
5
|
import { type E2BDesktopCreateOptions, type E2BDesktopModule, type E2BDesktopSandbox, type E2BListedSandbox } from "./sdk.js";
|
|
5
6
|
/** Where an allocation's receipt goes: the run's prepared root, under a public-safe participant
|
|
6
7
|
* label. The receipt line saves it as `laneId`. */
|
|
@@ -59,7 +60,7 @@ export type E2BSandboxDestroyOutcome = {
|
|
|
59
60
|
};
|
|
60
61
|
/** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
|
|
61
62
|
* reclaim's persisted outcome. */
|
|
62
|
-
export declare function destroyE2BSandbox(
|
|
63
|
+
export declare function destroyE2BSandbox(account: E2BAccount, sandboxId: string, options: {
|
|
63
64
|
requestTimeoutMs: number;
|
|
64
65
|
}): Promise<E2BSandboxDestroyOutcome>;
|
|
65
66
|
/** What `reclaim --check` learned about one sandbox by its exact id. */
|
|
@@ -73,7 +74,7 @@ export type E2BSandboxPresence = {
|
|
|
73
74
|
* Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
|
|
74
75
|
* type, means gone; any other error leaves the answer open.
|
|
75
76
|
*/
|
|
76
|
-
export declare function inspectE2BSandbox(
|
|
77
|
+
export declare function inspectE2BSandbox(account: E2BAccount, sandboxId: string, options: {
|
|
77
78
|
requestTimeoutMs: number;
|
|
78
79
|
}): Promise<E2BSandboxPresence>;
|
|
79
80
|
/** What E2B listed for a run's owner tags. `sandboxes` holds exact matches only. */
|
|
@@ -87,7 +88,7 @@ export interface E2BTagSearch {
|
|
|
87
88
|
* checked against every tag again, so a server that ignored the filter still cannot hand back
|
|
88
89
|
* another run's sandbox, and the search stops after a few pages instead of walking the account.
|
|
89
90
|
*/
|
|
90
|
-
export declare function findE2BSandboxesByTags(
|
|
91
|
+
export declare function findE2BSandboxesByTags(account: E2BAccount, tags: Record<string, string>, options: {
|
|
91
92
|
requestTimeoutMs: number;
|
|
92
93
|
}): Promise<E2BTagSearch>;
|
|
93
94
|
/** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
|
|
@@ -11,6 +11,7 @@ import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
|
11
11
|
import { beginSandboxCreate, recordSandboxOwnerOnce, SandboxCreateRefusedError, sandboxOwnerTags, } from "../../run/sandbox-creates.js";
|
|
12
12
|
import { appendSandboxOwner, appendSandboxReceipt } from "../../run/sandbox-receipts.js";
|
|
13
13
|
import { ownDesktopAllocation, } from "../desktop-session.js";
|
|
14
|
+
import { e2bAccount } from "./connection.js";
|
|
14
15
|
import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, observeDesktopAllocation, } from "./sdk.js";
|
|
15
16
|
/** A desktop sandbox, on the stock `desktop` template or a named one. */
|
|
16
17
|
export function acquireE2BDesktopSandbox(request) {
|
|
@@ -67,7 +68,8 @@ async function acquire(request, template) {
|
|
|
67
68
|
: module.Sandbox.create(template, options);
|
|
68
69
|
}, { ...request.retry, canRetry: () => ticket?.stopping() !== true });
|
|
69
70
|
ticket?.created(sandbox.sandboxId);
|
|
70
|
-
|
|
71
|
+
// The release reaches the account the create used, whatever process.env names.
|
|
72
|
+
const allocation = ownE2BSandbox(e2bAccount(module, options), sandbox.sandboxId, ticket);
|
|
71
73
|
try {
|
|
72
74
|
// Best effort by contract: a failed write leaves the owner tags and the TTL as the
|
|
73
75
|
// backstops and never fails the participant. One line per sandbox: a receipt the guard's
|
|
@@ -90,12 +92,6 @@ async function acquire(request, template) {
|
|
|
90
92
|
ticket?.settled();
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
|
-
/** The provider's kill method bound to its class, or undefined when the SDK has none. */
|
|
94
|
-
function boundKill(module) {
|
|
95
|
-
return typeof module.Sandbox.kill === "function"
|
|
96
|
-
? module.Sandbox.kill.bind(module.Sandbox)
|
|
97
|
-
: undefined;
|
|
98
|
-
}
|
|
99
95
|
/**
|
|
100
96
|
* Kill one sandbox by exact id and read the answer in the release vocabulary both callers share.
|
|
101
97
|
* The SDK resolves false for an id it no longer knows (a 404), and a SandboxNotFoundError,
|
|
@@ -129,9 +125,8 @@ async function killById(kill, sandboxId, options) {
|
|
|
129
125
|
return { status: "unconfirmed", reason: "release_failed", error };
|
|
130
126
|
}
|
|
131
127
|
}
|
|
132
|
-
function ownE2BSandbox(
|
|
133
|
-
|
|
134
|
-
const kill = boundKill(module);
|
|
128
|
+
function ownE2BSandbox(account, resourceId, ticket) {
|
|
129
|
+
const { kill } = account;
|
|
135
130
|
return ownDesktopAllocation({
|
|
136
131
|
resourceId,
|
|
137
132
|
release: async () => {
|
|
@@ -189,8 +184,8 @@ export function releaseUnavailableDetail(detail) {
|
|
|
189
184
|
}
|
|
190
185
|
/** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
|
|
191
186
|
* reclaim's persisted outcome. */
|
|
192
|
-
export async function destroyE2BSandbox(
|
|
193
|
-
const released = await killById(
|
|
187
|
+
export async function destroyE2BSandbox(account, sandboxId, options) {
|
|
188
|
+
const released = await killById(account.kill, sandboxId, options);
|
|
194
189
|
if (released.status === "released")
|
|
195
190
|
return { state: released.reason === "terminated" ? "killed" : "already-gone" };
|
|
196
191
|
switch (released.reason) {
|
|
@@ -209,14 +204,14 @@ export async function destroyE2BSandbox(module, sandboxId, options) {
|
|
|
209
204
|
* Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
|
|
210
205
|
* type, means gone; any other error leaves the answer open.
|
|
211
206
|
*/
|
|
212
|
-
export async function inspectE2BSandbox(
|
|
213
|
-
if (
|
|
207
|
+
export async function inspectE2BSandbox(account, sandboxId, options) {
|
|
208
|
+
if (account.getInfo === undefined)
|
|
214
209
|
return {
|
|
215
210
|
state: "check-failed",
|
|
216
211
|
detail: "the installed @e2b/desktop SDK has no Sandbox.getInfo",
|
|
217
212
|
};
|
|
218
213
|
try {
|
|
219
|
-
await
|
|
214
|
+
await account.getInfo(sandboxId, options);
|
|
220
215
|
return { state: "running" };
|
|
221
216
|
}
|
|
222
217
|
catch (error) {
|
|
@@ -234,8 +229,8 @@ const TAG_SEARCH_MAX_PAGES = 3;
|
|
|
234
229
|
* checked against every tag again, so a server that ignored the filter still cannot hand back
|
|
235
230
|
* another run's sandbox, and the search stops after a few pages instead of walking the account.
|
|
236
231
|
*/
|
|
237
|
-
export async function findE2BSandboxesByTags(
|
|
238
|
-
if (
|
|
232
|
+
export async function findE2BSandboxesByTags(account, tags, options) {
|
|
233
|
+
if (account.list === undefined)
|
|
239
234
|
return {
|
|
240
235
|
status: "unavailable",
|
|
241
236
|
sandboxes: [],
|
|
@@ -243,7 +238,7 @@ export async function findE2BSandboxesByTags(module, tags, options) {
|
|
|
243
238
|
};
|
|
244
239
|
const sandboxes = [];
|
|
245
240
|
try {
|
|
246
|
-
const pages =
|
|
241
|
+
const pages = account.list({
|
|
247
242
|
query: { metadata: tags },
|
|
248
243
|
limit: TAG_SEARCH_PAGE_SIZE,
|
|
249
244
|
requestTimeoutMs: options.requestTimeoutMs,
|
|
@@ -15,18 +15,14 @@ export interface E2BDesktopModule {
|
|
|
15
15
|
* killed, false otherwise (the SDK's own doc comment). This boolean is the primary by-id
|
|
16
16
|
* cleanup proof: a caller never needs to re-list to confirm reclamation.
|
|
17
17
|
*/
|
|
18
|
-
kill?(sandboxId: string, options?:
|
|
19
|
-
requestTimeoutMs?: number;
|
|
20
|
-
}): Promise<boolean>;
|
|
18
|
+
kill?(sandboxId: string, options?: E2BRequestOptions): Promise<boolean>;
|
|
21
19
|
/**
|
|
22
20
|
* Fetch one sandbox by its exact id (never account-wide). Throws a SandboxNotFoundError-
|
|
23
21
|
* shaped error (see isSandboxNotFoundError below) when the id no longer exists; that thrown
|
|
24
22
|
* error is the by-id confirmation that a killed sandbox is gone. Optional: older SDKs may
|
|
25
23
|
* lack it, so callers fall back to kill()'s own boolean rather than ever calling Sandbox.list.
|
|
26
24
|
*/
|
|
27
|
-
getInfo?(sandboxId: string, options?:
|
|
28
|
-
requestTimeoutMs?: number;
|
|
29
|
-
}): Promise<E2BSandboxInfo>;
|
|
25
|
+
getInfo?(sandboxId: string, options?: E2BRequestOptions): Promise<E2BSandboxInfo>;
|
|
30
26
|
/**
|
|
31
27
|
* List sandboxes whose metadata matches every `query.metadata` pair, filtered server-side.
|
|
32
28
|
* Reclaim calls it only with a run's owner tags (sandboxOwnerTags in src/run/sandbox-creates.ts)
|
|
@@ -36,12 +32,23 @@ export interface E2BDesktopModule {
|
|
|
36
32
|
list?(options?: E2BSandboxListOptions): E2BSandboxPaginator;
|
|
37
33
|
};
|
|
38
34
|
}
|
|
39
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Which E2B account a call reaches. The SDK reads E2B_API_KEY and E2B_DOMAIN from process.env for
|
|
37
|
+
* a call that names neither, and an empty value counts as none.
|
|
38
|
+
*/
|
|
39
|
+
export interface E2BConnection {
|
|
40
|
+
apiKey?: string;
|
|
41
|
+
domain?: string;
|
|
42
|
+
}
|
|
43
|
+
/** The options of a call on a sandbox that exists: its account, and how long the request may take. */
|
|
44
|
+
interface E2BRequestOptions extends E2BConnection {
|
|
45
|
+
requestTimeoutMs?: number;
|
|
46
|
+
}
|
|
47
|
+
interface E2BSandboxListOptions extends E2BRequestOptions {
|
|
40
48
|
query?: {
|
|
41
49
|
metadata?: Record<string, string>;
|
|
42
50
|
};
|
|
43
51
|
limit?: number;
|
|
44
|
-
requestTimeoutMs?: number;
|
|
45
52
|
}
|
|
46
53
|
interface E2BSandboxPaginator {
|
|
47
54
|
readonly hasNext: boolean;
|
|
@@ -78,7 +85,11 @@ export interface E2BNetworkOptions {
|
|
|
78
85
|
};
|
|
79
86
|
}[]>;
|
|
80
87
|
}
|
|
81
|
-
|
|
88
|
+
/**
|
|
89
|
+
* A create names its account. e2bAccount (connection.ts) gives every later call on the sandbox the
|
|
90
|
+
* same one.
|
|
91
|
+
*/
|
|
92
|
+
export interface E2BDesktopCreateOptions extends E2BConnection {
|
|
82
93
|
apiKey: string;
|
|
83
94
|
dpi?: number;
|
|
84
95
|
envs?: Record<string, string>;
|
|
@@ -53,8 +53,23 @@ export declare function localCapacity(host: LocalCapacity["host"], size: Machine
|
|
|
53
53
|
}): LocalCapacity;
|
|
54
54
|
/** This machine's memory and CPUs, or the size a test passes in. */
|
|
55
55
|
export declare function machineSize(machine?: MachineSize): MachineSize;
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/** The humanish Lima VM as `limactl list` shows it: whether it exists, and its size when listed. */
|
|
57
|
+
interface LimaVmListing {
|
|
58
|
+
readonly exists: boolean;
|
|
59
|
+
readonly size?: MachineSize;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The humanish Lima VM's memory and CPUs: the size Lima lists for it, or for a VM that does not
|
|
63
|
+
* exist yet, the size setup creates (room for four desktops, capped at half the Mac's memory and
|
|
64
|
+
* CPUs). Undefined for an existing VM that lists no size: Lima omits the memory it cannot parse,
|
|
65
|
+
* and a default in its place would be a guess.
|
|
66
|
+
*/
|
|
67
|
+
export declare function limaVmSize(vm: LimaVmListing, mac: MachineSize): VmSize | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Desktops the humanish Lima VM holds, or will hold once setup creates it. Undefined when its size
|
|
70
|
+
* is unknown.
|
|
71
|
+
*/
|
|
72
|
+
export declare function limaCapacity(vm: LimaVmListing, mac: MachineSize): LocalCapacity | undefined;
|
|
58
73
|
/**
|
|
59
74
|
* Why a VM size the user asked for cannot work, or undefined: it must hold at least one desktop
|
|
60
75
|
* and fit within the Mac.
|
|
@@ -73,3 +88,4 @@ export declare function capacityShortfall(capacity: LocalCapacity, needed: numbe
|
|
|
73
88
|
} | undefined;
|
|
74
89
|
/** What a participant whose desktop Docker killed for memory is told, with what to change. */
|
|
75
90
|
export declare function outOfMemoryMessage(host: LocalCapacity["host"]): string;
|
|
91
|
+
export {};
|
|
@@ -50,14 +50,33 @@ function vmSizeFor(desktops) {
|
|
|
50
50
|
export function machineSize(machine) {
|
|
51
51
|
return machine ?? { memoryBytes: totalmem(), cpus: availableParallelism() };
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The humanish Lima VM's memory and CPUs: the size Lima lists for it, or for a VM that does not
|
|
55
|
+
* exist yet, the size setup creates (room for four desktops, capped at half the Mac's memory and
|
|
56
|
+
* CPUs). Undefined for an existing VM that lists no size: Lima omits the memory it cannot parse,
|
|
57
|
+
* and a default in its place would be a guess.
|
|
58
|
+
*/
|
|
59
|
+
export function limaVmSize(vm, mac) {
|
|
60
|
+
if (vm.size !== undefined)
|
|
61
|
+
return { memoryGiB: vm.size.memoryBytes / GiB, cpus: vm.size.cpus };
|
|
62
|
+
if (vm.exists)
|
|
63
|
+
return undefined;
|
|
55
64
|
const wanted = vmSizeFor(DEFAULT_VM_DESKTOPS);
|
|
56
65
|
return {
|
|
57
66
|
memoryGiB: Math.min(wanted.memoryGiB, Math.floor(mac.memoryBytes / GiB / 2)),
|
|
58
67
|
cpus: Math.min(wanted.cpus, Math.floor(mac.cpus / 2)),
|
|
59
68
|
};
|
|
60
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Desktops the humanish Lima VM holds, or will hold once setup creates it. Undefined when its size
|
|
72
|
+
* is unknown.
|
|
73
|
+
*/
|
|
74
|
+
export function limaCapacity(vm, mac) {
|
|
75
|
+
const size = limaVmSize(vm, mac);
|
|
76
|
+
if (size === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
return localCapacity("lima-vm", { memoryBytes: size.memoryGiB * GiB, cpus: size.cpus }, { planned: !vm.exists, machine: mac });
|
|
79
|
+
}
|
|
61
80
|
/**
|
|
62
81
|
* Why a VM size the user asked for cannot work, or undefined: it must hold at least one desktop
|
|
63
82
|
* and fit within the Mac.
|