humanish 0.110.2 → 0.110.3
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 +10 -15
- package/CHANGELOG.md +13 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/registry.js +1 -1
- package/dist/analysis/automatic-completion.js +8 -0
- package/dist/cli/commands/observe.js +3 -2
- package/dist/cli/commands/study-run.js +9 -19
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +21 -0
- package/dist/cli/io.d.ts +2 -1
- package/dist/cli/io.js +3 -2
- package/dist/cli/observer-follow.d.ts +6 -2
- package/dist/cli/observer-follow.js +15 -5
- package/dist/evidence/redaction.js +15 -4
- package/dist/routes/computer-use/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +1 -1
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +17 -78
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +10 -42
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +24 -63
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +19 -59
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +2 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- package/dist/run/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run.d.ts +6 -1
- package/dist/run/run.js +11 -2
- package/dist/run/status.d.ts +2 -0
- package/dist/run/status.js +5 -0
- package/dist/study/composition-rules.js +1 -1
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -34
- package/dist/study/init.js +1 -1
- package/dist/study/parse/subject.js +1 -1
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +13 -13
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -9,25 +9,20 @@ longer declares that name. [CONTEXT.md](CONTEXT.md) defines the domain terms, an
|
|
|
9
9
|
|
|
10
10
|
The steps follow a live computer-use study on a hosted E2B desktop. Every route shares steps 1, 2, 7
|
|
11
11
|
and 8. Steps 3 to 6 run in the route's admit function and the `run()` it returns, which `runStudyWith`
|
|
12
|
-
reaches through `admitPlan` (`src/run-study.ts`)
|
|
12
|
+
reaches through `admitPlan` (`src/run-study.ts`). Four of the admit functions hand their checks and
|
|
13
|
+
the body they run inside the run scope to `admitRoute` (`src/run/route-shell.ts`). It completes the
|
|
14
|
+
automatic analysis of a refusal, and its `run()` opens the run scope, runs the body and completes
|
|
15
|
+
the analysis of the run:
|
|
13
16
|
|
|
14
|
-
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`)
|
|
15
|
-
`
|
|
16
|
-
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`); its `run()` calls
|
|
21
|
-
`runAdmittedSharedWorldRun` (`src/routes/shared-world/route.ts`).
|
|
17
|
+
- computer-use: `admitComputerUsePlan` (`src/routes/computer-use/route.ts`), with the checks in
|
|
18
|
+
`admitCuaRun` (`src/routes/computer-use/setup.ts`).
|
|
19
|
+
- scripted: `admitScriptedPlan` (`src/routes/scripted/route.ts`), whose checks run inside the run.
|
|
20
|
+
- terminal: `admitTerminalPlan` (`src/routes/terminal/route.ts`), with a live plan's checks in
|
|
21
|
+
`checkLiveTerminalMachine` (`src/routes/terminal/session.ts`).
|
|
22
|
+
- shared-world: `admitSharedWorldPlan` (`src/routes/shared-world/route.ts`).
|
|
22
23
|
- preview: `admitPreviewPlan` (`src/routes/preview.ts`); its `run()` calls `runPreviewPlan`
|
|
23
24
|
(`src/routes/preview.ts`), which publishes a fixture bundle.
|
|
24
25
|
|
|
25
|
-
`runComputerUsePlan` (`src/routes/computer-use/route.ts`), `runTerminalPlan`
|
|
26
|
-
(`src/routes/terminal/route.ts`) and `runSharedWorldPlan` (`src/routes/shared-world/route.ts`)
|
|
27
|
-
admit and run in one call. Only tests call them, directly or through the route modules'
|
|
28
|
-
`runCuaActorStudy`, `runTerminalProductStudy` and `runConcurrentSharedWorld`. `runStudyWith` and the CLI do
|
|
29
|
-
not.
|
|
30
|
-
|
|
31
26
|
1. **Parse.** `runStudyCommand` (`src/cli/commands/study-run.ts`) calls `resolveStudyManifest`
|
|
32
27
|
(`src/study/discover.ts`), which calls `parseStudy` (`src/study/config.ts`). It rejects unknown
|
|
33
28
|
keys and the compositions the
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.110.3: Ctrl-C at the watch prompt, review says analysis is off, E2B docs links kept (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.110.3 runs the `watch` and `observe` shutdown, and prints `watch stopped` or `observe
|
|
14
|
+
stopped`, when a Ctrl-C lands right at their prompt. It starts no automatic analysis for a run a
|
|
15
|
+
signal interrupted. `humanish review` says when a study's `review.analysis: false` is why a live
|
|
16
|
+
run has no findings. `humanish verify` no longer blocks a run whose text holds a URL followed by an
|
|
17
|
+
escaped line break and an `E2B_` variable, and redacted output keeps the E2B docs link that E2B's
|
|
18
|
+
401 names. The `--env-file` alias is now removed in 0.112.0. Inside, the computer-use, scripted,
|
|
19
|
+
terminal and shared-world routes run through one shell, `admitRoute` in `src/run/route-shell.ts`;
|
|
20
|
+
the live smoke ran each route on it.
|
|
21
|
+
|
|
22
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.3)
|
|
23
|
+
|
|
11
24
|
## 0.110.2: Credential URLs refused, terminal key proxied, watch exits for agents (2026-10-05)
|
|
12
25
|
|
|
13
26
|
humanish 0.110.2 refuses a study URL that carries a user name, password or credential parameter,
|
package/CONTEXT.md
CHANGED
|
@@ -28,7 +28,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
28
28
|
| computer use | A model drives a participant by reading screenshots of a desktop and acting with mouse and keyboard. `openai-computer-use` and `local-agent` are the computer-use actors. CUA is the short form in code names such as `CuaExecutor` and in the older spellings listed under `cua` below. | `src/routes/computer-use/`, `src/actors/computer-use/` |
|
|
29
29
|
| fan-out | A computer-use run with more than one participant, each on its own desktop, or any rerun. It writes the fan-out bundle shape and passes only when every expected participant passes. One participant without a rerun keeps the single-participant shape. | `buildCuaFanoutBundle` in `src/routes/computer-use/fanout-bundle.ts`; `judgeComputerUseRun` in `src/routes/computer-use/bundle.ts` |
|
|
30
30
|
| route | The execution path for a study: `computer-use`, `shared-world`, `scripted`, `terminal` or `preview`. `preview` writes a dry-run bundle for `this-repo` subjects with no live participant. | `routeOf` and `StudyRoute` in `src/study/plan.ts`; predicates in `src/study/routing.ts`; one folder or file per route in `src/routes/` |
|
|
31
|
-
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's
|
|
31
|
+
| plan | What `planStudy` decides about a study before anything runs: the study id, whether it is a dry run, the analysis settings and the keys, connections and env vars the run needs, plus the route's participants, caps and own fields. There is one variant per route. `planStudy` returns a plan, or a refusal with the route's error code, and the route's admit function (`admitComputerUsePlan` and the others) takes the plan. | `StudyPlan` in `src/study/plan-types.ts`; `planStudy` in `src/study/plan.ts`; each route's planner in its `plan.ts` |
|
|
32
32
|
| substrate | Where a participant's desktop or shell runs: a hosted E2B sandbox, or a local Firecracker VM (inside Lima on macOS). | `src/substrates/e2b/`, `src/substrates/local/` |
|
|
33
33
|
| participant desktop | The desktop one computer-use or shared-world participant drives: a substrate plus the route's concerns, such as subject provisioning, comms and the participant's plan. It lives in the route, so `src/substrates/` holds only provider primitives ([ADR 0004](docs/decisions/0004-participant-desktops-stay-in-the-route.md)). | `ParticipantDesktop` in `src/routes/computer-use/participant-desktop.ts`; `createE2BParticipantDesktop` in `src/routes/computer-use/e2b-desktop/desktop.ts`; the local VM's in `src/routes/computer-use/local-vm.ts` |
|
|
34
34
|
| receipt | A line in a run's `sandbox-receipts.ndjson` that records a sandbox's id, provider, the participant or subject it belongs to (`laneId`), and its time limit as soon as the sandbox is created, before any work runs in it. `humanish reclaim` kills sandboxes by these ids after an interrupted run. | `SandboxReceipt` in `src/run/sandbox-receipts.ts`; `reclaimRunSandboxes` in `src/run/reclaim.ts` |
|
|
@@ -63,7 +63,7 @@ export async function runCuaActorSession(options) {
|
|
|
63
63
|
}
|
|
64
64
|
function defaultProvider(openai, strictSpend = false) {
|
|
65
65
|
if (!openai) {
|
|
66
|
-
throw new Error("
|
|
66
|
+
throw new Error("A computer-use session requires either `provider` (injected) or `openai` provider options.");
|
|
67
67
|
}
|
|
68
68
|
return createOpenAiResponsesProvider({
|
|
69
69
|
...openai,
|
|
@@ -72,7 +72,7 @@ function defaultProvider(openai, strictSpend = false) {
|
|
|
72
72
|
}
|
|
73
73
|
function defaultExecutor(desktop, executorOptions) {
|
|
74
74
|
if (!desktop) {
|
|
75
|
-
throw new Error("
|
|
75
|
+
throw new Error("A computer-use session requires either `executor` (injected) or `desktop` to build one.");
|
|
76
76
|
}
|
|
77
77
|
return createE2BDesktopExecutor(desktop, executorOptions ?? {});
|
|
78
78
|
}
|
|
@@ -96,7 +96,7 @@ export function spendLimit(session, estimate, maxUsd) {
|
|
|
96
96
|
// trip.
|
|
97
97
|
export const nonFiniteEstimate = {
|
|
98
98
|
completionReason: "harness_error",
|
|
99
|
-
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one
|
|
99
|
+
reason: "the injected estimateTurnCostUsd returned a non-finite estimate while caps.maxUsd is set; the estimator receives one object with the running token totals. Failing closed instead of running uncapped.",
|
|
100
100
|
};
|
|
101
101
|
/** A study-level stop is a recruiting decision hitting its limit, not this participant's runaway. */
|
|
102
102
|
export function runSpendLimit(reason) {
|
package/dist/actors/registry.js
CHANGED
|
@@ -74,7 +74,7 @@ export const actorRegistry = {
|
|
|
74
74
|
},
|
|
75
75
|
// The ActorId names the terminal-product dispatch slot; the live route records the concrete
|
|
76
76
|
// provider as "codex". keyPlacement "in-sandbox-command-scoped" is registry-declared and
|
|
77
|
-
// enforced by
|
|
77
|
+
// enforced by the terminal route before it creates a sandbox, which is where this actor runs.
|
|
78
78
|
"codex-exec": {
|
|
79
79
|
id: "codex-exec",
|
|
80
80
|
label: "Codex Exec (autonomous terminal agent, in-sandbox)",
|
|
@@ -25,6 +25,14 @@ export async function completeAutomaticAnalysis(result, finished, config, input,
|
|
|
25
25
|
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE" },
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
// A signal is stopping this process: the run is recorded interrupted, and the process exits
|
|
29
|
+
// once its sandboxes are reclaimed, so no analysis starts.
|
|
30
|
+
if (finished.interrupted)
|
|
31
|
+
return {
|
|
32
|
+
...result,
|
|
33
|
+
...origin,
|
|
34
|
+
automaticAnalysis: { state: "skipped", reason: "AUTOMATIC_ANALYSIS_ACTOR_CANCELLED" },
|
|
35
|
+
};
|
|
28
36
|
const prepared = finished.paths;
|
|
29
37
|
const seams = input?.deps?.analysis;
|
|
30
38
|
const signal = input?.analysisSignal;
|
|
@@ -164,8 +164,6 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
164
164
|
// Keep the JSON envelope on stdout clean: route attach/stop chatter to stderr
|
|
165
165
|
// for machine output, and to stdout for humans.
|
|
166
166
|
const note = options.json ? io.writeErr : io.writeOut;
|
|
167
|
-
note(`serving: ${server.url}\n`);
|
|
168
|
-
note("serving: press Ctrl-C to stop\n");
|
|
169
167
|
await new Promise((resolveWait) => {
|
|
170
168
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
171
169
|
const handlers = new Map();
|
|
@@ -195,6 +193,9 @@ async function serveObserveUntilSignal(io, server, options) {
|
|
|
195
193
|
handlers.set(signal, handler);
|
|
196
194
|
process.once(signal, handler);
|
|
197
195
|
}
|
|
196
|
+
// The prompt follows the handlers, as in followObserver.
|
|
197
|
+
note(`serving: ${server.url}\n`);
|
|
198
|
+
note("serving: press Ctrl-C to stop\n");
|
|
198
199
|
});
|
|
199
200
|
}
|
|
200
201
|
async function handleServe(io, options, command) {
|
|
@@ -94,7 +94,7 @@ export async function runStudyCommand(args) {
|
|
|
94
94
|
// checks (keys, runtime auth, subject env, the local agent, caps) refuse this machine, all before
|
|
95
95
|
// the scorer loads, so none imports the scorer's host code. The scripted route's checks and the
|
|
96
96
|
// run-id claim still come after it; the scripted route takes no scorer.
|
|
97
|
-
const routeRun =
|
|
97
|
+
const routeRun = ROUTE_RUNS[route]({ ...args, config });
|
|
98
98
|
if (routeRun === undefined)
|
|
99
99
|
return;
|
|
100
100
|
// Every route's runStudyWith options carry the study's provenance, from here only.
|
|
@@ -155,24 +155,14 @@ function announcedKeyNames(config, options) {
|
|
|
155
155
|
const planned = planStudy(config, { cwd: options.cwd, dryRun: false });
|
|
156
156
|
return planned.ok ? keyNamesOf(planned.planned.plan) : undefined;
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return scriptedRouteRun(args);
|
|
167
|
-
case "terminal":
|
|
168
|
-
return terminalRouteRun(args);
|
|
169
|
-
case "shared-world":
|
|
170
|
-
return sharedWorldRouteRun(args);
|
|
171
|
-
default:
|
|
172
|
-
// Compile-time exhaustiveness: a future route must be handled here, not silently no-op.
|
|
173
|
-
throw new Error(`Unhandled study route: ${String(route)}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
158
|
+
/** Each route's CLI setup. It returns undefined when it has already written its own result. */
|
|
159
|
+
const ROUTE_RUNS = {
|
|
160
|
+
preview: previewRouteRun,
|
|
161
|
+
"computer-use": computerUseRouteRun,
|
|
162
|
+
scripted: scriptedRouteRun,
|
|
163
|
+
terminal: terminalRouteRun,
|
|
164
|
+
"shared-world": sharedWorldRouteRun,
|
|
165
|
+
};
|
|
176
166
|
function studyRerunFlagsRequested(options) {
|
|
177
167
|
return options.rerunFailedFrom !== undefined || options.participants !== undefined;
|
|
178
168
|
}
|
package/dist/cli/findings.d.ts
CHANGED
|
@@ -75,6 +75,11 @@ interface FindingsSource {
|
|
|
75
75
|
loaded: LoadedAnalysis;
|
|
76
76
|
/** ` --cwd <dir>` when the commands this view names need it, or "". */
|
|
77
77
|
cwdFlag: string;
|
|
78
|
+
/**
|
|
79
|
+
* The project-relative path of the run's study file when that file sets `review.analysis:
|
|
80
|
+
* false`, read when the view is built. Undefined when it does not, or cannot be read.
|
|
81
|
+
*/
|
|
82
|
+
analysisOffIn?: string;
|
|
78
83
|
}
|
|
79
84
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
80
85
|
export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
|
package/dist/cli/findings.js
CHANGED
|
@@ -7,6 +7,8 @@ import { loadAnalysis } from "../analysis/load.js";
|
|
|
7
7
|
import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
|
|
8
8
|
import { loadRunBundlePrepared, resolveRunPath } from "../run/locate.js";
|
|
9
9
|
import { resolvePhysicalCwd, runIdOf } from "../run/paths.js";
|
|
10
|
+
import { studyProvenanceOf } from "../run/study-provenance.js";
|
|
11
|
+
import { resolveStudyManifest } from "../study/discover.js";
|
|
10
12
|
import { plural } from "../run/text.js";
|
|
11
13
|
import { shellQuote } from "../substrates/shell.js";
|
|
12
14
|
import { analysisOutcomeText, wantsJson } from "./io.js";
|
|
@@ -151,8 +153,23 @@ export function analysisFindings(source) {
|
|
|
151
153
|
}
|
|
152
154
|
if (job !== undefined || loaded.state === "invalid")
|
|
153
155
|
return withoutFindings(source, "unavailable", job?.reason ?? loaded.warnings[0] ?? null, "This run's analysis records could not be read or checked, so no findings are shown.", cli(`analyze list --run ${source.runId}${source.cwdFlag}`));
|
|
156
|
+
if (source.analysisOffIn !== undefined)
|
|
157
|
+
return withoutFindings(source, "skipped", "AUTOMATIC_ANALYSIS_DISABLED", `Analysis is turned off in this run's study (review.analysis: false in ${source.analysisOffIn}), so none ran after the run. To analyze this run anyway, run the next command.`, analyzeCommand(source, undefined));
|
|
154
158
|
return withoutFindings(source, "none", null, "No analysis has run for this run.", analyzeCommand(source, undefined));
|
|
155
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The run's study file, project-relative, when it still names the same study and sets
|
|
162
|
+
* `review.analysis: false`. A study that turns analysis off leaves no analysis record, so the file
|
|
163
|
+
* is the only place that says why a live run has none.
|
|
164
|
+
*/
|
|
165
|
+
async function studyAnalysisOff(cwd, study) {
|
|
166
|
+
if (study?.path === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const resolved = await resolveStudyManifest(cwd, study.path).catch(() => null);
|
|
169
|
+
if (!resolved?.ok || resolved.config.id !== study.id)
|
|
170
|
+
return undefined;
|
|
171
|
+
return resolved.config.review?.analysis === false ? study.path : undefined;
|
|
172
|
+
}
|
|
156
173
|
/**
|
|
157
174
|
* The run's bundle mode and its selected analysis: `analysisId`, or the version `analyze show`
|
|
158
175
|
* defaults to. Null when the run is missing or its storage is unsafe.
|
|
@@ -163,12 +180,16 @@ export async function readRunAnalysis(cwd, run, analysisId) {
|
|
|
163
180
|
if (!prepared)
|
|
164
181
|
return null;
|
|
165
182
|
const bundle = await loadRunBundlePrepared(physical, prepared).catch(() => null);
|
|
183
|
+
const analysisOffIn = bundle?.bundle.mode === "live"
|
|
184
|
+
? await studyAnalysisOff(physical, studyProvenanceOf(bundle.bundle))
|
|
185
|
+
: undefined;
|
|
166
186
|
return {
|
|
167
187
|
runId: runIdOf(prepared),
|
|
168
188
|
mode: bundle?.bundle.mode,
|
|
169
189
|
runRoot: prepared.relativeRunRoot,
|
|
170
190
|
loaded: await loadAnalysis(prepared, analysisId),
|
|
171
191
|
cwdFlag: cwdFlag(cwd),
|
|
192
|
+
...(analysisOffIn === undefined ? {} : { analysisOffIn }),
|
|
172
193
|
};
|
|
173
194
|
}
|
|
174
195
|
/** The view of a run that could not be found. */
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -17,7 +17,8 @@ export declare const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this
|
|
|
17
17
|
/**
|
|
18
18
|
* The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
|
|
19
19
|
* `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
|
|
20
|
-
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
20
|
+
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
21
|
+
* Removed in 0.112.0: deprecated in 0.110.0 on 2026-10-03, it stays 30 days and one minor release.
|
|
21
22
|
*/
|
|
22
23
|
export declare function envFileAliasOption(): Option;
|
|
23
24
|
/** The dotenv path a command was given: `--dotenv`, or the `--env-file` alias with its warning. */
|
package/dist/cli/io.js
CHANGED
|
@@ -15,11 +15,12 @@ export const JSON_OPTION_DESCRIPTION = "Print a machine-readable JSON response."
|
|
|
15
15
|
export const CWD_OPTION_DESCRIPTION = "Project directory.";
|
|
16
16
|
export const RUN_OPTION_DESCRIPTION = "Run id, or latest.";
|
|
17
17
|
export const DOTENV_OPTION_DESCRIPTION = "Load unset variables from this dotenv file. Values are never printed or saved.";
|
|
18
|
-
const ENV_FILE_RENAMED = "`--env-file` is now `--dotenv`; `--env-file` is removed in 0.
|
|
18
|
+
const ENV_FILE_RENAMED = "`--env-file` is now `--dotenv`; `--env-file` is removed in 0.112.0.";
|
|
19
19
|
/**
|
|
20
20
|
* The hidden `--env-file <path>` that `--dotenv` replaced. Node scans the whole argv for
|
|
21
21
|
* `--env-file` before a script runs, and stops only at `--` (src/node_dotenv.cc), so a missing file
|
|
22
|
-
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
22
|
+
* exits 9 through Node and never reaches humanish. `--dotenv` does not collide.
|
|
23
|
+
* Removed in 0.112.0: deprecated in 0.110.0 on 2026-10-03, it stays 30 days and one minor release.
|
|
23
24
|
*/
|
|
24
25
|
export function envFileAliasOption() {
|
|
25
26
|
return new Option("--env-file <path>").hideHelp().conflicts("dotenv");
|
|
@@ -26,8 +26,12 @@ export declare function followDecision(args: {
|
|
|
26
26
|
follow: boolean;
|
|
27
27
|
unattended: boolean;
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The warning a command adds when it printed the Observer and exited because nobody was attached.
|
|
31
|
+
* It names the reason personAtTerminal found: an agent runner's marker, which can sit beside a real
|
|
32
|
+
* terminal, or no terminal at all.
|
|
33
|
+
*/
|
|
34
|
+
export declare function unattendedObserverWarning(command: "watch" | "observe", env?: NodeJS.ProcessEnv): string;
|
|
31
35
|
/**
|
|
32
36
|
* Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
|
|
33
37
|
* code 2 and returns null, so a caller refuses before starting a run.
|
|
@@ -23,9 +23,17 @@ export function followDecision(args) {
|
|
|
23
23
|
const attended = personAtTerminal();
|
|
24
24
|
return { follow: attended, unattended: !attended };
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The warning a command adds when it printed the Observer and exited because nobody was attached.
|
|
28
|
+
* It names the reason personAtTerminal found: an agent runner's marker, which can sit beside a real
|
|
29
|
+
* terminal, or no terminal at all.
|
|
30
|
+
*/
|
|
31
|
+
export function unattendedObserverWarning(command, env = process.env) {
|
|
32
|
+
const agent = detectAgentSession(env);
|
|
33
|
+
const reason = agent === undefined
|
|
34
|
+
? "No interactive terminal"
|
|
35
|
+
: `An agent environment was detected (${agent.marker} is set, which ${agent.runner} sets)`;
|
|
36
|
+
return `${reason}, so ${command} printed the Observer path and exited instead of serving it until Ctrl-C. Pass --serve to keep serving, or open the file.`;
|
|
29
37
|
}
|
|
30
38
|
/**
|
|
31
39
|
* Parse --port and decide open and follow. On an invalid port it writes the failure, sets exit
|
|
@@ -112,8 +120,6 @@ export function formatObserverHuman(result) {
|
|
|
112
120
|
].join("\n") + "\n");
|
|
113
121
|
}
|
|
114
122
|
export async function followObserver(io, result, server, options = {}) {
|
|
115
|
-
io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
|
|
116
|
-
io.writeOut("watching: press Ctrl-C to stop\n");
|
|
117
123
|
await new Promise((resolve) => {
|
|
118
124
|
const signalTarget = options.signalTarget ?? process;
|
|
119
125
|
const signals = options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
@@ -155,6 +161,10 @@ export async function followObserver(io, result, server, options = {}) {
|
|
|
155
161
|
handlers.set(signal, handler);
|
|
156
162
|
signalTarget.once(signal, handler);
|
|
157
163
|
}
|
|
164
|
+
// Print the prompt only once the handlers are in place: a Ctrl-C that arrives sooner would
|
|
165
|
+
// take Node's default action and end the process without closing the server.
|
|
166
|
+
io.writeOut(`watching: ${result.serverUrl ?? result.observerUrl ?? result.observerPath}\n`);
|
|
167
|
+
io.writeOut("watching: press Ctrl-C to stop\n");
|
|
158
168
|
});
|
|
159
169
|
}
|
|
160
170
|
export function exitCodeForSignal(signal) {
|
|
@@ -58,6 +58,16 @@ const CREDENTIAL_PARAMETER = [
|
|
|
58
58
|
// Upper-case variable names that hold a credential, as an env file, a shell or a JSON dump writes
|
|
59
59
|
// them: GITHUB_TOKEN, VERCEL_TOKEN, AWS_SECRET_ACCESS_KEY, DATABASE_PASSWORD.
|
|
60
60
|
const CREDENTIAL_VARIABLE = String.raw `\b(?:[A-Z][A-Z0-9_]{0,48}_)?(?:TOKEN|SECRET|SECRET_KEY|PASSWORD|PASSWD|API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY)`;
|
|
61
|
+
// The public E2B pages its errors link to, which name no sandbox and carry no credential: the
|
|
62
|
+
// dashboard as the SDK's missing-key error names it (https://e2b.dev/dashboard?tab=keys), with no
|
|
63
|
+
// path below /dashboard and no query but one short `tab`, and a docs page as the API's 401 names
|
|
64
|
+
// it (https://docs.e2b.dev/api-key), whose path segments hold only letters and hyphens and which
|
|
65
|
+
// has no query. Neither has user info, and only closing punctuation, ")" included, may follow
|
|
66
|
+
// before the next space or the end of the text.
|
|
67
|
+
const E2B_PUBLIC_PAGE = [
|
|
68
|
+
String.raw `(?:www\.)?e2b\.dev\/dashboard\/?(?:\?tab=[a-z]{1,16})?`,
|
|
69
|
+
String.raw `(?:docs\.e2b\.dev|(?:www\.)?e2b\.dev\/docs)(?:\/[a-z][a-z-]{0,39}){0,8}\/?`,
|
|
70
|
+
].join("|");
|
|
61
71
|
// A pattern that matches a credential by its context puts the context in a `keep` group, which
|
|
62
72
|
// redaction leaves in place: `?token=[REDACTED_SECRET]`. Context goes in a group and not in a
|
|
63
73
|
// lookbehind, because a pattern that starts with a lookbehind is tried at every position of the
|
|
@@ -106,10 +116,11 @@ const SECRET_PATTERNS = [
|
|
|
106
116
|
// bounded, so a long run of `TOKEN=` costs linear time.
|
|
107
117
|
new RegExp(String.raw `(?<keep>${CREDENTIAL_VARIABLE}${QUOTE}[ \t]{0,4}[:=][ \t]{0,4}${QUOTE})${notAPlaceholder(VARIABLE_END)}${NOT_A_NUMBER}(?=[^\s"'\\,;&]{0,255}[0-9])(?=[^\s"'\\,;&]{0,255}[A-Za-z])[^\s"'\\,;&]{16}[^\s"'\\,;&]*`, "g"),
|
|
108
118
|
// Any URL on an E2B host: a sandbox host names its sandbox, and a stream URL carries its auth
|
|
109
|
-
// key.
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
119
|
+
// key. The slashes may be JSON-escaped (`https:\/\/`). User info runs to its last `@`, as a URL
|
|
120
|
+
// parser reads it, and may hold any character but a slash or space. The host part stops at a
|
|
121
|
+
// backslash, quote or angle bracket, so a URL followed by an escaped line break and an `E2B_...`
|
|
122
|
+
// variable in JSON text is not read as an E2B host. A public page followed by `)@` is user info.
|
|
123
|
+
new RegExp(String.raw `(?!https:\/\/(?:${E2B_PUBLIC_PAGE})[.,;:!?'")]*(?:\s|$))https?:\\?\/\\?\/(?:[^/\s@]*@)*[^/\s\\"'<>@]*e2b[^)\s]+`, "gi"),
|
|
113
124
|
/BEGIN (RSA|OPENSSH|PRIVATE) KEY/gi,
|
|
114
125
|
];
|
|
115
126
|
const LOCAL_PATH_PATTERNS = [
|
|
@@ -3,7 +3,7 @@ import { type StudyConfig } from "../../study/types.js";
|
|
|
3
3
|
import { type ResolvedPersona } from "../../study/persona.js";
|
|
4
4
|
import { type RunRerunLineage } from "../../run/bundle.js";
|
|
5
5
|
import type { PreparedSelectedOutputDirectory } from "../../run/contained-output.js";
|
|
6
|
-
import { type CuaActorStudyErrorCode, type CuaParticipantPlan, type CuaParticipantPlanEntry, type DesktopParticipantRun } from "./types.js";
|
|
6
|
+
import { type CuaActorStudyErrorCode, type CuaParticipantPlan, type CuaParticipantPlanEntry, type DesktopParticipantRun, type ParticipantRunsAndPlan } from "./types.js";
|
|
7
7
|
export declare function defaultSessionTimeoutMs(config: StudyConfig): number;
|
|
8
8
|
/** Per-participant sandbox deadline (each one owns its own desktop). Mirrors the
|
|
9
9
|
* single-participant formula verbatim so N=1 stays byte-stable: explicit sandboxTimeoutMs, else session budget + (clone
|
|
@@ -21,17 +21,14 @@ export declare function resolveParticipantSandboxMs(config: StudyConfig): number
|
|
|
21
21
|
*/
|
|
22
22
|
export declare function boundedConcurrency(declared: number | undefined, participantCount: number): number;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* marked $0 (dryRun: true).
|
|
24
|
+
* Build the participant runs and the public plan from a computer-use plan (pure). The same plan
|
|
25
|
+
* appears in dry-run and live, marked $0 for a dry run. `env` may lower the planned concurrency;
|
|
26
|
+
* `personas` are the compiled committed personas the prompts carry.
|
|
28
27
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
countOverride?: number;
|
|
28
|
+
export declare function participantRunsAndPlan(plan: ComputerUsePlan, opts?: {
|
|
31
29
|
env?: Record<string, string | undefined>;
|
|
32
|
-
dryRun?: boolean;
|
|
33
30
|
personas?: Map<string, ResolvedPersona>;
|
|
34
|
-
}):
|
|
31
|
+
}): ParticipantRunsAndPlan;
|
|
35
32
|
/** Print the participant plan to stderr before any sandbox/provider call (public-safe: ids, devices,
|
|
36
33
|
* digests, and budgets only; no prompt text, no secrets). */
|
|
37
34
|
export declare function emitPreflightPlan(participantPlan: CuaParticipantPlan, studyId: string): void;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// compiles the committed personas first and narrows a rerun to its selected participants.
|
|
5
5
|
import { isLocalBrowserStudy, LOCAL_BROWSER_LIFETIME_MS, } from "../../substrates/local/runtime-config.js";
|
|
6
6
|
import { DEFAULT_STATE_STEP_TIMEOUT_MS } from "../../subject/state.js";
|
|
7
|
-
import { computerUseParticipants, } from "../../study/plan-participants.js";
|
|
8
7
|
import { resolveParticipant } from "../../run/participant.js";
|
|
9
8
|
import {} from "../../study/types.js";
|
|
10
9
|
import { scrubPersonaBrief } from "../../study/persona.js";
|
|
@@ -71,23 +70,17 @@ function envLoweredConcurrency(planned, participantCount, env) {
|
|
|
71
70
|
}
|
|
72
71
|
return { bound: planned };
|
|
73
72
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
sessionBudgetMs: config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config),
|
|
81
|
-
sandboxMs: resolveParticipantSandboxMs(config),
|
|
82
|
-
desktopCli: config.subject.source === "desktop-cli",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
/** Build the participant runs and the public plan from the planned participants (pure). */
|
|
86
|
-
function participantRunsAndPlan(planned, opts = {}) {
|
|
73
|
+
/**
|
|
74
|
+
* Build the participant runs and the public plan from a computer-use plan (pure). The same plan
|
|
75
|
+
* appears in dry-run and live, marked $0 for a dry run. `env` may lower the planned concurrency;
|
|
76
|
+
* `personas` are the compiled committed personas the prompts carry.
|
|
77
|
+
*/
|
|
78
|
+
export function participantRunsAndPlan(plan, opts = {}) {
|
|
87
79
|
const env = opts.env ?? {};
|
|
88
80
|
// Who each participant is (id, persona, focus, device, limits) comes from the plan; this adds
|
|
89
81
|
// what the route derives from it: the prompt, bundle ids and artifact paths.
|
|
90
|
-
const { participants } =
|
|
82
|
+
const { participants } = plan.runner;
|
|
83
|
+
const desktopCli = plan.runner.subject.kind === "desktop-cli";
|
|
91
84
|
const participantCount = participants.length;
|
|
92
85
|
const runs = participants.map((participant) => {
|
|
93
86
|
const mission = participant.assignment.mission ?? DEFAULT_MISSION;
|
|
@@ -102,7 +95,7 @@ function participantRunsAndPlan(planned, opts = {}) {
|
|
|
102
95
|
...(resolvedPersona === undefined ? {} : { resolvedPersona }),
|
|
103
96
|
...(focus === undefined ? {} : { instruction: focus }),
|
|
104
97
|
device: { name: device.name, preset: device.preset },
|
|
105
|
-
...(
|
|
98
|
+
...(desktopCli ? { surface: "desktop-cli" } : {}),
|
|
106
99
|
});
|
|
107
100
|
const run = resolveParticipant(participant, {
|
|
108
101
|
persona: composed.persona,
|
|
@@ -119,9 +112,9 @@ function participantRunsAndPlan(planned, opts = {}) {
|
|
|
119
112
|
traceArtifactPath: participantCount === 1 ? "actor.json" : `actors/${run.streamId}.json`,
|
|
120
113
|
};
|
|
121
114
|
});
|
|
122
|
-
const resolved = envLoweredConcurrency(
|
|
115
|
+
const resolved = envLoweredConcurrency(plan.concurrency, participantCount, env);
|
|
123
116
|
const concurrency = resolved.bound;
|
|
124
|
-
const { sessionBudgetMs, sandboxMs } =
|
|
117
|
+
const { sessionBudgetMs, sandboxMs } = plan;
|
|
125
118
|
const participantPlan = {
|
|
126
119
|
strategy: CUA_FANOUT_STRATEGY,
|
|
127
120
|
laneCount: participantCount,
|
|
@@ -132,7 +125,7 @@ function participantRunsAndPlan(planned, opts = {}) {
|
|
|
132
125
|
waves: Math.ceil(participantCount / concurrency),
|
|
133
126
|
perLaneSessionBudgetMs: sessionBudgetMs,
|
|
134
127
|
worstCaseSandboxMinutes: Math.round((participantCount * sandboxMs) / 60_000),
|
|
135
|
-
dryRun:
|
|
128
|
+
dryRun: plan.dryRun,
|
|
136
129
|
lanes: runs.map((spec) => ({
|
|
137
130
|
id: spec.planned.id,
|
|
138
131
|
...(spec.planned.labels.actorType === undefined
|
|
@@ -162,16 +155,6 @@ function participantRunsAndPlan(planned, opts = {}) {
|
|
|
162
155
|
};
|
|
163
156
|
return { runs, participantPlan };
|
|
164
157
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Pure pre-flight plan resolver (runs in dry-run and live). Returns the participant table, the
|
|
167
|
-
* effective concurrency, the wave count, the per-participant session budget, and the worst-case total
|
|
168
|
-
* sandbox-minutes, before any sandbox or provider call. The same plan appears in dry-run,
|
|
169
|
-
* marked $0 (dryRun: true).
|
|
170
|
-
*/
|
|
171
|
-
export function resolveCuaParticipantPlan(config, opts = {}) {
|
|
172
|
-
const { countOverride, ...rest } = opts;
|
|
173
|
-
return participantRunsAndPlan(plannedParticipantsOf(config, countOverride), rest).participantPlan;
|
|
174
|
-
}
|
|
175
158
|
/** Print the participant plan to stderr before any sandbox/provider call (public-safe: ids, devices,
|
|
176
159
|
* digests, and budgets only; no prompt text, no secrets). */
|
|
177
160
|
export function emitPreflightPlan(participantPlan, studyId) {
|
|
@@ -216,13 +199,10 @@ export async function loadCuaParticipants(args) {
|
|
|
216
199
|
// Compile any committed personas before planning, so the plan builder stays pure and each participant's
|
|
217
200
|
// prompt carries real behavioral directives rather than a bare `Persona: <id>.` label.
|
|
218
201
|
const personas = await compileParticipantPersonas(args.projectRoot, participants.map((participant) => participant.personaId));
|
|
219
|
-
const { runs: participantRuns, participantPlan } = participantRunsAndPlan({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
sandboxMs: plan.sandboxMs,
|
|
224
|
-
desktopCli: plan.runner.subject.kind === "desktop-cli",
|
|
225
|
-
}, { env: args.env, dryRun: plan.dryRun, personas });
|
|
202
|
+
const { runs: participantRuns, participantPlan } = participantRunsAndPlan(plan, {
|
|
203
|
+
env: args.env,
|
|
204
|
+
personas,
|
|
205
|
+
});
|
|
226
206
|
const rerun = plan.rerun;
|
|
227
207
|
if (!rerun)
|
|
228
208
|
return { ok: true, participantRuns, participantPlan };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CuaActorDescriptor } from "../../actors/registry.js";
|
|
2
2
|
import type { ComputerUsePlan, ComputerUseRunner, RoutePlanResult, RouteRefusal } from "../../study/plan-types.js";
|
|
3
3
|
import type { StudyConfig, StudySubjectState } from "../../study/types.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type ComputerUseRunInput, type CuaActorStudyErrorCode } from "./types.js";
|
|
5
5
|
/** The error a computer-use study returns before a run starts. */
|
|
6
6
|
export interface ComputerUseRefusal extends RouteRefusal<"computer-use", CuaActorStudyErrorCode> {
|
|
7
7
|
/**
|
|
@@ -44,5 +44,5 @@ export declare function planComputerUseStudy(config: StudyConfig, input: {
|
|
|
44
44
|
/** Which of the caller's driving homes are set; neither when absent. */
|
|
45
45
|
readonly driving?: CallerDriving;
|
|
46
46
|
readonly countOverride?: number;
|
|
47
|
-
readonly rerun?:
|
|
47
|
+
readonly rerun?: ComputerUseRunInput["rerun"];
|
|
48
48
|
}): ComputerUsePlanResult;
|
|
@@ -232,7 +232,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
232
232
|
// participants. It comes after the rules above, so a shared-world config that breaks one of them,
|
|
233
233
|
// which runStudyWith sends here, still gets that rule's message.
|
|
234
234
|
if (config.subject.topology === "shared-world")
|
|
235
|
-
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", "route: shared-world studies run every participant against one shared app on the shared-world route; this route runs independent participants.
|
|
235
|
+
return refuse("in-scope", "HUMANISH_COMPUTER_USE_SUBJECT_INVALID", "route: shared-world studies run every participant against one shared app on the shared-world route; this route runs independent participants. The shared-world route takes subject.source clone or local-tree, or app-url with policies.allowPublicTargets: true, on execution.target e2b-desktop.", actor);
|
|
236
236
|
// The in-process route drives subject.appUrl on this machine and creates no desktop, so it would
|
|
237
237
|
// skip the subject a clone, local-tree or desktop-cli study declares.
|
|
238
238
|
const source = config.subject.source;
|
|
@@ -1,33 +1,20 @@
|
|
|
1
|
+
import { type RefusedStudy } from "../../run/route-shell.js";
|
|
1
2
|
import type { ComputerUsePlan } from "../../study/plan-types.js";
|
|
2
3
|
import type { AdmittedPlan } from "../../run-study.js";
|
|
3
4
|
import type { StudyConfig } from "../../study/types.js";
|
|
4
5
|
import { type ComputerUseRefusal } from "./plan.js";
|
|
5
|
-
import { type ComputerUseRunInput, type CuaActorStudyResult
|
|
6
|
-
/**
|
|
7
|
-
* Plans and runs a computer-use study in one call. It is not exported from src/index.ts; tests call
|
|
8
|
-
* it. It plans the config with planComputerUseStudy and runs
|
|
9
|
-
* the plan with runComputerUsePlan, whose run scope and withTransientCommsSecrets wrapper cover the
|
|
10
|
-
* run and its analysis.
|
|
11
|
-
*/
|
|
12
|
-
export declare function runCuaActorStudy(options: RunCuaActorStudyOptions): Promise<CuaActorStudyResult>;
|
|
6
|
+
import { type ComputerUseRunInput, type CuaActorStudyResult } from "./types.js";
|
|
13
7
|
/**
|
|
14
8
|
* A refused computer-use study's result, at the refusal's stage: a before-scope refusal has its own
|
|
15
9
|
* envelope and no analysis record; the others come after the cwd checks, and the participant cap after
|
|
16
10
|
* the personas are read.
|
|
17
11
|
*/
|
|
18
|
-
export declare function computerUseStudyRefusal(options:
|
|
12
|
+
export declare function computerUseStudyRefusal(options: ComputerUseRunInput & RefusedStudy, refusal: ComputerUseRefusal): Promise<CuaActorStudyResult>;
|
|
19
13
|
/**
|
|
20
14
|
* runStudyWith's step for a computer-use plan. It runs the plan's local checks (admitCuaRun: the
|
|
21
15
|
* participants, the preflight plan, the keys, the local agent, subject env and caps, and the
|
|
22
16
|
* local-tree archive) before any run scope opens, so the CLI can present their refusal before it
|
|
23
|
-
* loads a declared scorer, and returns the run that continues from them with that scorer.
|
|
17
|
+
* loads a declared scorer, and returns the run that continues from them with that scorer. The
|
|
18
|
+
* run and its analysis share one scope for the email secrets it registers.
|
|
24
19
|
*/
|
|
25
20
|
export declare function admitComputerUsePlan(plan: ComputerUsePlan, input: ComputerUseRunInput, config: StudyConfig): Promise<AdmittedPlan<"computer-use">>;
|
|
26
|
-
/**
|
|
27
|
-
* Run a computer-use plan: its local checks, then the run. The run scope in runAdmittedCuaRun
|
|
28
|
-
* finalizes any status record the run opened, on every exit, so a test or library caller does not
|
|
29
|
-
* leave the 5 s status cadence writing into a directory something else is deleting (an unrelated
|
|
30
|
-
* ENOTEMPTY). Participants, the brain, the subject, the caps and the residual config come from the
|
|
31
|
-
* plan. `config` is what the caller's createProvider and inProcess executor receive.
|
|
32
|
-
*/
|
|
33
|
-
export declare function runComputerUsePlan(plan: ComputerUsePlan, input: ComputerUseRunInput, config: StudyConfig): Promise<CuaActorStudyResult>;
|