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.
- package/CHANGELOG.md +41 -0
- package/CONTEXT.md +1 -1
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/closing-report-limits.d.ts +11 -0
- package/dist/actors/closing-report-limits.js +13 -0
- package/dist/actors/closing-report.d.ts +68 -0
- package/dist/actors/closing-report.js +55 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/codex/restricted-participant-policy.d.ts +4 -47
- package/dist/actors/codex/restricted-participant-policy.js +12 -40
- package/dist/actors/computer-use/loop/debrief.d.ts +1 -10
- package/dist/actors/computer-use/loop/debrief.js +68 -70
- package/dist/actors/computer-use/loop/impressions.d.ts +2 -2
- package/dist/actors/computer-use/loop/impressions.js +6 -1
- package/dist/actors/computer-use/loop.d.ts +0 -1
- package/dist/actors/computer-use/loop.js +3 -3
- package/dist/actors/computer-use/openai-conversation.d.ts +1 -1
- package/dist/actors/computer-use/openai-conversation.js +3 -29
- package/dist/actors/computer-use/openai-provider.js +11 -5
- package/dist/actors/contract.d.ts +2 -20
- package/dist/actors/contract.js +0 -13
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/admission.d.ts +14 -4
- package/dist/analysis/admission.js +14 -9
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/execute.d.ts +5 -0
- package/dist/analysis/execute.js +3 -0
- package/dist/analysis/finding-lead.d.ts +27 -0
- package/dist/analysis/finding-lead.js +23 -0
- package/dist/analysis/service.js +5 -3
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +5 -3
- 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 +11 -195
- package/dist/cli/io.js +10 -8
- package/dist/cli/telemetry.js +10 -22
- package/dist/cli/update-check.js +10 -19
- package/dist/cli/user-config.d.ts +11 -0
- package/dist/cli/user-config.js +25 -0
- package/dist/comms/connections.js +4 -3
- package/dist/comms/email-catch.js +7 -31
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/encoded-text.d.ts +12 -11
- package/dist/evidence/encoded-text.js +81 -70
- package/dist/evidence/redaction.d.ts +11 -0
- package/dist/evidence/redaction.js +30 -11
- package/dist/evidence/secret-scrub.d.ts +26 -3
- package/dist/evidence/secret-scrub.js +172 -38
- package/dist/feedback/draft.d.ts +1 -1
- package/dist/feedback/draft.js +13 -16
- package/dist/feedback/export-bundle.js +21 -17
- package/dist/feedback/export.js +32 -46
- package/dist/feedback/feedback.js +31 -26
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +62 -55
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/artifact.d.ts +1 -1
- package/dist/observer/http.d.ts +7 -0
- package/dist/observer/http.js +19 -1
- package/dist/observer/notes-route.d.ts +0 -1
- package/dist/observer/notes-route.js +24 -44
- package/dist/observer/pinned-files.js +4 -19
- package/dist/observer/render.js +6 -4
- package/dist/observer/run-routes.js +13 -28
- package/dist/observer/serve.js +2 -1
- 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 +59 -10
- package/dist/run/contained-output.js +160 -68
- 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 -1
- package/dist/run/evidence-files.js +24 -18
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/note-files.d.ts +26 -45
- package/dist/run/note-files.js +86 -58
- package/dist/run/note-shape.d.ts +35 -0
- package/dist/run/note-shape.js +20 -0
- package/dist/run/notes.d.ts +3 -14
- package/dist/run/notes.js +23 -75
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/participant-records.d.ts +2 -0
- package/dist/run/participant-records.js +17 -2
- 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.d.ts +6 -5
- package/dist/run/transient-comms-secrets.js +23 -39
- 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 +12 -7
- package/dist/study/summary.js +24 -24
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +39 -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/actor-contract.md +3 -3
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/state-driven-executor.md +5 -3
- package/docs/contracts/run-bundle.md +25 -13
- package/docs/contracts/schemas.md +38 -29
- package/docs/contracts/study-analysis.md +22 -5
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +8 -7
- package/skills/humanish/SKILL.md +1 -2
package/dist/study/summary.js
CHANGED
|
@@ -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"
|
|
11
|
-
import { resolveStudyDryRun } from "./plan.js";
|
|
12
|
-
import {
|
|
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
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
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 (
|
|
98
|
-
const requirements = planned
|
|
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 =
|
|
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 =
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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:
|
|
170
|
+
...(caps === undefined ? {} : { caps }),
|
|
171
|
+
mode,
|
|
172
172
|
...(keysReady === undefined ? {} : { keysReady }),
|
|
173
173
|
...(missingKeys === undefined ? {} : { missingKeys }),
|
|
174
174
|
...(planRefusal === undefined ? {} : { planRefusal }),
|
package/dist/study/warnings.d.ts
CHANGED
|
@@ -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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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[];
|
package/dist/study/warnings.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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, ...
|
|
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
|
-
|
|
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
|
-
/**
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
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
|
-
/**
|
|
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,
|