humanish 0.114.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CONTEXT.md +1 -1
  3. package/README.md +7 -6
  4. package/TELEMETRY.md +6 -0
  5. package/dist/actors/closing-report-limits.d.ts +11 -0
  6. package/dist/actors/closing-report-limits.js +13 -0
  7. package/dist/actors/closing-report.d.ts +68 -0
  8. package/dist/actors/closing-report.js +55 -0
  9. package/dist/actors/codex/app-server-ui.js +11 -4
  10. package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
  11. package/dist/actors/codex/restricted-participant-policy.js +12 -40
  12. package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
  13. package/dist/actors/computer-use/loop/debrief.js +68 -70
  14. package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
  15. package/dist/actors/computer-use/loop/impressions.js +6 -1
  16. package/dist/actors/computer-use/loop.d.ts +0 -1
  17. package/dist/actors/computer-use/loop.js +3 -3
  18. package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
  19. package/dist/actors/computer-use/openai-conversation.js +3 -29
  20. package/dist/actors/computer-use/openai-provider.js +11 -5
  21. package/dist/actors/contract.d.ts +2 -20
  22. package/dist/actors/contract.js +0 -13
  23. package/dist/actors/scripted-browser/steps.js +6 -5
  24. package/dist/analysis/admission.d.ts +14 -4
  25. package/dist/analysis/admission.js +14 -9
  26. package/dist/analysis/analysis-limits.d.ts +35 -0
  27. package/dist/analysis/analysis-limits.js +41 -0
  28. package/dist/analysis/execute.d.ts +5 -0
  29. package/dist/analysis/execute.js +3 -0
  30. package/dist/analysis/finding-lead.d.ts +27 -0
  31. package/dist/analysis/finding-lead.js +23 -0
  32. package/dist/analysis/service.js +5 -3
  33. package/dist/analysis/validation.js +14 -21
  34. package/dist/cli/commands/analyze.js +2 -1
  35. package/dist/cli/commands/notes.js +5 -3
  36. package/dist/cli/commands/run-signals.js +5 -1
  37. package/dist/cli/commands/runs.js +2 -1
  38. package/dist/cli/commands/setup.js +3 -3
  39. package/dist/cli/commands/study-format.d.ts +5 -2
  40. package/dist/cli/commands/study-format.js +29 -9
  41. package/dist/cli/commands/study-route-computer-use.js +2 -2
  42. package/dist/cli/commands/study-route-scripted.js +1 -1
  43. package/dist/cli/commands/tui.js +59 -17
  44. package/dist/cli/doctor.js +9 -2
  45. package/dist/cli/findings-text.d.ts +5 -0
  46. package/dist/cli/findings-text.js +169 -0
  47. package/dist/cli/findings.d.ts +0 -4
  48. package/dist/cli/findings.js +11 -195
  49. package/dist/cli/io.js +10 -8
  50. package/dist/cli/telemetry.js +10 -22
  51. package/dist/cli/update-check.js +10 -19
  52. package/dist/cli/user-config.d.ts +11 -0
  53. package/dist/cli/user-config.js +25 -0
  54. package/dist/comms/connections.js +4 -3
  55. package/dist/comms/email-catch.js +7 -31
  56. package/dist/comms/setup.js +18 -12
  57. package/dist/evidence/encoded-text.d.ts +12 -11
  58. package/dist/evidence/encoded-text.js +81 -70
  59. package/dist/evidence/redaction.d.ts +11 -0
  60. package/dist/evidence/redaction.js +30 -11
  61. package/dist/evidence/secret-scrub.d.ts +26 -3
  62. package/dist/evidence/secret-scrub.js +172 -38
  63. package/dist/feedback/draft.d.ts +1 -1
  64. package/dist/feedback/draft.js +13 -16
  65. package/dist/feedback/export-bundle.js +21 -17
  66. package/dist/feedback/export.js +32 -46
  67. package/dist/feedback/feedback.js +31 -26
  68. package/dist/guest/desktop-keys.js +59 -5
  69. package/dist/keys/key-resolution.d.ts +9 -0
  70. package/dist/keys/key-resolution.js +62 -55
  71. package/dist/keys/key-status.d.ts +2 -0
  72. package/dist/keys/key-status.js +9 -16
  73. package/dist/observer/artifact.d.ts +1 -1
  74. package/dist/observer/http.d.ts +7 -0
  75. package/dist/observer/http.js +19 -1
  76. package/dist/observer/notes-route.d.ts +0 -1
  77. package/dist/observer/notes-route.js +24 -44
  78. package/dist/observer/pinned-files.js +4 -19
  79. package/dist/observer/render.js +6 -4
  80. package/dist/observer/run-routes.js +13 -28
  81. package/dist/observer/serve.js +2 -1
  82. package/dist/observer-app.html +7 -7
  83. package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
  84. package/dist/routes/computer-use/bundle-parts.js +2 -2
  85. package/dist/routes/computer-use/bundle.d.ts +4 -5
  86. package/dist/routes/computer-use/bundle.js +1 -3
  87. package/dist/routes/computer-use/fanout-records.js +10 -11
  88. package/dist/routes/computer-use/live-phase.d.ts +1 -1
  89. package/dist/routes/computer-use/live-phase.js +0 -1
  90. package/dist/routes/computer-use/participant-execution.d.ts +3 -2
  91. package/dist/routes/computer-use/participant-runs.d.ts +6 -2
  92. package/dist/routes/computer-use/participant-runs.js +9 -5
  93. package/dist/routes/computer-use/plan.d.ts +3 -0
  94. package/dist/routes/computer-use/plan.js +14 -11
  95. package/dist/routes/computer-use/rerun-selection.js +2 -2
  96. package/dist/routes/computer-use/result.js +3 -6
  97. package/dist/routes/computer-use/setup.d.ts +4 -6
  98. package/dist/routes/computer-use/setup.js +2 -6
  99. package/dist/routes/computer-use/single-bundle.d.ts +4 -5
  100. package/dist/routes/computer-use/single-bundle.js +22 -25
  101. package/dist/routes/computer-use/subject-projection.d.ts +4 -8
  102. package/dist/routes/computer-use/subject-projection.js +9 -31
  103. package/dist/routes/computer-use/types.d.ts +4 -41
  104. package/dist/routes/scripted/scenario.js +28 -12
  105. package/dist/routes/scripted/session-result.js +6 -3
  106. package/dist/routes/shared-world/bundle-records.js +2 -8
  107. package/dist/routes/shared-world/participant-specs.d.ts +0 -2
  108. package/dist/routes/shared-world/participant-specs.js +1 -20
  109. package/dist/routes/shared-world/plan.d.ts +3 -0
  110. package/dist/routes/shared-world/plan.js +23 -3
  111. package/dist/routes/shared-world/setup.js +2 -2
  112. package/dist/routes/terminal/ledger.d.ts +9 -9
  113. package/dist/routes/terminal/ledger.js +63 -40
  114. package/dist/routes/terminal/live-finish.js +7 -10
  115. package/dist/routes/terminal/plan.d.ts +3 -0
  116. package/dist/routes/terminal/plan.js +8 -5
  117. package/dist/routes/terminal/token-usage.d.ts +1 -2
  118. package/dist/routes/terminal/token-usage.js +4 -6
  119. package/dist/routes/terminal/types.d.ts +9 -1
  120. package/dist/run/bundle-shape.js +6 -1
  121. package/dist/run/bundle.d.ts +5 -3
  122. package/dist/run/contained-output.d.ts +59 -10
  123. package/dist/run/contained-output.js +160 -68
  124. package/dist/run/detail.d.ts +2 -1
  125. package/dist/run/detail.js +12 -7
  126. package/dist/run/escape-sequences.d.ts +11 -0
  127. package/dist/run/escape-sequences.js +93 -0
  128. package/dist/run/evidence-files.d.ts +8 -1
  129. package/dist/run/evidence-files.js +24 -18
  130. package/dist/run/locate.d.ts +32 -3
  131. package/dist/run/locate.js +45 -40
  132. package/dist/run/note-files.d.ts +26 -45
  133. package/dist/run/note-files.js +86 -58
  134. package/dist/run/note-shape.d.ts +35 -0
  135. package/dist/run/note-shape.js +20 -0
  136. package/dist/run/notes.d.ts +3 -14
  137. package/dist/run/notes.js +23 -75
  138. package/dist/run/outcomes.d.ts +24 -0
  139. package/dist/run/outcomes.js +19 -0
  140. package/dist/run/participant-records.d.ts +2 -0
  141. package/dist/run/participant-records.js +17 -2
  142. package/dist/run/paths.d.ts +2 -0
  143. package/dist/run/paths.js +5 -1
  144. package/dist/run/preflight-receipts.js +13 -8
  145. package/dist/run/project.js +7 -4
  146. package/dist/run/reclaim-outcomes.d.ts +8 -6
  147. package/dist/run/reclaim-outcomes.js +10 -6
  148. package/dist/run/reclaim.d.ts +5 -0
  149. package/dist/run/reclaim.js +43 -16
  150. package/dist/run/run-clock.d.ts +50 -0
  151. package/dist/run/run-clock.js +66 -0
  152. package/dist/run/run.js +5 -2
  153. package/dist/run/sandbox-ids.d.ts +7 -2
  154. package/dist/run/sandbox-ids.js +27 -11
  155. package/dist/run/secrets.d.ts +2 -1
  156. package/dist/run/secrets.js +18 -22
  157. package/dist/run/stored-runs.js +14 -7
  158. package/dist/run/transient-comms-secrets.d.ts +6 -5
  159. package/dist/run/transient-comms-secrets.js +23 -39
  160. package/dist/run-study.js +22 -13
  161. package/dist/study/adapter-scorer-loader.js +6 -4
  162. package/dist/study/discover.js +9 -6
  163. package/dist/study/init-paths.js +7 -5
  164. package/dist/study/persona-resolve.d.ts +2 -2
  165. package/dist/study/persona-resolve.js +9 -5
  166. package/dist/study/plan-types.d.ts +2 -2
  167. package/dist/study/plan.d.ts +5 -3
  168. package/dist/study/plan.js +18 -9
  169. package/dist/study/preflight-probes.js +11 -3
  170. package/dist/study/preflight.js +9 -5
  171. package/dist/study/summary.d.ts +12 -7
  172. package/dist/study/summary.js +24 -24
  173. package/dist/study/warnings.d.ts +19 -12
  174. package/dist/study/warnings.js +39 -17
  175. package/dist/substrates/e2b/connection.d.ts +12 -0
  176. package/dist/substrates/e2b/connection.js +35 -0
  177. package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
  178. package/dist/substrates/e2b/desktop-executor.js +42 -9
  179. package/dist/substrates/e2b/lifetime.d.ts +24 -4
  180. package/dist/substrates/e2b/lifetime.js +41 -4
  181. package/dist/substrates/e2b/sandbox.d.ts +4 -3
  182. package/dist/substrates/e2b/sandbox.js +13 -18
  183. package/dist/substrates/e2b/sdk.d.ts +20 -9
  184. package/dist/substrates/local/capacity.d.ts +18 -2
  185. package/dist/substrates/local/capacity.js +21 -2
  186. package/dist/substrates/local/runtime-host.js +21 -14
  187. package/dist/substrates/local/runtime.js +3 -11
  188. package/dist/tui/contract.d.ts +30 -5
  189. package/dist/tui/contract.js +4 -1
  190. package/dist/tui-app.js +149 -147
  191. package/dist/verify/artifacts.d.ts +9 -2
  192. package/dist/verify/artifacts.js +37 -13
  193. package/dist/verify/liveness.js +19 -5
  194. package/dist/verify/verify.js +54 -27
  195. package/docs/architecture/actor-contract.md +3 -3
  196. package/docs/architecture/guest-desktop.md +7 -2
  197. package/docs/architecture/restricted-codex-analysis.md +1 -1
  198. package/docs/architecture/state-driven-executor.md +5 -3
  199. package/docs/contracts/run-bundle.md +25 -13
  200. package/docs/contracts/schemas.md +38 -29
  201. package/docs/contracts/study-analysis.md +22 -5
  202. package/docs/product/automatic-analysis.md +7 -1
  203. package/package.json +8 -7
  204. package/skills/humanish/SKILL.md +1 -2
