humanish 0.110.0 → 0.110.2
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 +15 -12
- package/CHANGELOG.md +26 -0
- package/CONTEXT.md +8 -6
- package/README.md +103 -53
- 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 +17 -0
- package/dist/actors/local-agent/cli.js +111 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +20 -12
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/observe.js +15 -2
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +106 -45
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +113 -74
- package/dist/cli/commands/setup.js +23 -14
- package/dist/cli/commands/study-route-computer-use.js +9 -3
- package/dist/cli/commands/study-route-open.js +1 -0
- package/dist/cli/commands/study-route-preview.js +1 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +6 -2
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/watch.js +14 -4
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +51 -10
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +317 -0
- package/dist/cli/first-run-path.d.ts +32 -11
- package/dist/cli/first-run-path.js +113 -69
- package/dist/cli/invocation.d.ts +7 -0
- package/dist/cli/invocation.js +32 -0
- package/dist/cli/io.d.ts +3 -1
- package/dist/cli/io.js +8 -15
- package/dist/cli/observer-follow.d.ts +20 -0
- package/dist/cli/observer-follow.js +36 -1
- package/dist/cli/orientation.js +16 -14
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +11 -0
- package/dist/evidence/encoded-text.d.ts +9 -6
- package/dist/evidence/encoded-text.js +84 -19
- package/dist/evidence/redaction.d.ts +2 -0
- package/dist/evidence/redaction.js +123 -21
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.js +6 -5
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/exposure.js +1 -1
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +21 -17
- package/dist/observer/render.js +10 -3
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +51 -35
- package/dist/observer/serve.js +2 -1
- 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/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +9 -4
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- 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/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +6 -2
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/terminal/bundle.js +6 -3
- package/dist/routes/terminal/credentials.d.ts +5 -5
- package/dist/routes/terminal/credentials.js +14 -6
- 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 +15 -13
- package/dist/routes/terminal/plan.js +12 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/route.js +5 -4
- package/dist/routes/terminal/runtime-auth.d.ts +6 -0
- package/dist/routes/terminal/runtime-auth.js +5 -0
- package/dist/routes/terminal/session.js +1 -1
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +25 -1
- package/dist/run/bundle.d.ts +24 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +13 -8
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +12 -1
- package/dist/run/local-only-files.d.ts +13 -0
- package/dist/run/local-only-files.js +35 -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 +23 -17
- package/dist/run/reclaim.js +213 -114
- package/dist/run/results.d.ts +10 -20
- 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-not-found.js +3 -2
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +166 -67
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +140 -0
- package/dist/run/sandbox-receipts.d.ts +20 -5
- package/dist/run/sandbox-receipts.js +56 -7
- 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 -23
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -146
- package/dist/run/streams.d.ts +3 -2
- package/dist/study/agents-section.d.ts +21 -0
- package/dist/study/agents-section.js +86 -0
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +8 -4
- package/dist/study/init-templates.js +0 -95
- package/dist/study/init.js +45 -37
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/local-browser-host.d.ts +11 -0
- package/dist/study/local-browser-host.js +42 -0
- package/dist/study/parse/actors.js +9 -1
- package/dist/study/parse/subject.js +12 -0
- package/dist/study/parse/url-credentials.d.ts +9 -0
- package/dist/study/parse/url-credentials.js +50 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/preflight-probes.js +2 -1
- package/dist/study/types.d.ts +8 -6
- package/dist/study/url-credentials.d.ts +6 -0
- package/dist/study/url-credentials.js +34 -0
- 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 +141 -30
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/local/firecracker-desktop.js +3 -2
- package/dist/substrates/local/runtime-host.js +5 -4
- package/dist/substrates/local/runtime.js +5 -4
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui/contract.js +5 -4
- package/dist/tui-app.js +133 -133
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +47 -25
- package/dist/verify/verify.d.ts +3 -0
- package/dist/verify/verify.js +8 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +15 -13
- package/docs/contracts/run-bundle.md +95 -10
- package/docs/contracts/schemas.md +44 -26
- 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 +6 -3
- package/skills/humanish/SKILL.md +38 -20
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Every URL a study config declares, checked for a credential. Route planners call it, since a
|
|
2
|
+
// library caller's config skips the parser that refuses these first.
|
|
3
|
+
import { rosterOf } from "./parse/actors.js";
|
|
4
|
+
import { entryCredentialReason, urlCredentialReason } from "./parse/url-credentials.js";
|
|
5
|
+
/**
|
|
6
|
+
* The first URL in `config` that carries a credential, with why: the subject's app, serve and
|
|
7
|
+
* public surface URLs, and each participant's target and entry.
|
|
8
|
+
*/
|
|
9
|
+
export function studyUrlCredentialReason(config) {
|
|
10
|
+
const { subject } = config;
|
|
11
|
+
const reasons = [
|
|
12
|
+
() => subject.appUrl === undefined
|
|
13
|
+
? undefined
|
|
14
|
+
: urlCredentialReason("subject.appUrl", subject.appUrl),
|
|
15
|
+
() => subject.serve === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: urlCredentialReason("subject.serve.url", subject.serve.url),
|
|
18
|
+
...(subject.product?.publicSurfaces ?? []).map((surface) => () => urlCredentialReason("subject.product.publicSurfaces", surface)),
|
|
19
|
+
...config.actors.flatMap((actor) => (rosterOf(actor) ?? []).flatMap((participant, index) => [
|
|
20
|
+
() => participant.target === undefined
|
|
21
|
+
? undefined
|
|
22
|
+
: urlCredentialReason(`participants[${index}].target`, participant.target),
|
|
23
|
+
() => participant.entry === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: entryCredentialReason(`participants[${index}].entry`, participant.entry),
|
|
26
|
+
])),
|
|
27
|
+
];
|
|
28
|
+
for (const reason of reasons) {
|
|
29
|
+
const found = reason();
|
|
30
|
+
if (found !== undefined)
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
type Manager = "npm" | "pnpm";
|
|
2
|
+
/** Where the running humanish is installed, read from its own file's real path. */
|
|
3
|
+
export type HumanishInstall =
|
|
4
|
+
/** The nearest package.json declaring humanish: the working directory's, or `up` levels above. */
|
|
5
|
+
{
|
|
6
|
+
kind: "project";
|
|
7
|
+
up: number;
|
|
8
|
+
manager: Manager;
|
|
9
|
+
}
|
|
10
|
+
/** A one-shot runner's cache: `npx humanish` or `pnpm dlx humanish`. */
|
|
11
|
+
| {
|
|
12
|
+
kind: "one-shot";
|
|
13
|
+
unmanagedCwd: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** The global directory npm installs to from here, or pnpm's global directory. */
|
|
16
|
+
| {
|
|
17
|
+
kind: "global";
|
|
18
|
+
manager: Manager;
|
|
19
|
+
}
|
|
20
|
+
/** A source checkout of humanish, run with node or tsx. */
|
|
21
|
+
| {
|
|
22
|
+
kind: "checkout";
|
|
23
|
+
}
|
|
24
|
+
/** Anywhere else, such as another project or a global prefix npm is not configured for here. */
|
|
25
|
+
| {
|
|
26
|
+
kind: "other";
|
|
27
|
+
unmanagedCwd: boolean;
|
|
28
|
+
};
|
|
29
|
+
export interface InstallContext {
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** The directory npm's global installs go to from this environment. */
|
|
32
|
+
npmGlobalRoot: string;
|
|
33
|
+
/** Whether the package.json in `directory` declares humanish as a dependency. */
|
|
34
|
+
declaresHumanish: (directory: string) => boolean;
|
|
35
|
+
/** Whether `npm i` from `directory` installs into a node_modules no package.json declares. */
|
|
36
|
+
unmanaged: (directory: string) => boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The install `modulePath` belongs to. */
|
|
39
|
+
export declare function classifyHumanishInstall(modulePath: string, context: InstallContext): HumanishInstall;
|
|
40
|
+
/** The running humanish's install, from this file's real path. */
|
|
41
|
+
export declare function humanishInstall(cwd?: string): HumanishInstall;
|
|
42
|
+
/**
|
|
43
|
+
* Where this humanish is installed, the command that puts `spec` (`@e2b/desktop`, or a version of
|
|
44
|
+
* it) where it will resolve, and that command as one or two sentences of advice.
|
|
45
|
+
*/
|
|
46
|
+
export declare function desktopPeerAdvice(install: HumanishInstall, spec?: string): {
|
|
47
|
+
where: string;
|
|
48
|
+
command: string;
|
|
49
|
+
advice: string;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// How to add the optional @e2b/desktop peer depends on where humanish itself is installed: Node
|
|
2
|
+
// resolves the peer from humanish's own directory, not from the project a command runs in. Advice
|
|
3
|
+
// for the wrong place installs a copy Node never reads, and `npm i` in a directory whose manifest
|
|
4
|
+
// does not declare what its node_modules holds prunes it, so a command names only a project whose
|
|
5
|
+
// package.json declares humanish, or npm's or pnpm's global directory.
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { dirname, join, sep } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
const NODE_MODULES = `${sep}node_modules${sep}`;
|
|
11
|
+
/** The install `modulePath` belongs to. */
|
|
12
|
+
export function classifyHumanishInstall(modulePath, context) {
|
|
13
|
+
const { cwd } = context;
|
|
14
|
+
const segments = modulePath.split(sep);
|
|
15
|
+
if (segments.includes("_npx") || segments.includes("dlx"))
|
|
16
|
+
return { kind: "one-shot", unmanagedCwd: context.unmanaged(cwd) };
|
|
17
|
+
if (modulePath.includes(`${sep}pnpm${sep}global${sep}`))
|
|
18
|
+
return { kind: "global", manager: "pnpm" };
|
|
19
|
+
const at = modulePath.indexOf(NODE_MODULES);
|
|
20
|
+
if (at < 0)
|
|
21
|
+
return { kind: "checkout" };
|
|
22
|
+
// The first node_modules is the project's; pnpm keeps humanish's files deeper, in .pnpm.
|
|
23
|
+
const root = modulePath.slice(0, at);
|
|
24
|
+
if (root === context.npmGlobalRoot)
|
|
25
|
+
return { kind: "global", manager: "npm" };
|
|
26
|
+
if (cwd === root || cwd.startsWith(`${root}${sep}`)) {
|
|
27
|
+
const manager = modulePath.includes(`${NODE_MODULES}.pnpm${sep}`) ? "pnpm" : "npm";
|
|
28
|
+
// A workspace member declares humanish in its own package.json, and the root may not.
|
|
29
|
+
for (let directory = cwd, up = 0;; directory = dirname(directory), up += 1) {
|
|
30
|
+
if (context.declaresHumanish(directory))
|
|
31
|
+
return { kind: "project", up, manager };
|
|
32
|
+
if (directory === root)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { kind: "other", unmanagedCwd: context.unmanaged(cwd) };
|
|
37
|
+
}
|
|
38
|
+
/** The `prefix` an npmrc file sets, with `~` and `$HOME` expanded; undefined when it sets none. */
|
|
39
|
+
function npmrcPrefix(file) {
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = readFileSync(file, "utf8");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const value = /^\s*prefix\s*=\s*(.+?)\s*$/m.exec(text)?.[1];
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
return value
|
|
51
|
+
.replace(/^~(?=$|[\\/])/, homedir())
|
|
52
|
+
.replace(/\$\{?HOME\}?/g, homedir())
|
|
53
|
+
.replace(/^"(.*)"$/, "$1");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The global root npm installs to from this environment, by npm's own order: the prefix in the
|
|
57
|
+
* environment, then the user, global and built-in npmrc files (Homebrew sets it in the built-in
|
|
58
|
+
* one), then the prefix the Node binary implies.
|
|
59
|
+
*/
|
|
60
|
+
function npmGlobalRoot() {
|
|
61
|
+
const windows = process.platform === "win32";
|
|
62
|
+
const nodePrefix = windows ? dirname(process.execPath) : dirname(dirname(process.execPath));
|
|
63
|
+
const prefix = process.env.npm_config_prefix ||
|
|
64
|
+
process.env.NPM_CONFIG_PREFIX ||
|
|
65
|
+
npmrcPrefix(process.env.NPM_CONFIG_USERCONFIG || join(homedir(), ".npmrc")) ||
|
|
66
|
+
npmrcPrefix(process.env.NPM_CONFIG_GLOBALCONFIG || join(nodePrefix, "etc", "npmrc")) ||
|
|
67
|
+
npmrcPrefix(windows
|
|
68
|
+
? join(nodePrefix, "node_modules", "npm", "npmrc")
|
|
69
|
+
: join(nodePrefix, "lib", "node_modules", "npm", "npmrc")) ||
|
|
70
|
+
nodePrefix;
|
|
71
|
+
return windows ? prefix : join(prefix, "lib");
|
|
72
|
+
}
|
|
73
|
+
function declaresHumanish(directory) {
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
76
|
+
return ["dependencies", "devDependencies", "optionalDependencies"].some((field) => {
|
|
77
|
+
const declared = manifest[field];
|
|
78
|
+
return typeof declared === "object" && declared !== null && "humanish" in declared;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether `npm i` from `directory` would install into a node_modules no package.json declares: npm
|
|
87
|
+
* installs into the nearest directory, this one or above, that holds either.
|
|
88
|
+
*/
|
|
89
|
+
function unmanaged(directory) {
|
|
90
|
+
for (let current = directory;; current = dirname(current)) {
|
|
91
|
+
if (existsSync(join(current, "package.json")))
|
|
92
|
+
return false;
|
|
93
|
+
if (existsSync(join(current, "node_modules")))
|
|
94
|
+
return true;
|
|
95
|
+
if (dirname(current) === current)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The running humanish's install, from this file's real path. */
|
|
100
|
+
export function humanishInstall(cwd = process.cwd()) {
|
|
101
|
+
return classifyHumanishInstall(fileURLToPath(import.meta.url), {
|
|
102
|
+
cwd,
|
|
103
|
+
npmGlobalRoot: npmGlobalRoot(),
|
|
104
|
+
declaresHumanish,
|
|
105
|
+
unmanaged,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Where this humanish is installed, the command that puts `spec` (`@e2b/desktop`, or a version of
|
|
110
|
+
* it) where it will resolve, and that command as one or two sentences of advice.
|
|
111
|
+
*/
|
|
112
|
+
export function desktopPeerAdvice(install, spec = "@e2b/desktop") {
|
|
113
|
+
const both = `npm i -D humanish ${spec}`;
|
|
114
|
+
const relativeTo = "Node resolves the peer relative to humanish itself, so";
|
|
115
|
+
switch (install.kind) {
|
|
116
|
+
case "project": {
|
|
117
|
+
// A directory option, not `cd`: one command, and no `&&` for Windows PowerShell 5.1.
|
|
118
|
+
const ups = Array.from({ length: install.up }, () => "..").join("/");
|
|
119
|
+
const command = install.manager === "pnpm"
|
|
120
|
+
? `pnpm add -D ${ups ? `--dir ${ups} ` : ""}${spec}`
|
|
121
|
+
: `npm i -D ${ups ? `--prefix ${ups} ` : ""}${spec}`;
|
|
122
|
+
const place = ups ? "the project above this directory" : "this project";
|
|
123
|
+
return {
|
|
124
|
+
where: "",
|
|
125
|
+
command,
|
|
126
|
+
advice: `Install it beside humanish in ${place}: \`${command}\`.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
case "global": {
|
|
130
|
+
const command = install.manager === "pnpm" ? `pnpm add -g ${spec}` : `npm i -g ${spec}`;
|
|
131
|
+
return {
|
|
132
|
+
where: ", and humanish is installed globally",
|
|
133
|
+
command,
|
|
134
|
+
advice: `${relativeTo} install it globally too: \`${command}\`. That reaches humanish while ` +
|
|
135
|
+
"the global directory is still the one it was installed in.",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
case "checkout":
|
|
139
|
+
return {
|
|
140
|
+
where: ", and humanish is running from a source checkout",
|
|
141
|
+
command: "pnpm install",
|
|
142
|
+
advice: spec === "@e2b/desktop"
|
|
143
|
+
? "Run `pnpm install` in the humanish checkout, which installs it as a dev dependency."
|
|
144
|
+
: `Set ${spec} in the humanish checkout's package.json, then run \`pnpm install\` there.`,
|
|
145
|
+
};
|
|
146
|
+
case "one-shot":
|
|
147
|
+
case "other": {
|
|
148
|
+
const where = install.kind === "one-shot"
|
|
149
|
+
? ", and humanish is running from an npx or pnpm dlx cache"
|
|
150
|
+
: ", and humanish is installed outside this project";
|
|
151
|
+
const run = `\`${both}\` then \`npx humanish run <study>\``;
|
|
152
|
+
return {
|
|
153
|
+
where,
|
|
154
|
+
command: both,
|
|
155
|
+
advice: install.unmanagedCwd
|
|
156
|
+
? `${relativeTo} it has to be installed with humanish. From here npm would install into a node_modules folder that no package.json declares, and \`npm i\` would remove what it holds. In your project's directory, run ${run}.`
|
|
157
|
+
: `${relativeTo} a copy installed here is not found. Install both into this project and run that copy: ${run}.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -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,17 @@
|
|
|
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. The guard
|
|
8
|
+
// reports the id before desktop startup begins, and the id goes to the registry and the receipt
|
|
9
|
+
// journal at that point, so a startup error that quotes it is scrubbed from every run file.
|
|
7
10
|
import { redactText, toErrorMessage } from "../../evidence/redaction.js";
|
|
8
|
-
import {
|
|
11
|
+
import { beginSandboxCreate, recordSandboxOwnerOnce, SandboxCreateRefusedError, sandboxOwnerTags, } from "../../run/sandbox-creates.js";
|
|
12
|
+
import { appendSandboxOwner, appendSandboxReceipt } from "../../run/sandbox-receipts.js";
|
|
9
13
|
import { ownDesktopAllocation, } from "../desktop-session.js";
|
|
10
|
-
import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, } from "./sdk.js";
|
|
14
|
+
import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, observeDesktopAllocation, } from "./sdk.js";
|
|
11
15
|
/** A desktop sandbox, on the stock `desktop` template or a named one. */
|
|
12
16
|
export function acquireE2BDesktopSandbox(request) {
|
|
13
17
|
return acquire(request, request.template);
|
|
@@ -17,33 +21,74 @@ export function acquireE2BShellSandbox(request) {
|
|
|
17
21
|
return acquire(request, undefined);
|
|
18
22
|
}
|
|
19
23
|
async function acquire(request, template) {
|
|
20
|
-
const { module
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const { module } = request;
|
|
25
|
+
const target = request.receipt;
|
|
26
|
+
// Refused before any provider call once the run is stopping.
|
|
27
|
+
const ticket = target === null ? undefined : beginSandboxCreate(target.root, target.participantId);
|
|
28
|
+
// The receipts written when the guard reported an id, before desktop startup. A startup error
|
|
29
|
+
// can quote the id and the create may never return, so the journal holds it first.
|
|
30
|
+
const reported = new Map();
|
|
26
31
|
try {
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// The owner tags let reclaim find this sandbox on E2B even if its id never reaches this
|
|
33
|
+
// process: a create that throws after E2B allocated, or a process that dies mid-create. The
|
|
34
|
+
// journal records them first, so reclaim knows which tags to search for.
|
|
35
|
+
const owner = target === null ? undefined : sandboxOwnerTags(target.root);
|
|
36
|
+
if (target !== null && owner !== undefined)
|
|
37
|
+
await recordSandboxOwnerOnce(target.root, () => appendSandboxOwner(target.root, owner));
|
|
38
|
+
const options = owner === undefined
|
|
39
|
+
? request.options
|
|
40
|
+
: { ...request.options, metadata: { ...request.options.metadata, ...owner } };
|
|
41
|
+
const receipt = (sandboxId) => {
|
|
42
|
+
if (target === null)
|
|
43
|
+
return Promise.resolve(false);
|
|
44
|
+
const { root, participantId, now = Date.now } = target;
|
|
45
|
+
return appendSandboxReceipt(root, {
|
|
32
46
|
at: new Date(now()).toISOString(),
|
|
33
47
|
laneId: participantId,
|
|
34
48
|
provider: "e2b",
|
|
35
|
-
sandboxId
|
|
49
|
+
sandboxId,
|
|
36
50
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
37
51
|
});
|
|
52
|
+
};
|
|
53
|
+
if (ticket !== undefined)
|
|
54
|
+
observeDesktopAllocation(options, (sandboxId) => {
|
|
55
|
+
ticket.created(sandboxId);
|
|
56
|
+
// Started synchronously, so the run's writers know the id at once. A clock that throws
|
|
57
|
+
// fails only this write; the receipt after create throws it again.
|
|
58
|
+
if (!reported.has(sandboxId))
|
|
59
|
+
reported.set(sandboxId, new Promise((resolve) => resolve(receipt(sandboxId))).catch(() => false));
|
|
60
|
+
});
|
|
61
|
+
const sandbox = await withOneRetryOnTransientE2BError(() => {
|
|
62
|
+
if (ticket?.stopping())
|
|
63
|
+
throw new SandboxCreateRefusedError();
|
|
64
|
+
// Without a template the SDK's one-argument create chooses its stock desktop template.
|
|
65
|
+
return template === undefined
|
|
66
|
+
? module.Sandbox.create(options)
|
|
67
|
+
: module.Sandbox.create(template, options);
|
|
68
|
+
}, { ...request.retry, canRetry: () => ticket?.stopping() !== true });
|
|
69
|
+
ticket?.created(sandbox.sandboxId);
|
|
70
|
+
const allocation = ownE2BSandbox(module, sandbox.sandboxId, ticket);
|
|
71
|
+
try {
|
|
72
|
+
// Best effort by contract: a failed write leaves the owner tags and the TTL as the
|
|
73
|
+
// backstops and never fails the participant. One line per sandbox: a receipt the guard's
|
|
74
|
+
// report wrote is not written again, so reclaim counts the sandbox once.
|
|
75
|
+
if (!(await reported.get(allocation.resourceId)))
|
|
76
|
+
await receipt(allocation.resourceId);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
// The caller never receives this sandbox, so release it here. A throwing or invalid injected
|
|
80
|
+
// clock is the known case.
|
|
81
|
+
await allocation.close();
|
|
82
|
+
throw error;
|
|
38
83
|
}
|
|
84
|
+
return { sandbox, allocation };
|
|
39
85
|
}
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
await
|
|
44
|
-
|
|
86
|
+
finally {
|
|
87
|
+
// A create that threw settles once its reported receipts are on disk, as one that returned
|
|
88
|
+
// does. These writes never reject.
|
|
89
|
+
await Promise.all(reported.values());
|
|
90
|
+
ticket?.settled();
|
|
45
91
|
}
|
|
46
|
-
return { sandbox, allocation };
|
|
47
92
|
}
|
|
48
93
|
/** The provider's kill method bound to its class, or undefined when the SDK has none. */
|
|
49
94
|
function boundKill(module) {
|
|
@@ -57,7 +102,7 @@ function boundKill(module) {
|
|
|
57
102
|
* recognized by type, means the same. Every other throw is unconfirmed, whatever its message
|
|
58
103
|
* says: the SDK answers a 404 with false before it throws, so "not found" or "404" in a thrown
|
|
59
104
|
* message comes from some other failure, such as a trace id. A true in debug mode sent no
|
|
60
|
-
* request, so it confirms nothing.
|
|
105
|
+
* request, so it confirms nothing.
|
|
61
106
|
*/
|
|
62
107
|
async function killById(kill, sandboxId, options) {
|
|
63
108
|
if (kill === undefined)
|
|
@@ -84,12 +129,18 @@ async function killById(kill, sandboxId, options) {
|
|
|
84
129
|
return { status: "unconfirmed", reason: "release_failed", error };
|
|
85
130
|
}
|
|
86
131
|
}
|
|
87
|
-
function ownE2BSandbox(module, resourceId) {
|
|
132
|
+
function ownE2BSandbox(module, resourceId, ticket) {
|
|
88
133
|
// Bind the provider method now, before any hook can replace it on the shared module.
|
|
89
134
|
const kill = boundKill(module);
|
|
90
135
|
return ownDesktopAllocation({
|
|
91
136
|
resourceId,
|
|
92
|
-
release: () =>
|
|
137
|
+
release: async () => {
|
|
138
|
+
const released = await killById(kill, resourceId, { requestTimeoutMs: 60_000 });
|
|
139
|
+
// The signal handler skips a sandbox its route already released.
|
|
140
|
+
if (released.status === "released")
|
|
141
|
+
ticket?.released(resourceId);
|
|
142
|
+
return released;
|
|
143
|
+
},
|
|
93
144
|
});
|
|
94
145
|
}
|
|
95
146
|
/**
|
|
@@ -136,10 +187,8 @@ export function releaseUnavailableDetail(detail) {
|
|
|
136
187
|
? "installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox"
|
|
137
188
|
: `${detail}; if the sandbox exists, server-side kill-on-timeout will reclaim it`;
|
|
138
189
|
}
|
|
139
|
-
/**
|
|
140
|
-
*
|
|
141
|
-
* reclaim's persisted outcome. It never lists the account.
|
|
142
|
-
*/
|
|
190
|
+
/** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
|
|
191
|
+
* reclaim's persisted outcome. */
|
|
143
192
|
export async function destroyE2BSandbox(module, sandboxId, options) {
|
|
144
193
|
const released = await killById(boundKill(module), sandboxId, options);
|
|
145
194
|
if (released.status === "released")
|
|
@@ -156,6 +205,68 @@ export async function destroyE2BSandbox(module, sandboxId, options) {
|
|
|
156
205
|
return { state: "kill-failed", detail: redactText(toErrorMessage(released.error)) };
|
|
157
206
|
}
|
|
158
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
|
|
210
|
+
* type, means gone; any other error leaves the answer open.
|
|
211
|
+
*/
|
|
212
|
+
export async function inspectE2BSandbox(module, sandboxId, options) {
|
|
213
|
+
if (typeof module.Sandbox.getInfo !== "function")
|
|
214
|
+
return {
|
|
215
|
+
state: "check-failed",
|
|
216
|
+
detail: "the installed @e2b/desktop SDK has no Sandbox.getInfo",
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
await module.Sandbox.getInfo(sandboxId, options);
|
|
220
|
+
return { state: "running" };
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (isSandboxNotFoundError(error))
|
|
224
|
+
return { state: "already-gone" };
|
|
225
|
+
return { state: "check-failed", detail: redactText(toErrorMessage(error)) };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const TAG_SEARCH_PAGE_SIZE = 100;
|
|
229
|
+
// One run tags at most a few dozen sandboxes. More pages than this means the filter was not
|
|
230
|
+
// applied, and reading on would walk the whole account.
|
|
231
|
+
const TAG_SEARCH_MAX_PAGES = 3;
|
|
232
|
+
/**
|
|
233
|
+
* List the sandboxes E2B matches to every one of `tags`, filtered server-side. Each result is
|
|
234
|
+
* checked against every tag again, so a server that ignored the filter still cannot hand back
|
|
235
|
+
* another run's sandbox, and the search stops after a few pages instead of walking the account.
|
|
236
|
+
*/
|
|
237
|
+
export async function findE2BSandboxesByTags(module, tags, options) {
|
|
238
|
+
if (typeof module.Sandbox.list !== "function")
|
|
239
|
+
return {
|
|
240
|
+
status: "unavailable",
|
|
241
|
+
sandboxes: [],
|
|
242
|
+
detail: "the installed @e2b/desktop SDK has no Sandbox.list",
|
|
243
|
+
};
|
|
244
|
+
const sandboxes = [];
|
|
245
|
+
try {
|
|
246
|
+
const pages = module.Sandbox.list({
|
|
247
|
+
query: { metadata: tags },
|
|
248
|
+
limit: TAG_SEARCH_PAGE_SIZE,
|
|
249
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
250
|
+
});
|
|
251
|
+
for (let page = 0; pages.hasNext; page += 1) {
|
|
252
|
+
if (page === TAG_SEARCH_MAX_PAGES)
|
|
253
|
+
return {
|
|
254
|
+
status: "failed",
|
|
255
|
+
sandboxes,
|
|
256
|
+
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`,
|
|
257
|
+
};
|
|
258
|
+
for (const listed of await pages.nextItems()) {
|
|
259
|
+
const metadata = listed.metadata ?? {};
|
|
260
|
+
if (Object.entries(tags).every(([key, value]) => metadata[key] === value))
|
|
261
|
+
sandboxes.push(listed);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { status: "done", sandboxes };
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
return { status: "failed", sandboxes, detail: redactText(toErrorMessage(error)) };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
159
270
|
/** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
|
|
160
271
|
export const E2B_SPEECH_TEMPLATE = "7409n13kr83f7g7abx5g";
|
|
161
272
|
/** The desktop template a study asks for; undefined selects the SDK default. */
|
|
@@ -206,7 +317,7 @@ export async function withOneRetryOnTransientE2BError(attempt, hooks) {
|
|
|
206
317
|
return await attempt();
|
|
207
318
|
}
|
|
208
319
|
catch (error) {
|
|
209
|
-
if (!isTransientE2BError(error))
|
|
320
|
+
if (!isTransientE2BError(error) || hooks?.canRetry?.() === false)
|
|
210
321
|
throw error;
|
|
211
322
|
const reason = error instanceof Error ? error.message : String(error);
|
|
212
323
|
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 {
|