humanish 0.109.1 → 0.110.1
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/ARCHITECTURE.md +14 -11
- package/CHANGELOG.md +24 -0
- package/CONTEXT.md +8 -6
- package/README.md +97 -48
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +11 -0
- package/dist/actors/local-agent/cli.js +103 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +18 -11
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +96 -44
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +89 -67
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/commands/study-format.js +29 -2
- package/dist/cli/commands/study-route-computer-use.js +2 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +2 -0
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/study.js +4 -3
- package/dist/cli/commands/tui.js +4 -3
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/doctor.d.ts +7 -2
- package/dist/cli/doctor.js +47 -8
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +316 -0
- package/dist/cli/first-run-path.d.ts +5 -4
- package/dist/cli/first-run-path.js +14 -7
- package/dist/cli/io.d.ts +17 -3
- package/dist/cli/io.js +22 -4
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/comms/receiving-runtime.js +1 -1
- package/dist/comms/setup.js +1 -1
- package/dist/evidence/redaction.d.ts +7 -3
- package/dist/evidence/redaction.js +68 -24
- package/dist/feedback/export-bundle.js +6 -1
- package/dist/feedback/export.js +3 -0
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.d.ts +7 -0
- package/dist/keys/key-resolution.js +10 -8
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +18 -15
- package/dist/observer/render.js +8 -2
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +43 -34
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +4 -4
- package/dist/routes/computer-use/preflight.js +2 -2
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/computer-use/types.d.ts +6 -0
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/scripted/types.d.ts +1 -0
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/bundle.js +4 -2
- package/dist/routes/terminal/credentials.js +1 -1
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +23 -17
- package/dist/routes/terminal/plan.js +4 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/session.js +1 -1
- package/dist/routes/terminal/types.d.ts +3 -0
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +26 -1
- package/dist/run/bundle.d.ts +30 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +11 -7
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +10 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +18 -14
- package/dist/run/reclaim.js +179 -105
- package/dist/run/results.d.ts +11 -18
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +177 -72
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +138 -0
- package/dist/run/sandbox-ids.d.ts +53 -0
- package/dist/run/sandbox-ids.js +198 -0
- package/dist/run/sandbox-receipts.d.ts +16 -0
- package/dist/run/sandbox-receipts.js +70 -0
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -20
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -137
- package/dist/run/streams.d.ts +3 -2
- package/dist/run-study.js +6 -1
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +7 -4
- package/dist/study/init-templates.js +2 -2
- package/dist/study/init.js +14 -3
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/types.d.ts +1 -1
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +128 -35
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui-app.js +133 -133
- package/dist/verify/artifacts.d.ts +4 -0
- package/dist/verify/artifacts.js +8 -0
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +53 -28
- package/dist/verify/verify.d.ts +4 -1
- package/dist/verify/verify.js +27 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +90 -7
- package/docs/contracts/schemas.md +39 -22
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +5 -3
- package/skills/humanish/SKILL.md +18 -4
|
@@ -1,7 +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 E2BDesktopCreateOptions, type E2BDesktopModule, type E2BDesktopSandbox } from "./sdk.js";
|
|
4
|
+
import { type E2BDesktopCreateOptions, type E2BDesktopModule, type E2BDesktopSandbox, type E2BListedSandbox } from "./sdk.js";
|
|
5
5
|
/** Where an allocation's receipt goes: the run's prepared root, under a public-safe participant
|
|
6
6
|
* label. The receipt line saves it as `laneId`. */
|
|
7
7
|
interface E2BSandboxReceiptTarget {
|
|
@@ -16,7 +16,7 @@ export interface E2BSandboxRequest {
|
|
|
16
16
|
retry?: TransientRetryHooks;
|
|
17
17
|
/**
|
|
18
18
|
* Required so every caller decides. `null` is only for a caller that could not open a journal;
|
|
19
|
-
* that sandbox is reclaimable by its create-time `timeoutMs` alone.
|
|
19
|
+
* that sandbox carries no owner tags and is reclaimable by its create-time `timeoutMs` alone.
|
|
20
20
|
*/
|
|
21
21
|
receipt: E2BSandboxReceiptTarget | null;
|
|
22
22
|
}
|
|
@@ -57,13 +57,39 @@ export type E2BSandboxDestroyOutcome = {
|
|
|
57
57
|
state: "kill-failed";
|
|
58
58
|
detail: string;
|
|
59
59
|
};
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
* reclaim's persisted outcome. It never lists the account.
|
|
63
|
-
*/
|
|
60
|
+
/** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
|
|
61
|
+
* reclaim's persisted outcome. */
|
|
64
62
|
export declare function destroyE2BSandbox(module: E2BDesktopModule, sandboxId: string, options: {
|
|
65
63
|
requestTimeoutMs: number;
|
|
66
64
|
}): Promise<E2BSandboxDestroyOutcome>;
|
|
65
|
+
/** What `reclaim --check` learned about one sandbox by its exact id. */
|
|
66
|
+
export type E2BSandboxPresence = {
|
|
67
|
+
state: "running" | "already-gone";
|
|
68
|
+
} | {
|
|
69
|
+
state: "check-failed";
|
|
70
|
+
detail: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
|
|
74
|
+
* type, means gone; any other error leaves the answer open.
|
|
75
|
+
*/
|
|
76
|
+
export declare function inspectE2BSandbox(module: E2BDesktopModule, sandboxId: string, options: {
|
|
77
|
+
requestTimeoutMs: number;
|
|
78
|
+
}): Promise<E2BSandboxPresence>;
|
|
79
|
+
/** What E2B listed for a run's owner tags. `sandboxes` holds exact matches only. */
|
|
80
|
+
export interface E2BTagSearch {
|
|
81
|
+
status: "done" | "unavailable" | "failed";
|
|
82
|
+
sandboxes: E2BListedSandbox[];
|
|
83
|
+
detail?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* List the sandboxes E2B matches to every one of `tags`, filtered server-side. Each result is
|
|
87
|
+
* checked against every tag again, so a server that ignored the filter still cannot hand back
|
|
88
|
+
* another run's sandbox, and the search stops after a few pages instead of walking the account.
|
|
89
|
+
*/
|
|
90
|
+
export declare function findE2BSandboxesByTags(module: E2BDesktopModule, tags: Record<string, string>, options: {
|
|
91
|
+
requestTimeoutMs: number;
|
|
92
|
+
}): Promise<E2BTagSearch>;
|
|
67
93
|
/** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
|
|
68
94
|
export declare const E2B_SPEECH_TEMPLATE = "7409n13kr83f7g7abx5g";
|
|
69
95
|
/** The desktop template a study asks for; undefined selects the SDK default. */
|
|
@@ -74,6 +100,8 @@ export declare function e2bDesktopTemplate(config: {
|
|
|
74
100
|
export interface TransientRetryHooks {
|
|
75
101
|
onRetry?: (reason: string) => void;
|
|
76
102
|
sleep?: (ms: number) => Promise<void>;
|
|
103
|
+
/** False skips the retry, as when the run started stopping during the first attempt. */
|
|
104
|
+
canRetry?: () => boolean;
|
|
77
105
|
}
|
|
78
106
|
/** Wall-clock pause before the single retry; envd routing settles within a few seconds. */
|
|
79
107
|
export declare const TRANSIENT_RETRY_DELAY_MS = 3000;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// E2B sandbox acquisition and release. Every route that allocates an E2B sandbox calls this module,
|
|
2
|
-
// so each allocation runs the same steps in the same order:
|
|
2
|
+
// so each allocation runs the same steps in the same order: register the create with the run's
|
|
3
|
+
// in-process registry, tag the sandbox with the run's owner tags, create, one retry on a transient
|
|
3
4
|
// provider error, capture of the exact id and kill authority, and a provider-qualified receipt in
|
|
4
5
|
// the run directory before the caller gets the handle. The desktop startup guard
|
|
5
6
|
// (guardDesktopSandboxCreate) is installed by loadE2BDesktopModule, so a create whose desktop
|
|
6
|
-
// startup fails has already reclaimed its handle when the error reaches the retry here
|
|
7
|
+
// startup fails has already reclaimed its handle when the error reaches the retry here, and it
|
|
8
|
+
// reports the id to the registry before desktop startup begins.
|
|
7
9
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
8
|
-
import {
|
|
10
|
+
import { beginSandboxCreate, recordSandboxOwnerOnce, SandboxCreateRefusedError, sandboxOwnerTags, } from "../../run/sandbox-creates.js";
|
|
11
|
+
import { appendSandboxOwner, appendSandboxReceipt } from "../../run/sandbox-receipts.js";
|
|
9
12
|
import { ownDesktopAllocation, } from "../desktop-session.js";
|
|
10
|
-
import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, } from "./sdk.js";
|
|
13
|
+
import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, observeDesktopAllocation, } from "./sdk.js";
|
|
11
14
|
/** A desktop sandbox, on the stock `desktop` template or a named one. */
|
|
12
15
|
export function acquireE2BDesktopSandbox(request) {
|
|
13
16
|
return acquire(request, request.template);
|
|
@@ -17,33 +20,57 @@ export function acquireE2BShellSandbox(request) {
|
|
|
17
20
|
return acquire(request, undefined);
|
|
18
21
|
}
|
|
19
22
|
async function acquire(request, template) {
|
|
20
|
-
const { module
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: module.Sandbox.create(template, options), request.retry);
|
|
25
|
-
const allocation = ownE2BSandbox(module, sandbox.sandboxId);
|
|
23
|
+
const { module } = request;
|
|
24
|
+
const target = request.receipt;
|
|
25
|
+
// Refused before any provider call once the run is stopping.
|
|
26
|
+
const ticket = target === null ? undefined : beginSandboxCreate(target.root, target.participantId);
|
|
26
27
|
try {
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
// The owner tags let reclaim find this sandbox on E2B even if its id never reaches this
|
|
29
|
+
// process: a create that throws after E2B allocated, or a process that dies mid-create. The
|
|
30
|
+
// journal records them first, so reclaim knows which tags to search for.
|
|
31
|
+
const owner = target === null ? undefined : sandboxOwnerTags(target.root);
|
|
32
|
+
if (target !== null && owner !== undefined)
|
|
33
|
+
await recordSandboxOwnerOnce(target.root, () => appendSandboxOwner(target.root, owner));
|
|
34
|
+
const options = owner === undefined
|
|
35
|
+
? request.options
|
|
36
|
+
: { ...request.options, metadata: { ...request.options.metadata, ...owner } };
|
|
37
|
+
if (ticket !== undefined)
|
|
38
|
+
observeDesktopAllocation(options, (sandboxId) => ticket.created(sandboxId));
|
|
39
|
+
const sandbox = await withOneRetryOnTransientE2BError(() => {
|
|
40
|
+
if (ticket?.stopping())
|
|
41
|
+
throw new SandboxCreateRefusedError();
|
|
42
|
+
// Without a template the SDK's one-argument create chooses its stock desktop template.
|
|
43
|
+
return template === undefined
|
|
44
|
+
? module.Sandbox.create(options)
|
|
45
|
+
: module.Sandbox.create(template, options);
|
|
46
|
+
}, { ...request.retry, canRetry: () => ticket?.stopping() !== true });
|
|
47
|
+
ticket?.created(sandbox.sandboxId);
|
|
48
|
+
const allocation = ownE2BSandbox(module, sandbox.sandboxId, ticket);
|
|
49
|
+
try {
|
|
50
|
+
if (target !== null) {
|
|
51
|
+
const { root, participantId, now = Date.now } = target;
|
|
52
|
+
// Best effort by contract: a failed write leaves the owner tags and the TTL as the
|
|
53
|
+
// backstops and never fails the participant.
|
|
54
|
+
await appendSandboxReceipt(root, {
|
|
55
|
+
at: new Date(now()).toISOString(),
|
|
56
|
+
laneId: participantId,
|
|
57
|
+
provider: "e2b",
|
|
58
|
+
sandboxId: allocation.resourceId,
|
|
59
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
38
62
|
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// The caller never receives this sandbox, so release it here. A throwing or invalid injected
|
|
65
|
+
// clock is the known case.
|
|
66
|
+
await allocation.close();
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return { sandbox, allocation };
|
|
39
70
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// clock is the known case.
|
|
43
|
-
await allocation.close();
|
|
44
|
-
throw error;
|
|
71
|
+
finally {
|
|
72
|
+
ticket?.settled();
|
|
45
73
|
}
|
|
46
|
-
return { sandbox, allocation };
|
|
47
74
|
}
|
|
48
75
|
/** The provider's kill method bound to its class, or undefined when the SDK has none. */
|
|
49
76
|
function boundKill(module) {
|
|
@@ -57,7 +84,7 @@ function boundKill(module) {
|
|
|
57
84
|
* recognized by type, means the same. Every other throw is unconfirmed, whatever its message
|
|
58
85
|
* says: the SDK answers a 404 with false before it throws, so "not found" or "404" in a thrown
|
|
59
86
|
* message comes from some other failure, such as a trace id. A true in debug mode sent no
|
|
60
|
-
* request, so it confirms nothing.
|
|
87
|
+
* request, so it confirms nothing.
|
|
61
88
|
*/
|
|
62
89
|
async function killById(kill, sandboxId, options) {
|
|
63
90
|
if (kill === undefined)
|
|
@@ -84,12 +111,18 @@ async function killById(kill, sandboxId, options) {
|
|
|
84
111
|
return { status: "unconfirmed", reason: "release_failed", error };
|
|
85
112
|
}
|
|
86
113
|
}
|
|
87
|
-
function ownE2BSandbox(module, resourceId) {
|
|
114
|
+
function ownE2BSandbox(module, resourceId, ticket) {
|
|
88
115
|
// Bind the provider method now, before any hook can replace it on the shared module.
|
|
89
116
|
const kill = boundKill(module);
|
|
90
117
|
return ownDesktopAllocation({
|
|
91
118
|
resourceId,
|
|
92
|
-
release: () =>
|
|
119
|
+
release: async () => {
|
|
120
|
+
const released = await killById(kill, resourceId, { requestTimeoutMs: 60_000 });
|
|
121
|
+
// The signal handler skips a sandbox its route already released.
|
|
122
|
+
if (released.status === "released")
|
|
123
|
+
ticket?.released(resourceId);
|
|
124
|
+
return released;
|
|
125
|
+
},
|
|
93
126
|
});
|
|
94
127
|
}
|
|
95
128
|
/**
|
|
@@ -136,10 +169,8 @@ export function releaseUnavailableDetail(detail) {
|
|
|
136
169
|
? "installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox"
|
|
137
170
|
: `${detail}; if the sandbox exists, server-side kill-on-timeout will reclaim it`;
|
|
138
171
|
}
|
|
139
|
-
/**
|
|
140
|
-
*
|
|
141
|
-
* reclaim's persisted outcome. It never lists the account.
|
|
142
|
-
*/
|
|
172
|
+
/** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
|
|
173
|
+
* reclaim's persisted outcome. */
|
|
143
174
|
export async function destroyE2BSandbox(module, sandboxId, options) {
|
|
144
175
|
const released = await killById(boundKill(module), sandboxId, options);
|
|
145
176
|
if (released.status === "released")
|
|
@@ -156,6 +187,68 @@ export async function destroyE2BSandbox(module, sandboxId, options) {
|
|
|
156
187
|
return { state: "kill-failed", detail: redactText(toErrorMessage(released.error)) };
|
|
157
188
|
}
|
|
158
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
|
|
192
|
+
* type, means gone; any other error leaves the answer open.
|
|
193
|
+
*/
|
|
194
|
+
export async function inspectE2BSandbox(module, sandboxId, options) {
|
|
195
|
+
if (typeof module.Sandbox.getInfo !== "function")
|
|
196
|
+
return {
|
|
197
|
+
state: "check-failed",
|
|
198
|
+
detail: "the installed @e2b/desktop SDK has no Sandbox.getInfo",
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
await module.Sandbox.getInfo(sandboxId, options);
|
|
202
|
+
return { state: "running" };
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (isSandboxNotFoundError(error))
|
|
206
|
+
return { state: "already-gone" };
|
|
207
|
+
return { state: "check-failed", detail: redactText(toErrorMessage(error)) };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const TAG_SEARCH_PAGE_SIZE = 100;
|
|
211
|
+
// One run tags at most a few dozen sandboxes. More pages than this means the filter was not
|
|
212
|
+
// applied, and reading on would walk the whole account.
|
|
213
|
+
const TAG_SEARCH_MAX_PAGES = 3;
|
|
214
|
+
/**
|
|
215
|
+
* List the sandboxes E2B matches to every one of `tags`, filtered server-side. Each result is
|
|
216
|
+
* checked against every tag again, so a server that ignored the filter still cannot hand back
|
|
217
|
+
* another run's sandbox, and the search stops after a few pages instead of walking the account.
|
|
218
|
+
*/
|
|
219
|
+
export async function findE2BSandboxesByTags(module, tags, options) {
|
|
220
|
+
if (typeof module.Sandbox.list !== "function")
|
|
221
|
+
return {
|
|
222
|
+
status: "unavailable",
|
|
223
|
+
sandboxes: [],
|
|
224
|
+
detail: "the installed @e2b/desktop SDK has no Sandbox.list",
|
|
225
|
+
};
|
|
226
|
+
const sandboxes = [];
|
|
227
|
+
try {
|
|
228
|
+
const pages = module.Sandbox.list({
|
|
229
|
+
query: { metadata: tags },
|
|
230
|
+
limit: TAG_SEARCH_PAGE_SIZE,
|
|
231
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
232
|
+
});
|
|
233
|
+
for (let page = 0; pages.hasNext; page += 1) {
|
|
234
|
+
if (page === TAG_SEARCH_MAX_PAGES)
|
|
235
|
+
return {
|
|
236
|
+
status: "failed",
|
|
237
|
+
sandboxes,
|
|
238
|
+
detail: `E2B returned more than ${TAG_SEARCH_MAX_PAGES * TAG_SEARCH_PAGE_SIZE} sandboxes for one run's tags, so the search stopped before reading the rest`,
|
|
239
|
+
};
|
|
240
|
+
for (const listed of await pages.nextItems()) {
|
|
241
|
+
const metadata = listed.metadata ?? {};
|
|
242
|
+
if (Object.entries(tags).every(([key, value]) => metadata[key] === value))
|
|
243
|
+
sandboxes.push(listed);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return { status: "done", sandboxes };
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
return { status: "failed", sandboxes, detail: redactText(toErrorMessage(error)) };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
159
252
|
/** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
|
|
160
253
|
export const E2B_SPEECH_TEMPLATE = "7409n13kr83f7g7abx5g";
|
|
161
254
|
/** The desktop template a study asks for; undefined selects the SDK default. */
|
|
@@ -206,7 +299,7 @@ export async function withOneRetryOnTransientE2BError(attempt, hooks) {
|
|
|
206
299
|
return await attempt();
|
|
207
300
|
}
|
|
208
301
|
catch (error) {
|
|
209
|
-
if (!isTransientE2BError(error))
|
|
302
|
+
if (!isTransientE2BError(error) || hooks?.canRetry?.() === false)
|
|
210
303
|
throw error;
|
|
211
304
|
const reason = error instanceof Error ? error.message : String(error);
|
|
212
305
|
hooks?.onRetry?.(reason);
|
|
@@ -27,7 +27,32 @@ export interface E2BDesktopModule {
|
|
|
27
27
|
getInfo?(sandboxId: string, options?: {
|
|
28
28
|
requestTimeoutMs?: number;
|
|
29
29
|
}): Promise<E2BSandboxInfo>;
|
|
30
|
+
/**
|
|
31
|
+
* List sandboxes whose metadata matches every `query.metadata` pair, filtered server-side.
|
|
32
|
+
* Reclaim calls it only with a run's owner tags (sandboxOwnerTags in src/run/sandbox-creates.ts)
|
|
33
|
+
* and re-checks each tag on what comes back. The e2b SDK on the supported @e2b/desktop range
|
|
34
|
+
* (2.38 and later) returns a paginator.
|
|
35
|
+
*/
|
|
36
|
+
list?(options?: E2BSandboxListOptions): E2BSandboxPaginator;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
interface E2BSandboxListOptions {
|
|
40
|
+
query?: {
|
|
41
|
+
metadata?: Record<string, string>;
|
|
30
42
|
};
|
|
43
|
+
limit?: number;
|
|
44
|
+
requestTimeoutMs?: number;
|
|
45
|
+
}
|
|
46
|
+
interface E2BSandboxPaginator {
|
|
47
|
+
readonly hasNext: boolean;
|
|
48
|
+
nextItems(): Promise<E2BListedSandbox[]>;
|
|
49
|
+
}
|
|
50
|
+
/** One sandbox as the list endpoint describes it. */
|
|
51
|
+
export interface E2BListedSandbox {
|
|
52
|
+
sandboxId: string;
|
|
53
|
+
metadata?: Record<string, string>;
|
|
54
|
+
/** `running` or `paused`. */
|
|
55
|
+
state?: string;
|
|
31
56
|
}
|
|
32
57
|
interface E2BSandboxInfo {
|
|
33
58
|
id?: string;
|
|
@@ -161,6 +186,14 @@ export interface E2BDesktopSandbox {
|
|
|
161
186
|
}
|
|
162
187
|
export declare function loadE2BDesktopModule(): Promise<E2BDesktopModule>;
|
|
163
188
|
export declare const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10000;
|
|
189
|
+
/**
|
|
190
|
+
* Call `observer` with the sandbox id as soon as the guarded SDK constructs the handle for a
|
|
191
|
+
* create made with this exact options object, before desktop startup runs. Startup can take
|
|
192
|
+
* seconds after E2B created the sandbox; this is the earliest point the id exists in this process.
|
|
193
|
+
* A module without the guard (a test fake) never calls it, and create's own result still reports
|
|
194
|
+
* the id.
|
|
195
|
+
*/
|
|
196
|
+
export declare function observeDesktopAllocation(options: E2BDesktopCreateOptions, observer: (sandboxId: string) => void): void;
|
|
164
197
|
type DesktopCreateCleanup = "killed" | "already_gone" | "unconfirmed";
|
|
165
198
|
/** Startup failed after this call acquired a handle. No credentials/options are included here. */
|
|
166
199
|
export declare class E2BDesktopStartupError extends Error {
|
|
@@ -5,27 +5,32 @@
|
|
|
5
5
|
// E2BDesktopSandbox is the command, file, stream, launch and input subset the participants use; `open` is
|
|
6
6
|
// optional because older SDKs lack it (the computer-use route then falls back to launch). It
|
|
7
7
|
// satisfies the executor's E2BDesktopLike port (src/substrates/e2b/desktop-executor.ts) as is.
|
|
8
|
-
import { sep } from "node:path";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
8
|
import { protectDesktopScreenshotCleanup } from "./desktop-screenshot-cleanup.js";
|
|
9
|
+
import { desktopPeerAdvice, humanishInstall } from "./peer-install.js";
|
|
11
10
|
export async function loadE2BDesktopModule() {
|
|
12
11
|
try {
|
|
13
12
|
return guardDesktopSandboxCreate((await import("@e2b/desktop")));
|
|
14
13
|
}
|
|
15
14
|
catch (error) {
|
|
16
15
|
if (isMissingE2BDesktopDependency(error)) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"in this project: `npm i -D @e2b/desktop`."
|
|
20
|
-
: "Live E2B desktop launch requires the optional peer @e2b/desktop, and humanish is running from an " +
|
|
21
|
-
"npx cache. Node resolves the peer relative to humanish itself, so a copy installed in this project " +
|
|
22
|
-
"is not found. Install both into the project and run it from there: " +
|
|
23
|
-
"`npm i -D humanish @e2b/desktop` then `npx humanish run <study>`.");
|
|
16
|
+
const { where, advice } = desktopPeerAdvice(humanishInstall());
|
|
17
|
+
throw new Error(`Live E2B desktop launch requires the optional peer @e2b/desktop${where}. ${advice}`);
|
|
24
18
|
}
|
|
25
19
|
throw error;
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
22
|
export const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10_000;
|
|
23
|
+
const allocationObservers = new WeakMap();
|
|
24
|
+
/**
|
|
25
|
+
* Call `observer` with the sandbox id as soon as the guarded SDK constructs the handle for a
|
|
26
|
+
* create made with this exact options object, before desktop startup runs. Startup can take
|
|
27
|
+
* seconds after E2B created the sandbox; this is the earliest point the id exists in this process.
|
|
28
|
+
* A module without the guard (a test fake) never calls it, and create's own result still reports
|
|
29
|
+
* the id.
|
|
30
|
+
*/
|
|
31
|
+
export function observeDesktopAllocation(options, observer) {
|
|
32
|
+
allocationObservers.set(options, observer);
|
|
33
|
+
}
|
|
29
34
|
/** Startup failed after this call acquired a handle. No credentials/options are included here. */
|
|
30
35
|
export class E2BDesktopStartupError extends Error {
|
|
31
36
|
cleanup;
|
|
@@ -67,6 +72,7 @@ export function guardDesktopSandboxCreate(module) {
|
|
|
67
72
|
: DESKTOP_CREATE_CLEANUP_TIMEOUT_MS;
|
|
68
73
|
let cleanupOwned;
|
|
69
74
|
let restoreKill;
|
|
75
|
+
const observeAllocation = createOptions === undefined ? undefined : allocationObservers.get(createOptions);
|
|
70
76
|
// oxlint-disable-next-line typescript/no-this-alias -- the attempt subclasses whichever SDK class was called
|
|
71
77
|
const CallingSandbox = this;
|
|
72
78
|
class AttemptSandbox extends CallingSandbox {
|
|
@@ -91,6 +97,12 @@ export function guardDesktopSandboxCreate(module) {
|
|
|
91
97
|
else
|
|
92
98
|
Reflect.deleteProperty(this, "kill");
|
|
93
99
|
};
|
|
100
|
+
try {
|
|
101
|
+
observeAllocation?.(this.sandboxId);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// An observer that throws must not fail the create; the resolved handle reports the id.
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
108
|
try {
|
|
@@ -144,18 +156,6 @@ async function reclaimFailedDesktopCreate(kill, timeoutMs) {
|
|
|
144
156
|
clearTimeout(timer);
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Is humanish running from this project's node_modules, or from an npx cache?
|
|
149
|
-
*
|
|
150
|
-
* It decides which advice is true. `npx humanish@latest` resolves its optional peer relative to
|
|
151
|
-
* itself, so "install @e2b/desktop in this project" is advice that cannot work there, and that
|
|
152
|
-
* message cost two cold verification runs before the difference was noticed. A one-shot npx
|
|
153
|
-
* invocation cannot do a live desktop run at all; humanish has to be installed alongside the peer.
|
|
154
|
-
*/
|
|
155
|
-
function runningFromProject() {
|
|
156
|
-
const here = fileURLToPath(import.meta.url);
|
|
157
|
-
return here.startsWith(`${process.cwd()}${sep}node_modules${sep}`);
|
|
158
|
-
}
|
|
159
159
|
function isMissingE2BDesktopDependency(error) {
|
|
160
160
|
const value = error;
|
|
161
161
|
return value.code === "ERR_MODULE_NOT_FOUND" && value.message?.includes("@e2b/desktop") === true;
|
|
@@ -39,6 +39,8 @@ export declare function runOrThrow(shell: Shell, command: string, options?: Shel
|
|
|
39
39
|
export declare function throwOnExit(result: ShellResult): ShellResult;
|
|
40
40
|
/** Single-quote a value for a Shell command line. */
|
|
41
41
|
export declare function shellQuote(value: string): string;
|
|
42
|
+
/** A command-line argument, single-quoted only when the shell would split or expand it. */
|
|
43
|
+
export declare function shellArg(value: string): string;
|
|
42
44
|
/** Sanitized, whitespace-collapsed, length-capped tail of command output. */
|
|
43
45
|
export declare function tailOf(value: string | undefined): string;
|
|
44
46
|
export {};
|
package/dist/substrates/shell.js
CHANGED
|
@@ -16,6 +16,10 @@ export function throwOnExit(result) {
|
|
|
16
16
|
export function shellQuote(value) {
|
|
17
17
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
18
18
|
}
|
|
19
|
+
/** A command-line argument, single-quoted only when the shell would split or expand it. */
|
|
20
|
+
export function shellArg(value) {
|
|
21
|
+
return /^[\w@%+=:,./-]+$/.test(value) ? value : shellQuote(value);
|
|
22
|
+
}
|
|
19
23
|
/** Sanitized, whitespace-collapsed, length-capped tail of command output. */
|
|
20
24
|
export function tailOf(value) {
|
|
21
25
|
return (value ?? "").trim().replace(/\s+/g, " ").slice(-240);
|