@@ -7,12 +7,11 @@ import { localRuntimeStatus } from "../substrates/local/runtime.js";
7
7
  // what a stakeholder reads on the screen where they decide whether to press Start.
8
8
  //
9
9
  // Resolved analysis defaults are shown independently of declared participant caps. A cap that is not
10
- // declared is not "unlimited" and not "$0"; it is a line the screen does not draw.
11
- import { resolveStudyDryRun } from "./plan.js";
12
- import { routeOf } from "./plan.js";
13
- import { localCodexParticipantCheck, planCliRun } from "./doctor.js";
10
+ // declared is not "unlimited" and not "$0": the screen says that no cap is set.
11
+ import { planStudy, resolveStudyDryRun, routeOf } from "./plan.js";
12
+ import { localCodexParticipantCheck } from "./doctor.js";
14
13
  import { requiredKeys, requiredSubjectEnv } from "./requirements.js";
15
- import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
14
+ import { automaticAnalysisBudget, formatAutomaticAnalysisBudget, } from "../analysis/automatic-config.js";
16
15
  import { DEFAULT_OPENAI_CU_MODEL, DEFAULT_OPENAI_CU_REASONING_EFFORT, } from "../actors/computer-use/openai-provider.js";
17
16
  import { inspectStudyManifest } from "./discover.js";
18
17
  import { computerUseParticipantCount, isComputerUseComposition } from "./routing.js";
@@ -70,7 +69,7 @@ function participantsOf(config) {
70
69
  /** The computer-use caps. Other routes draw none. */
71
70
  function summaryCapsOf(config) {
72
71
  if (!isComputerUseComposition(config))
73
- return {};
72
+ return undefined;
74
73
  const caps = config.caps;
75
74
  return {
76
75
  ...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
@@ -89,18 +88,17 @@ export async function readStudySummary(cwd, study, options = {}) {
89
88
  const route = routeOf(inspected.config);
90
89
  let keysReady;
91
90
  let missingKeys;
92
- const dryRun = resolveStudyDryRun(inspected.config, undefined, true) === true;
93
- // A live key check reads the plan's requirements. A study the planner refuses has none to check,
94
- // so the summary reports the refusal in place of its keys.
95
- const planned = options.checkKeys === true && !dryRun ? await planCliRun(inspected.config, cwd) : undefined;
91
+ const mode = resolveStudyDryRun(inspected.config, undefined, true) === true ? "dry-run" : "live";
92
+ // The keys are the live plan's, whatever the file's mode: the screen offers a live run of every
93
+ // study, and a dry run needs no keys to report. A study the planner refuses live has none to
94
+ // check, so the summary reports the refusal in place of its keys.
95
+ const planned = options.checkKeys === true ? planStudy(inspected.config, { cwd, dryRun: false }) : undefined;
96
96
  const planRefusal = planned?.ok === false ? planned.refusal.message : undefined;
97
- if (options.checkKeys === true && planned?.ok !== false) {
98
- const requirements = planned?.planned.plan.requirements ?? [];
97
+ if (planned?.ok === true) {
98
+ const requirements = planned.planned.plan.requirements;
99
99
  const subjectKeys = requiredSubjectEnv(requirements);
100
100
  const email = inspected.config.comms?.email;
101
- const receivingKey = !dryRun && email?.kind === "real"
102
- ? await receivingRequiredKey(cwd, email.connection)
103
- : undefined;
101
+ const receivingKey = email?.kind === "real" ? await receivingRequiredKey(cwd, email.connection) : undefined;
104
102
  const candidates = [
105
103
  "OPENAI_API_KEY",
106
104
  "CODEX_API_KEY",
@@ -108,13 +106,11 @@ export async function readStudySummary(cwd, study, options = {}) {
108
106
  ...subjectKeys,
109
107
  ...(receivingKey ? [receivingKey] : []),
110
108
  ];
111
- const probes = dryRun
112
- ? []
113
- : await probeKeySources(candidates, {
114
- cwd,
115
- env: options.env ?? process.env,
116
- ...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
117
- }).catch(() => []);
109
+ const probes = await probeKeySources(candidates, {
110
+ cwd,
111
+ env: options.env ?? process.env,
112
+ ...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
113
+ }).catch(() => []);
118
114
  const present = new Set(probes.filter((probe) => probe.source !== null).map((probe) => probe.name));
119
115
  const required = requiredKeys(requirements, (name) => present.has(name));
120
116
  const missing = [
@@ -130,6 +126,7 @@ export async function readStudySummary(cwd, study, options = {}) {
130
126
  const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
131
127
  const subject = subjectOf(config);
132
128
  const participants = participantsOf(inspected.config);
129
+ const caps = summaryCapsOf(inspected.config);
133
130
  const runtime = options.checkKeys === true && isLocalBrowserStudy(inspected.config)
134
131
  ? await localRuntimeStatus({
135
132
  ...(options.env ? { env: options.env } : {}),
@@ -143,7 +140,9 @@ export async function readStudySummary(cwd, study, options = {}) {
143
140
  ? await localCodexParticipantCheck({ env: options.env ?? process.env })
144
141
  : undefined;
145
142
  return {
146
- ...(analysis ? { analysis } : {}),
143
+ ...(analysis
144
+ ? { analysis: { ...analysis, line: formatAutomaticAnalysisBudget(analysis) } }
145
+ : {}),
147
146
  ...(runtime
148
147
  ? { runtime: { ok: runtime.ok, installed: runtime.installed, message: runtime.message } }
149
148
  : {}),
@@ -168,7 +167,8 @@ export async function readStudySummary(cwd, study, options = {}) {
168
167
  ...(participants === undefined ? {} : { participants }),
169
168
  model: inspected.config.actor?.model ?? DEFAULT_OPENAI_CU_MODEL,
170
169
  reasoningEffort: reasoningEffortOf(inspected.config),
171
- caps: summaryCapsOf(inspected.config),
170
+ ...(caps === undefined ? {} : { caps }),
171
+ mode,
172
172
  ...(keysReady === undefined ? {} : { keysReady }),
173
173
  ...(missingKeys === undefined ? {} : { missingKeys }),
174
174
  ...(planRefusal === undefined ? {} : { planRefusal }),
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedPersona } from "./persona.js";
2
2
  import type { StudyConfig } from "./types.js";
3
+ import { type Participant } from "./plan-participants.js";
3
4
  /**
4
5
  * The fields a config sets that its route does not read, each as `field` or `field (reason)`.
5
6
  * parseStudy refuses a study for any of them.
@@ -10,17 +11,23 @@ export declare function inertFieldLabels(config: StudyConfig): string[];
10
11
  * `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
11
12
  */
12
13
  export declare function inertFieldPaths(config: StudyConfig): string[];
14
+ export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
13
15
  /**
14
- * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
15
- * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
16
- * study author would trust a list that blocks nothing. `humanish study check` and every run print
17
- * it, and the run records it in its bundle.
18
- * 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
19
- * route.
16
+ * The warnings about a study's own fields that a run records in its bundle as `study.warning`
17
+ * events: `execution.egressAllow` off the terminal route, and participant text that reads like a
18
+ * script. parseStudy reports them too, after its other warnings, so `study check` and every run
19
+ * print them.
20
20
  */
21
- export declare function egressAllowIgnoredWarning(config: StudyConfig): string | undefined;
22
- export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
23
- /** Authored participant text is checked before runtime instructions or researcher criteria join it. */
24
- export declare function scriptedMissionWarnings(config: StudyConfig): string[];
25
- /** Backgrounds give participants a reason to act beyond the assigned task. */
26
- export declare function personaBackgroundWarnings(config: StudyConfig, personas: ReadonlyMap<string, ResolvedPersona>): string[];
21
+ export declare function recordedStudyWarnings(config: StudyConfig): string[];
22
+ /**
23
+ * A warning for each participant whose persona has no background, which gives a participant a
24
+ * reason to act beyond the assigned task. `participants` are the records the planner builds: a
25
+ * computer-use or shared-world plan's participants, or `plannedParticipants` before a plan exists.
26
+ */
27
+ export declare function personaBackgroundWarnings(studyId: string, participants: readonly Pick<Participant, "id" | "personaId">[], personas: ReadonlyMap<string, ResolvedPersona>): string[];
28
+ /**
29
+ * The participant records planStudy builds for a study's declared participants, for `study check`
30
+ * and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
31
+ * `--count` is not applied.
32
+ */
33
+ export declare function plannedParticipants(config: StudyConfig): readonly Participant[];
@@ -1,5 +1,6 @@
1
+ import { cli } from "../cli/invocation.js";
1
2
  import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, } from "./routing.js";
2
- import { declaredParticipantIds } from "./plan-participants.js";
3
+ import { computerUseParticipants, declaredParticipantIds, sharedWorldParticipants, } from "./plan-participants.js";
3
4
  import { addressedRecipients } from "./parse/comms.js";
4
5
  import { participantList, participantInstruction, declaredParticipantCount, } from "./study-fields.js";
5
6
  // The computer-use and terminal routes consume mission/persona/model (they compose the agent prompt
@@ -268,12 +269,11 @@ export function inertFieldPaths(config) {
268
269
  /**
269
270
  * The warning for `execution.egressAllow` on a route that ignores it, or undefined. Only the
270
271
  * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
271
- * study author would trust a list that blocks nothing. `humanish study check` and every run print
272
- * it, and the run records it in its bundle.
272
+ * study author would trust a list that blocks nothing.
273
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
274
274
  * route.
275
275
  */
276
- export function egressAllowIgnoredWarning(config) {
276
+ function egressAllowIgnoredWarning(config) {
277
277
  if (config.execution?.egressAllow === undefined || config.route === "terminal")
278
278
  return undefined;
279
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.`;
@@ -288,9 +288,6 @@ export function forwardDeclaredWarnings(config) {
288
288
  : [
289
289
  `These fields are set, but no route reads them yet, so they have no effect: ${inert.join(", ")}.`,
290
290
  ];
291
- const egressIgnored = egressAllowIgnoredWarning(config);
292
- if (egressIgnored !== undefined)
293
- warnings.push(egressIgnored);
294
291
  // A declared cap below the participant count is legal but loud: the roster promises N live actors
295
292
  // and the cap delivers waves of M. Say so up front (inspect + dry-run + run): a green run in
296
293
  // waves is otherwise indistinguishable from the all-live run the author meant.
@@ -311,7 +308,20 @@ export function forwardDeclaredWarnings(config) {
311
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.`);
312
309
  }
313
310
  }
314
- return [...warnings, ...scriptedMissionWarnings(config)];
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
+ ];
315
325
  }
316
326
  const UI_ACTION = /\b(?:click|tap|press|type\b[^.!?\n]*?\binto|select\b[^.!?\n]*?\bfrom)\b/gi;
317
327
  function normalizedUrl(value) {
@@ -342,7 +352,7 @@ function actionChain(text) {
342
352
  /\bthen\b/i.test(text.slice((actions[0]?.index ?? 0) + (actions[0]?.[0].length ?? 0)))));
343
353
  }
344
354
  /** Authored participant text is checked before runtime instructions or researcher criteria join it. */
345
- export function scriptedMissionWarnings(config) {
355
+ function scriptedMissionWarnings(config) {
346
356
  if (config.route !== "computer-use" && config.route !== "shared-world")
347
357
  return [];
348
358
  const fields = [
@@ -373,20 +383,32 @@ export function scriptedMissionWarnings(config) {
373
383
  ];
374
384
  });
375
385
  }
376
- /** Backgrounds give participants a reason to act beyond the assigned task. */
377
- export function personaBackgroundWarnings(config, personas) {
378
- if (config.route !== "computer-use" && config.route !== "shared-world")
379
- return [];
380
- const roster = participantList(config);
381
- return declaredParticipantIds(config).flatMap((id, index) => {
382
- const personaId = roster?.[index]?.persona ?? config.actor?.persona;
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 }) => {
383
393
  if (personaId && personas.get(personaId)?.background)
384
394
  return [];
385
395
  const reason = personaId
386
396
  ? `persona ${personaId} has no readable background`
387
397
  : "no persona is assigned";
388
398
  return [
389
- `Participant ${id} has no persona background because ${reason}. Add a short, fictional background describing their experience and situation. Run humanish study show ${config.id} --json to see what the participant receives.`,
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.`,
390
400
  ];
391
401
  });
392
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
- /** Optional command surface used only for best-effort substrate fallbacks. */
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 HELD_KEYS_TIMEOUT_MS = 15_000;
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
- // The SDK press() accepts a string[] directly; pass the keys through so
239
- // a chord (e.g. ["Control", "a"]) is pressed together, not in sequence.
240
- await desktop.press(action.keys);
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.run(`xdotool keydown ${chord}`, options);
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.run(`xdotool keyup ${chord}`, options);
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
- * E2B refuses a sandbox lifetime over one hour ("400: Timeout cannot be greater than 1 hours").
3
- * A derived deadline has to stay under it, and saying so at plan time beats discovering it from a
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 declare const MAX_SANDBOX_MS: number;
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
- * E2B refuses a sandbox lifetime over one hour ("400: Timeout cannot be greater than 1 hours").
7
- * A derived deadline has to stay under it, and saying so at plan time beats discovering it from a
8
- * raw provider 400 after a plan has already printed.
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 const MAX_SANDBOX_MS = 60 * 60_000;
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(module: E2BDesktopModule, sandboxId: string, options: {
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(module: E2BDesktopModule, sandboxId: string, options: {
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(module: E2BDesktopModule, tags: Record<string, string>, options: {
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
- const allocation = ownE2BSandbox(module, sandbox.sandboxId, ticket);
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(module, resourceId, ticket) {
133
- // Bind the provider method now, before any hook can replace it on the shared module.
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(module, sandboxId, options) {
193
- const released = await killById(boundKill(module), sandboxId, options);
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(module, sandboxId, options) {
213
- if (typeof module.Sandbox.getInfo !== "function")
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 module.Sandbox.getInfo(sandboxId, options);
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(module, tags, options) {
238
- if (typeof module.Sandbox.list !== "function")
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 = module.Sandbox.list({
241
+ const pages = account.list({
247
242
  query: { metadata: tags },
248
243
  limit: TAG_SEARCH_PAGE_SIZE,
249
244
  requestTimeoutMs: options.requestTimeoutMs,