humanish 0.110.3 → 0.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +7 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study.js +2 -1
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +9 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +6 -2
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +6 -2
- package/dist/routes/shared-world/plan.js +8 -8
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +3 -2
- package/dist/routes/terminal/session.js +1 -0
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run.d.ts +11 -1
- package/dist/run/run.js +24 -5
- package/dist/run/status.d.ts +6 -0
- package/dist/run/status.js +1 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +49 -39
- package/dist/study/config.d.ts +31 -8
- package/dist/study/config.js +181 -155
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +5 -4
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +28 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +78 -0
- package/dist/study/migrate/v2.js +607 -0
- package/dist/study/parse/actors.d.ts +34 -10
- package/dist/study/parse/actors.js +76 -174
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +6 -41
- package/dist/study/parse/front.d.ts +36 -0
- package/dist/study/parse/{study-v3.js → front.js} +37 -88
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +7 -8
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +6 -6
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +5 -4
- package/dist/study/preflight-probes.js +4 -4
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +3 -3
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +17 -20
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +19 -0
- package/dist/study/study-fields.js +46 -0
- package/dist/study/summary.js +19 -18
- package/dist/study/types.d.ts +66 -104
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -3
- package/dist/study/validation.d.ts +4 -10
- package/dist/study/validation.js +54 -79
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +42 -110
- package/dist/substrates/local/runtime-config.js +14 -16
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +36 -8
- package/package.json +1 -1
- package/dist/study/parse/study-v3.d.ts +0 -21
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,35 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.111.0: StudyConfig has the keys of a v3 study file (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.111.0 is the first breaking release under the compatibility policy. It breaks library
|
|
14
|
+
callers that read or build a `StudyConfig`, the type `parseStudy` returns and `runStudy` takes:
|
|
15
|
+
the type now has the keys of a `humanish.study.v3` file, such as `actor`, `route`,
|
|
16
|
+
`participants`, `caps` and `mode`, where it had `actors[0]`, `execution.caps` and
|
|
17
|
+
`scenario.mode`. The release notes carry the migration table. `runStudy` refuses a config that
|
|
18
|
+
still sets a 0.110 field with `HUMANISH_STUDY_V2_UNSUPPORTED`, so a budget left in
|
|
19
|
+
`execution.caps` cannot run uncapped. Study files and CLI commands are unchanged, except two JSON
|
|
20
|
+
outputs: `humanish study show --json` prints the v3 config, and `humanish study check --json`
|
|
21
|
+
names a participant's target `participants[<i>].target`. The `--env-file` and `cleanup` aliases
|
|
22
|
+
still work and are removed in 0.112.0, on or after 2026-11-03.
|
|
23
|
+
|
|
24
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.111.0)
|
|
25
|
+
|
|
26
|
+
## 0.110.4: Scripted runs reclaim clean, review says interrupted, Chrome under a long TMPDIR (2026-10-05)
|
|
27
|
+
|
|
28
|
+
humanish 0.110.4 fixes four findings of the 0.110.3 live smoke. `humanish reclaim --check` reports
|
|
29
|
+
a live scripted run against an `app-url` subject `clean`, with reason `no-sandbox`, and exits 0.
|
|
30
|
+
`humanish review` on a computer-use run stopped by Ctrl-C after its desktop started says the run
|
|
31
|
+
was interrupted. A run that failed before any participant started no longer prints "Participants
|
|
32
|
+
finished; preparing analysis…". The scripted route launches Chrome when `TMPDIR` is too long for
|
|
33
|
+
Chrome's socket path. Planner refusals for a library config built without `parseStudy` name v3
|
|
34
|
+
keys, and so does `humanish migrate` when one of those checks refuses a v2 file. Inside, a v3
|
|
35
|
+
parser runs beside `parseStudy`, src reads the study through accessors, and `humanish migrate`
|
|
36
|
+
reads v2 files through its own front end; the library API is unchanged.
|
|
37
|
+
|
|
38
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.4)
|
|
39
|
+
|
|
11
40
|
## 0.110.3: Ctrl-C at the watch prompt, review says analysis is off, E2B docs links kept (2026-10-05)
|
|
12
41
|
|
|
13
42
|
humanish 0.110.3 runs the `watch` and `observe` shutdown, and prints `watch stopped` or `observe
|
package/CONTEXT.md
CHANGED
|
@@ -18,7 +18,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
18
18
|
| persona | Who a participant is: traits, background, device. `humanish/personas/<id>.yaml`. | `src/study/persona.ts`, `src/study/persona-resolve.ts` |
|
|
19
19
|
| mission | The free-form brief a model-driven participant receives: `actor.mission`. `actor.tasks[]` adds discrete tasks: each has a `goal` the participant sees and an optional `success` check it does not. | `StudyActor` in `src/study/types.ts` |
|
|
20
20
|
| assignment | What one participant was asked to do: the mission, a `participants` entry's `instruction` or the homogeneous `participants.instruction` (recorded as `focus`), and the tasks. Each stream records it as `streams[].assignment`. | `participantAssignment` in `src/study/participant-assignment.ts`; `RunParticipantAssignment` in `src/run/streams.ts` |
|
|
21
|
-
| scenario | A scenario file, `humanish/scenarios/<id>.yaml` (`humanish.scenario.v1`): a goal and ordered steps with expectations. A scripted study names one in `scenario:`, and only the scripted route executes those steps. A v2 study file's `scenario` block held run settings (`mode`, `caps`, `ref`), which v3 moves to the top level. A model-driven participant follows its mission. | `
|
|
21
|
+
| scenario | A scenario file, `humanish/scenarios/<id>.yaml` (`humanish.scenario.v1`): a goal and ordered steps with expectations. A scripted study names one in `scenario:`, and only the scripted route executes those steps. A v2 study file's `scenario` block held run settings (`mode`, `caps`, `ref`), which v3 moves to the top level. A model-driven participant follows its mission. | `scenario` in `StudyConfig` (`src/study/types.ts`); `src/routes/scripted/route.ts` resolves the scenario, and `executeBrowserPersonaStep` (`src/actors/scripted-browser/steps.ts`) runs each step |
|
|
22
22
|
| surface | One browser viewport that a scripted run replays its journey on: `desktop` or `mobile`. A scripted study runs the desktop surface by default, and `surfaces: [desktop, mobile]` adds the mobile one. Each surface writes `actor-<surface>.json` and `traces/<surface>.json`. | `BrowserSurface` and `browserSurfaces` in `src/actors/scripted-browser/types.ts`; the plan's `surfaces` in `src/routes/scripted/plan.ts` |
|
|
23
23
|
| journey | A scenario's `browser.steps` parsed into ordered steps with a goal and a start path. The scripted route replays it once on each surface, and each surface's session reports the first step that did not pass. | `BrowserPersonaJourney` in `src/actors/scripted-browser/types.ts`; `parseBrowserPersonaJourneyFromScenario` in `src/actors/scripted-browser/journey.ts` |
|
|
24
24
|
| subject | The product under study, declared by `subject.source`: `clone`, `app-url`, `local-tree`, `terminal-product`, `desktop-cli`, `local-app` or `this-repo`. `run.json` records its provenance under `subject`. | `StudySubject` in `src/study/types.ts`, parsed in `src/study/parse/subject.ts` |
|
|
@@ -33,7 +33,7 @@ bundle and output fields keep older spellings; the second table maps them.
|
|
|
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` |
|
|
35
35
|
| owner tags | The E2B metadata every sandbox humanish creates carries: `tool: humanish`, the run's `runId`, and `runKey`, a digest of the run directory's inode and creation time. The run journals them in an owner line before its first create. `humanish reclaim` lists sandboxes that match all three, which finds one whose id never reached a receipt. | `sandboxOwnerTags` in `src/run/sandbox-creates.ts`; `findE2BSandboxesByTags` in `src/substrates/e2b/sandbox.ts` |
|
|
36
|
-
| topology | Whether participants share one instance of the subject. `route: shared-world` gives one for all; every other route gives each participant its own (`per-lane-worlds`). `run.json` records it as `sharedWorld.topology`. | `
|
|
36
|
+
| topology | Whether participants share one instance of the subject. `route: shared-world` gives one for all; every other route gives each participant its own (`per-lane-worlds`). `run.json` records it as `sharedWorld.topology`. | `declaresSharedWorld` in `src/study/study-fields.ts` |
|
|
37
37
|
| plane | The one instance of the subject that a shared-world study's participants use together. A `provisioned` plane is a `clone` or `local-tree` subject that humanish serves in a sandbox, with a declared subject state. An `external-public` plane is a deployment already running at `subject.appUrl`, whose owner the study names in `subject.publicTarget.owner`. Each plane has at least two participants. | `SharedWorldPlane` in `src/study/plan-types.ts`; `src/routes/shared-world/` |
|
|
38
38
|
| stream | One live or recorded view of a participant, such as a browser or a terminal, in `run.json` and the Observer. | `RunStream` in `src/run/streams.ts` |
|
|
39
39
|
| Observer | The single-file review page built from `observer/observer-data.json` (`humanish.observer-data.v1`). | `observer/` (the page), `src/observer/` (data, render, serve) |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// One Claude Code process as the computer-use brain, instead of a fresh `claude -p` per turn.
|
|
2
2
|
//
|
|
3
|
-
// Why: `
|
|
3
|
+
// Why: `actor.localAgent: codex` already runs through a persistent app-server thread,
|
|
4
4
|
// so the participant remembers what it tried. `claude` spawned `claude -p` per turn, so every
|
|
5
5
|
// turn started cold. Measured on the same study with the same credentials (n=1 each): one-shot,
|
|
6
6
|
// 188 actions over 90 turns and never finished; a thread that remembers, 21 actions over 8 turns
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// mapping and JSON reading the two Claude Code paths share, and the one-shot Claude Code provider.
|
|
4
4
|
//
|
|
5
5
|
// A developer trying humanish often already has a coding agent signed in (Codex on a ChatGPT plan,
|
|
6
|
-
// Claude Code on a Max plan), so `
|
|
6
|
+
// Claude Code on a Max plan), so `actor.type: local-agent` lets that agent decide the next
|
|
7
7
|
// action instead of a provider API key. Codex runs as a restricted app-server participant
|
|
8
8
|
// (actors/codex/restricted-participant.ts) and Claude Code as one session for the whole run
|
|
9
9
|
// (claude-session.ts). The one-shot provider below spawns `claude -p` per turn, with no memory of
|
|
@@ -8,11 +8,11 @@ export interface ScriptedBrowserSessionOptions {
|
|
|
8
8
|
evidenceAppUrl?: string;
|
|
9
9
|
/** Defaults to loopback. Provisioned subjects drive a private URL but persist redacted labels. */
|
|
10
10
|
urlPolicy?: ScriptedBrowserEvidenceUrlPolicy;
|
|
11
|
-
/** Parsed + validated by the backend (scenario
|
|
11
|
+
/** Parsed + validated by the backend (`scenario` is consumed there, fail-closed). */
|
|
12
12
|
journey: BrowserPersonaJourney;
|
|
13
13
|
/** One session per surface. */
|
|
14
14
|
surface: BrowserSurface;
|
|
15
|
-
/** id =
|
|
15
|
+
/** id = actor.persona ?? "scripted-journey"; promptDigest = journey.sourceDigest prefix
|
|
16
16
|
* (the step manifest is the "prompt"; no model prompt exists on this route). */
|
|
17
17
|
persona: ActorPersonaRef;
|
|
18
18
|
/** Journey wall-clock budget in ms; the route passes execution.timeoutMs or 300_000. */
|
|
@@ -23,17 +23,26 @@ import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, pre
|
|
|
23
23
|
import { ACTOR_TRACE_SCHEMA, SCRIPTED_BROWSER_CAPABILITIES, } from "../contract.js";
|
|
24
24
|
import { buildBlockedBrowserPersonaSteps, buildBrowserTrace, captureBlockedStepScreenshot, compactBrowserError, executeBrowserPersonaStep, probeAppUrl, sanitizeBrowserEvidenceUrl, screenshotPathForBrowserStep, surfaceScreenshotPath, tracePathForBrowserSurface, } from "./steps.js";
|
|
25
25
|
import { LOOPBACK_EVIDENCE_URL_POLICY, } from "./types.js";
|
|
26
|
+
import { chromeLaunchEnv, chromeLaunchError } from "./launch-env.js";
|
|
26
27
|
/** Production default: lazy playwright-core import + chromium.launch, exactly as the driver
|
|
27
|
-
* always did. Kept in one place so the optional peer is touched by exactly one code path.
|
|
28
|
+
* always did. Kept in one place so the optional peer is touched by exactly one code path. A
|
|
29
|
+
* `TMPDIR` too long for Chrome's singleton socket is replaced for Chrome (chromeLaunchEnv). */
|
|
28
30
|
async function launchPlaywrightChromium(args) {
|
|
29
31
|
const { chromium } = await import("playwright-core");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const env = await chromeLaunchEnv();
|
|
33
|
+
try {
|
|
34
|
+
const browser = await chromium.launch({
|
|
35
|
+
executablePath: args.browserCommand,
|
|
36
|
+
headless: true,
|
|
37
|
+
args: [...CHROMIUM_EVIDENCE_HYGIENE_FLAGS, "--disable-gpu", "--disable-dev-shm-usage"],
|
|
38
|
+
timeout: args.timeoutMs,
|
|
39
|
+
...(env === undefined ? {} : { env }),
|
|
40
|
+
});
|
|
41
|
+
return browser;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw chromeLaunchError(error, env);
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
function assertScriptedSessionPathIds(options) {
|
|
39
48
|
assertSafeOutputPathSegment(options.surface.id, "Browser surface id");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment to launch Chrome with: `env` with `TMPDIR` set to /tmp when its own `TMPDIR` is
|
|
3
|
+
* too long for the singleton socket and /tmp is a writable directory. Undefined when Chrome can
|
|
4
|
+
* launch with `env` as it is, or when /tmp cannot be used; chromeLaunchError then names the cause.
|
|
5
|
+
*/
|
|
6
|
+
export declare function chromeLaunchEnv(env?: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv | undefined>;
|
|
7
|
+
/**
|
|
8
|
+
* The launch error to report: one that names the cause and the fix when Chrome aborted because its
|
|
9
|
+
* singleton socket path was too long, and `error` itself otherwise. `env` is the environment
|
|
10
|
+
* Chrome was launched with.
|
|
11
|
+
*/
|
|
12
|
+
export declare function chromeLaunchError(error: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Chrome's process singleton listens on a Unix socket at `$TMPDIR/<product>.XXXXXX/SingletonSocket`,
|
|
2
|
+
// where the product is com.google.Chrome, or org.chromium.Chromium for Chromium and Chrome for
|
|
3
|
+
// Testing. A Unix socket path holds at most 107 bytes on Linux and 103 on macOS, so under a longer
|
|
4
|
+
// `TMPDIR`, such as a sandboxed agent's private temp directory, Chrome aborts at launch with
|
|
5
|
+
// "Socket path too long", and Playwright reports only that the browser closed. The scripted
|
|
6
|
+
// browser gives Chrome /tmp in that case; the browser profile stays where Playwright puts it.
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { access } from "node:fs/promises";
|
|
9
|
+
/** `/org.chromium.Chromium.XXXXXX/SingletonSocket`: the longest path Chrome adds to `TMPDIR`. */
|
|
10
|
+
const SOCKET_SUFFIX_BYTES = 45;
|
|
11
|
+
/** The longest socket path, without its NUL: `sun_path` holds 104 bytes on macOS, 108 elsewhere. */
|
|
12
|
+
const SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
|
|
13
|
+
/** Where Chrome makes its socket directory when `TMPDIR` is unset. */
|
|
14
|
+
const DEFAULT_TMPDIR = "/tmp";
|
|
15
|
+
/** The byte length of `TMPDIR` when Chrome's singleton socket path under it would be too long. */
|
|
16
|
+
function overlongTmpdirBytes(env) {
|
|
17
|
+
const tmpdir = env.TMPDIR?.replace(/\/+$/, "");
|
|
18
|
+
if (tmpdir === undefined || tmpdir === "")
|
|
19
|
+
return undefined;
|
|
20
|
+
const bytes = Buffer.byteLength(tmpdir);
|
|
21
|
+
return bytes + SOCKET_SUFFIX_BYTES > SOCKET_PATH_MAX_BYTES ? bytes : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The environment to launch Chrome with: `env` with `TMPDIR` set to /tmp when its own `TMPDIR` is
|
|
25
|
+
* too long for the singleton socket and /tmp is a writable directory. Undefined when Chrome can
|
|
26
|
+
* launch with `env` as it is, or when /tmp cannot be used; chromeLaunchError then names the cause.
|
|
27
|
+
*/
|
|
28
|
+
export async function chromeLaunchEnv(env = process.env) {
|
|
29
|
+
if (process.platform === "win32" || overlongTmpdirBytes(env) === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
try {
|
|
32
|
+
await access(DEFAULT_TMPDIR, constants.W_OK | constants.X_OK);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return { ...env, TMPDIR: DEFAULT_TMPDIR };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The launch error to report: one that names the cause and the fix when Chrome aborted because its
|
|
41
|
+
* singleton socket path was too long, and `error` itself otherwise. `env` is the environment
|
|
42
|
+
* Chrome was launched with.
|
|
43
|
+
*/
|
|
44
|
+
export function chromeLaunchError(error, env = process.env) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
if (!message.includes("Socket path too long"))
|
|
47
|
+
return error;
|
|
48
|
+
const bytes = overlongTmpdirBytes(env);
|
|
49
|
+
return new Error(`Chrome could not start: its singleton socket path under \`TMPDIR\`${bytes === undefined ? "" : ` (${bytes} bytes)`} is longer than the ${SOCKET_PATH_MAX_BYTES} bytes a Unix socket path allows. Set \`TMPDIR\` to a directory of at most ${SOCKET_PATH_MAX_BYTES - SOCKET_SUFFIX_BYTES} bytes, such as /tmp, and run again.`);
|
|
50
|
+
}
|
|
@@ -9,7 +9,8 @@ export interface AnalysisInput {
|
|
|
9
9
|
readonly deps?: Pick<StudyDeps, "analysis">;
|
|
10
10
|
/** Cancels the analysis only. */
|
|
11
11
|
readonly analysisSignal?: AbortSignal;
|
|
12
|
-
/** Receives analysis-started and analysis-finished
|
|
12
|
+
/** Receives analysis-started and analysis-finished, around the analysis of a run where a
|
|
13
|
+
* participant's session started. */
|
|
13
14
|
readonly emit?: (event: StudyEvent) => void;
|
|
14
15
|
}
|
|
15
16
|
export interface AutomaticAnalysisResult {
|
|
@@ -38,8 +38,13 @@ export async function completeAutomaticAnalysis(result, finished, config, input,
|
|
|
38
38
|
const signal = input?.analysisSignal;
|
|
39
39
|
let started = false;
|
|
40
40
|
try {
|
|
41
|
-
|
|
42
|
-
started
|
|
41
|
+
// The CLI prints "Participants finished" on analysis-started. A run where no participant's
|
|
42
|
+
// session started (it failed before, such as at E2B login) still records its analysis
|
|
43
|
+
// outcome, but without the two events.
|
|
44
|
+
if (finished.participantsRan) {
|
|
45
|
+
input?.emit?.({ type: "analysis-started" });
|
|
46
|
+
started = true;
|
|
47
|
+
}
|
|
43
48
|
try {
|
|
44
49
|
await validatePreparedRunRootIdentity(prepared);
|
|
45
50
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { migrateStudies } from "../../study/migrate.js";
|
|
1
|
+
import { migrateStudies, } from "../../study/migrate/migrate.js";
|
|
2
2
|
import { CWD_OPTION_DESCRIPTION, JSON_OPTION_DESCRIPTION, wantsJson, writeResult, } from "../io.js";
|
|
3
3
|
function formatValue(value) {
|
|
4
4
|
const text = JSON.stringify(value);
|
|
@@ -217,9 +217,12 @@ function formatReclaimHuman(result, cwd, preflight) {
|
|
|
217
217
|
const search = result.tagSearch.status === "done"
|
|
218
218
|
? `E2B lists ${plural(result.tagSearch.found, "more sandbox", "more sandboxes")} tagged with it`
|
|
219
219
|
: `E2B tag search ${result.tagSearch.status}`;
|
|
220
|
+
const head = `${check ? "Reclaim check" : "Reclaim"} ${result.runId}: ${result.state}.`;
|
|
220
221
|
lines.push(result.reason === "dry-run"
|
|
221
|
-
? `${
|
|
222
|
-
:
|
|
222
|
+
? `${head} It was a dry run, which creates no sandboxes, so E2B was not contacted.`
|
|
223
|
+
: result.reason === "no-sandbox"
|
|
224
|
+
? `${head} Its status.json records that its route created no sandbox, so E2B was not contacted.`
|
|
225
|
+
: `${head} ${plural(result.receiptCount, "sandbox receipt")}; ${search}.`);
|
|
223
226
|
for (const outcome of result.outcomes) {
|
|
224
227
|
lines.push(` sandbox ${outcome.sandboxIdDigest} (${outcome.laneId}, from ${outcome.source}): ${outcome.state}${outcome.detail ? `: ${outcome.detail}` : ""}`);
|
|
225
228
|
}
|
|
@@ -13,6 +13,7 @@ import { writeRunFindings } from "../findings.js";
|
|
|
13
13
|
import { formatCuaStudyHuman } from "./study-format.js";
|
|
14
14
|
import { resolveRouteShouldOpen } from "./study-route-open.js";
|
|
15
15
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
16
|
+
import { actorOf, declaredParticipantCount } from "../../study/study-fields.js";
|
|
16
17
|
/**
|
|
17
18
|
* The computer-use route's CLI setup: its settings, the watch and exposure plan, the runStudyWith options
|
|
18
19
|
* with the live Observer hook, and how it presents the outcome or a run error. Undefined when
|
|
@@ -54,7 +55,7 @@ function refuseCua(args, dryRun, code, message) {
|
|
|
54
55
|
...studyResultIdentity("computer-use", args.config.id),
|
|
55
56
|
ok: false,
|
|
56
57
|
cwd: args.options.cwd,
|
|
57
|
-
actor: args.config
|
|
58
|
+
actor: actorOf(args.config)?.type ?? "",
|
|
58
59
|
appUrl: "",
|
|
59
60
|
dryRun,
|
|
60
61
|
runId: args.options.runId ?? "not-created",
|
|
@@ -79,7 +80,7 @@ function resolveCuaSettings(args) {
|
|
|
79
80
|
args.io.setExitCode(2);
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
|
-
const count = parseStudyCount(args.options.count, args.config
|
|
83
|
+
const count = parseStudyCount(args.options.count, declaredParticipantCount(args.config) ?? 1);
|
|
83
84
|
if (count === null) {
|
|
84
85
|
args.io.writeErr("error: --count must be a positive integer.\n");
|
|
85
86
|
args.io.setExitCode(2);
|
|
@@ -2,12 +2,13 @@ import { Command } from "commander";
|
|
|
2
2
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
3
3
|
import { formatRunHuman, parseStudyCount, writeResult, } from "../io.js";
|
|
4
4
|
import { planObserver, showObserver, staticObserverOpen } from "../observer-follow.js";
|
|
5
|
+
import { declaredParticipantCount } from "../../study/study-fields.js";
|
|
5
6
|
/**
|
|
6
7
|
* The preview route's CLI setup: the participant count and Observer plan, its runStudyWith options, and how it
|
|
7
8
|
* presents the outcome. Undefined when setup has already written its own result.
|
|
8
9
|
*/
|
|
9
10
|
export function previewRouteRun(args) {
|
|
10
|
-
const participantCount = parseStudyCount(args.options.count, args.config
|
|
11
|
+
const participantCount = parseStudyCount(args.options.count, declaredParticipantCount(args.config) ?? 4);
|
|
11
12
|
if (participantCount === null) {
|
|
12
13
|
const result = {
|
|
13
14
|
...studyResultIdentity("preview", args.config.id),
|
|
@@ -9,8 +9,8 @@ import { followDecision, followObserver, showObserver, staticObserverOpen, withO
|
|
|
9
9
|
import { writeRunFindings } from "../findings.js";
|
|
10
10
|
import { formatConcurrentSharedWorldStudyHuman } from "./study-format.js";
|
|
11
11
|
import { resolveRouteShouldOpen, watchFinishedPlan } from "./study-route-open.js";
|
|
12
|
-
import { rosterOf } from "../../study/parse/actors.js";
|
|
13
12
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
13
|
+
import { actorOf, participantList } from "../../study/study-fields.js";
|
|
14
14
|
/**
|
|
15
15
|
* The shared-world route's CLI setup: dry run, open and follow semantics, the live Observer server
|
|
16
16
|
* the run attaches, and how it presents the outcome. Undefined when setup has already written its
|
|
@@ -34,10 +34,10 @@ export function sharedWorldRouteRun(args) {
|
|
|
34
34
|
...studyResultIdentity("shared-world", args.config.id),
|
|
35
35
|
ok: false,
|
|
36
36
|
cwd: args.options.cwd,
|
|
37
|
-
actor: args.config
|
|
37
|
+
actor: actorOf(args.config)?.type ?? "",
|
|
38
38
|
topology: "shared-world",
|
|
39
39
|
topologyMode: "concurrent",
|
|
40
|
-
roleCount:
|
|
40
|
+
roleCount: participantList(args.config)?.length ?? 0,
|
|
41
41
|
concurrency: args.config.execution?.concurrency ?? 1,
|
|
42
42
|
dryRun,
|
|
43
43
|
runId: runId ?? args.options.runId ?? "not-created",
|
|
@@ -4,6 +4,7 @@ import { Command, Option } from "commander";
|
|
|
4
4
|
import { inspectStudyManifest, listStudyManifests } from "../../study/discover.js";
|
|
5
5
|
import { runStudyPreflight, } from "../../study/preflight.js";
|
|
6
6
|
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, humanError, } from "../io.js";
|
|
7
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
7
8
|
export function registerStudyCommands(parent, io) {
|
|
8
9
|
const study = parent
|
|
9
10
|
.command("study")
|
|
@@ -114,7 +115,7 @@ function formatStudyShowHuman(result) {
|
|
|
114
115
|
`id: ${config.id}`,
|
|
115
116
|
`subject: ${config.subject.source}`,
|
|
116
117
|
...(config.execution?.target ? [`execution: ${config.execution.target}`] : []),
|
|
117
|
-
`
|
|
118
|
+
`actor: ${actorOf(config)?.type ?? ""}`,
|
|
118
119
|
...(config.title ? [`title: ${config.title}`] : []),
|
|
119
120
|
...(config.description ? [`description: ${config.description}`] : []),
|
|
120
121
|
...(result.path ? [`path: ${result.path}`] : []),
|
|
@@ -102,7 +102,14 @@ export async function runStudyParticipants(setup, participants) {
|
|
|
102
102
|
let failFastReason;
|
|
103
103
|
try {
|
|
104
104
|
if (!dryRun)
|
|
105
|
-
({ outcomes, failFastReason } = await runAllCuaParticipants(participantRuns,
|
|
105
|
+
({ outcomes, failFastReason } = await runAllCuaParticipants(participantRuns, {
|
|
106
|
+
...deps,
|
|
107
|
+
// A session that throws returns no trace; the run still records that it started.
|
|
108
|
+
runSession: (options) => {
|
|
109
|
+
run.participantStarted();
|
|
110
|
+
return deps.runSession(options);
|
|
111
|
+
},
|
|
112
|
+
}, participantPlan, inProcess));
|
|
106
113
|
}
|
|
107
114
|
finally {
|
|
108
115
|
try {
|
|
@@ -10,6 +10,7 @@ import { createRestrictedCodexParticipant } from "../../actors/codex/restricted-
|
|
|
10
10
|
import { withCloseReport } from "./participant-model.js";
|
|
11
11
|
import { guestMediaConfigSchema } from "../../guest/media-config.js";
|
|
12
12
|
import { startLocalCapturedInbox } from "../../substrates/local/captured-inbox.js";
|
|
13
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
13
14
|
function guestMedia(config) {
|
|
14
15
|
const declaredMedia = config.execution?.desktop?.media;
|
|
15
16
|
return declaredMedia === undefined
|
|
@@ -164,7 +165,7 @@ export function prepareLocalVmRun(options) {
|
|
|
164
165
|
const media = guestMedia(config);
|
|
165
166
|
// A caller-supplied provider replaces the Codex account participant, so the account is
|
|
166
167
|
// neither checked nor used.
|
|
167
|
-
const account = config
|
|
168
|
+
const account = actorOf(config)?.type === "local-agent" && options.createProvider === undefined;
|
|
168
169
|
const state = { sessions: [], participants: [], cleanupUnconfirmed: false };
|
|
169
170
|
const context = {
|
|
170
171
|
config,
|
|
@@ -84,7 +84,7 @@ export async function startParticipantModel(spec, deps, executor) {
|
|
|
84
84
|
/** The options the participant's session runs with: prompt, model settings, spend caps and callbacks. */
|
|
85
85
|
export function participantSessionOptions(spec, deps, ready, provider, writeScreenshot) {
|
|
86
86
|
const { executor, inbox } = ready;
|
|
87
|
-
// The fail-closed spend cap (
|
|
87
|
+
// The fail-closed spend cap (caps.maxUsd) is wired into the loop as maxUsd + an
|
|
88
88
|
// injected pure per-turn estimator keyed on the resolved model. Preflight already refused a
|
|
89
89
|
// cap on an unpriced model, so the estimate is measurable whenever a cap is in force. The
|
|
90
90
|
// model id here matches provider.version (openai-responses-cu resolves the default when unset).
|
|
@@ -7,15 +7,16 @@ import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
|
7
7
|
import { isHttpUrl, isLoopbackUrl } from "../../study/parse/subject.js";
|
|
8
8
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
9
9
|
import { subjectStateInvalidReason } from "../../study/parse/subject-state.js";
|
|
10
|
-
import { brainOf, callerBrainOf,
|
|
10
|
+
import { brainOf, callerBrainOf, planCaps, desktopRequirements, planBase, provisionedSubject, } from "../../study/plan-base.js";
|
|
11
11
|
import { computerUseParticipants, declaredTargets } from "../../study/plan-participants.js";
|
|
12
12
|
import { MAX_COMPUTER_USE_PARTICIPANTS } from "../../study/routing.js";
|
|
13
|
-
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason,
|
|
13
|
+
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
14
14
|
import { desktopCliProductReason } from "../../study/composition-rules.js";
|
|
15
15
|
import { isLocalBrowserStudy } from "../../substrates/local/runtime-config.js";
|
|
16
16
|
import { boundedConcurrency, defaultSessionTimeoutMs, resolveParticipantSandboxMs, } from "./participant-runs.js";
|
|
17
17
|
import { MAX_SANDBOX_MS } from "../../substrates/e2b/lifetime.js";
|
|
18
18
|
import {} from "./types.js";
|
|
19
|
+
import { actorOf, declaresSharedWorld } from "../../study/study-fields.js";
|
|
19
20
|
/** The registered descriptor for a planned actor id; planComputerUseStudy checked the registry. */
|
|
20
21
|
export function cuaDescriptorOf(actor) {
|
|
21
22
|
const descriptor = actorRegistry[actor];
|
|
@@ -83,12 +84,10 @@ function cuaStudyRejection(config, hasRunSession, driving, subjectRoute) {
|
|
|
83
84
|
}
|
|
84
85
|
/** A declaration this route cannot honor, for any subject or with the caller's own driver. */
|
|
85
86
|
function unsupportedDeclarationReason(config, hasRunSession, driving, { inProcessRoute }) {
|
|
86
|
-
const reason = desktopMediaValidationReason(config) ||
|
|
87
|
-
outputTokenLimitValidationReason(config) ||
|
|
88
|
-
scenarioCapsValidationReason(config);
|
|
87
|
+
const reason = desktopMediaValidationReason(config) || outputTokenLimitValidationReason(config);
|
|
89
88
|
if (reason)
|
|
90
89
|
return invalid(reason);
|
|
91
|
-
if (config
|
|
90
|
+
if (actorOf(config)?.maxOutputTokens !== undefined &&
|
|
92
91
|
(hasRunSession || driving.createProvider || driving.inProcess))
|
|
93
92
|
return invalid("maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.");
|
|
94
93
|
const receivingReason = receivingEmailValidationReason(config);
|
|
@@ -176,7 +175,7 @@ function driverReason(driving, { localAppSubject, inProcessRoute }) {
|
|
|
176
175
|
}
|
|
177
176
|
/** The participant roster, then the sandbox deadline its session budget derives. */
|
|
178
177
|
function rosterShapeReason(config) {
|
|
179
|
-
//
|
|
178
|
+
// Device XOR raw resolution, cap, unique ids,
|
|
180
179
|
// allowPublicTargets with more than one participant, clone.fanout.
|
|
181
180
|
const fanoutReason = computerUseValidationReason(config);
|
|
182
181
|
if (fanoutReason)
|
|
@@ -220,7 +219,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
220
219
|
if (tasksReason)
|
|
221
220
|
return refuse("before-scope", "HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
222
221
|
// The parser checks the actor too; a library caller skips the parser.
|
|
223
|
-
const actorType = config
|
|
222
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
224
223
|
const descriptor = actorRegistry[actorType];
|
|
225
224
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
226
225
|
return refuse("in-scope", "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
@@ -231,7 +230,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
231
230
|
// A shared world runs every participant against one app; this route would run them as separate
|
|
232
231
|
// participants. It comes after the rules above, so a shared-world config that breaks one of them,
|
|
233
232
|
// which runStudyWith sends here, still gets that rule's message.
|
|
234
|
-
if (config
|
|
233
|
+
if (declaresSharedWorld(config))
|
|
235
234
|
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
235
|
// The in-process route drives subject.appUrl on this machine and creates no desktop, so it would
|
|
237
236
|
// skip the subject a clone, local-tree or desktop-cli study declares.
|
|
@@ -292,7 +291,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
292
291
|
concurrency: boundedConcurrency(declared, n),
|
|
293
292
|
sessionBudgetMs: config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config),
|
|
294
293
|
sandboxMs: resolveParticipantSandboxMs(config),
|
|
295
|
-
caps:
|
|
294
|
+
caps: planCaps(config),
|
|
296
295
|
...(input.rerun === undefined ? {} : { rerun: rerunPlan(input.rerun) }),
|
|
297
296
|
requirements: base.dryRun || runner.desktop === "in-process"
|
|
298
297
|
? []
|
|
@@ -59,7 +59,7 @@ export async function liveCuaRejection(args) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
// Fail-closed cap: a maxUsd cap needs a measurable per-turn estimate.
|
|
62
|
-
// If the operator set
|
|
62
|
+
// If the operator set caps.maxUsd but src/run/pricing.ts has no rate for the resolved
|
|
63
63
|
// model, the loop could not enforce the cap, and silently running uncapped would break the
|
|
64
64
|
// runaway-retry protection. Refuse at preflight (before any sandbox/spend) rather than run
|
|
65
65
|
// uncapped: an unenforceable cap is more dangerous than none. The operator picks a priced model
|
|
@@ -193,7 +193,7 @@ function computerUseExecutionFailures(outcomes, runId) {
|
|
|
193
193
|
];
|
|
194
194
|
}
|
|
195
195
|
/**
|
|
196
|
-
*
|
|
196
|
+
* caps.maxUsd is enforced inside each participant's loop independently, so an
|
|
197
197
|
* N-participant fan-out can spend up to N × maxUsd before any participant aborts, while the run cost summary reports the larger
|
|
198
198
|
* aggregate. The warning names that ceiling, unless the study declared a shared maxTotalUsd budget.
|
|
199
199
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The computer-use route: a subject (an app-url the caller provisioned, or a repo the
|
|
2
2
|
// humanish clones and serves in the sandbox) driven by a registry-resolved computer-use actor inside a
|
|
3
|
-
// hosted E2B desktop. On this path `
|
|
3
|
+
// hosted E2B desktop. On this path `actor.type` selects the actor: the
|
|
4
4
|
// descriptor returned by the registry runs the session; the route provisions the desktop and
|
|
5
5
|
// subject, composes the prompt from config, persists the evidence bundle, and tears down.
|
|
6
6
|
//
|
|
@@ -32,6 +32,7 @@ import { runStudyParticipants } from "./live-phase.js";
|
|
|
32
32
|
import { admitCuaRun, refuseCuaStudy, startCuaRun } from "./setup.js";
|
|
33
33
|
import {} from "./types.js";
|
|
34
34
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
35
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
35
36
|
/**
|
|
36
37
|
* A refused computer-use study's result, at the refusal's stage: a before-scope refusal has its own
|
|
37
38
|
* envelope and no analysis record; the others come after the cwd checks, and the participant cap after
|
|
@@ -44,7 +45,7 @@ export async function computerUseStudyRefusal(options, refusal) {
|
|
|
44
45
|
...studyResultIdentity("computer-use", config.id),
|
|
45
46
|
ok: false,
|
|
46
47
|
cwd: path.resolve(options.cwd),
|
|
47
|
-
actor: config
|
|
48
|
+
actor: actorOf(config)?.type ?? "",
|
|
48
49
|
dryRun,
|
|
49
50
|
runId: options.runId ?? "not-created",
|
|
50
51
|
appUrl: config.subject.appUrl ?? config.subject.serve?.url ?? "",
|
|
@@ -25,6 +25,7 @@ import { projectParticipantSubjects, subjectProvenanceArg } from "./subject-proj
|
|
|
25
25
|
import { participantSubjectEnv, } from "./types.js";
|
|
26
26
|
import { studyPersonaIds } from "../../study/persona-resolve.js";
|
|
27
27
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
28
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
28
29
|
/** The physical project, bound before any caller hook runs. */
|
|
29
30
|
async function bindProject(cwd) {
|
|
30
31
|
// Capture the physical project before reading or invoking any caller hook. A supported
|
|
@@ -47,7 +48,7 @@ export async function refuseCuaStudy(options, refusal) {
|
|
|
47
48
|
...studyResultIdentity("computer-use", config.id),
|
|
48
49
|
ok: false,
|
|
49
50
|
cwd: projectRoot.physicalPath,
|
|
50
|
-
actor: refusal.actor ?? config
|
|
51
|
+
actor: refusal.actor ?? actorOf(config)?.type ?? "",
|
|
51
52
|
appUrl: declaredAppUrl(config),
|
|
52
53
|
dryRun,
|
|
53
54
|
runId: options.runId ?? "not-created",
|
|
@@ -52,8 +52,8 @@ export interface ComputerUseRunInput {
|
|
|
52
52
|
cwd: string;
|
|
53
53
|
open?: boolean;
|
|
54
54
|
runId?: string;
|
|
55
|
-
/** CLI `--count` override for the homogeneous fan-out participant count (ignored when a `
|
|
56
|
-
*
|
|
55
|
+
/** CLI `--count` override for the homogeneous fan-out participant count (ignored when a `participants`
|
|
56
|
+
* list is declared; a roster's length is authoritative). */
|
|
57
57
|
countOverride?: number;
|
|
58
58
|
/** Explicitly create a new run containing failed or selected participants from a prior fan-out run. */
|
|
59
59
|
rerun?: {
|
|
@@ -182,7 +182,7 @@ export interface CuaParticipantSummary {
|
|
|
182
182
|
concurrency: number;
|
|
183
183
|
waves: number;
|
|
184
184
|
}
|
|
185
|
-
export type CuaActorStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
185
|
+
export type CuaActorStudyErrorCode = "HUMANISH_STUDY_ANALYSIS_INVALID" | "HUMANISH_STUDY_TASKS_UNSUPPORTED" | "HUMANISH_STUDY_OPTION_UNSUPPORTED" | "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID" | "HUMANISH_COMPUTER_USE_FAILED" | "HUMANISH_COMPUTER_USE_KEYS_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_MISSING" | "HUMANISH_COMPUTER_USE_AGENT_SIGNIN_REQUIRED" | "HUMANISH_COMPUTER_USE_SUBJECT_ENV_MISSING" | "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED" | "HUMANISH_COMPUTER_USE_SUBJECT_INVALID" | "HUMANISH_COMPUTER_USE_SUBJECT_UNSAFE" | "HUMANISH_COMPUTER_USE_EXECUTOR_NO_PROVIDER" | "HUMANISH_COMPUTER_USE_LOCAL_APP_NO_EXECUTOR" | "HUMANISH_COMPUTER_USE_LOCAL_DESKTOP_MISSING" | "HUMANISH_COMPUTER_USE_FANOUT_INVALID" | "HUMANISH_COMPUTER_USE_RERUN_INVALID" | "HUMANISH_COMPUTER_USE_DEVICE_GEOMETRY" | "HUMANISH_RUN_ID_IN_USE" | "HUMANISH_COMPUTER_USE_UNPRICED_CAP" | "HUMANISH_COMPUTER_USE_COMMS_CATCH_UNREACHABLE" | "HUMANISH_COMPUTER_USE_COMMS_TOKEN_INVALID" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE" | "HUMANISH_SERVE_TUNNEL_NOT_FOUND" | "HUMANISH_SERVE_TUNNEL_START_FAILED";
|
|
186
186
|
/** Subject provenance projection: what the actor actually drove. */
|
|
187
187
|
export interface CuaSubjectProjection {
|
|
188
188
|
source: "app-url" | "clone" | "local-tree";
|
|
@@ -385,7 +385,7 @@ export interface ParticipantModelDeps {
|
|
|
385
385
|
artifactRoot: PreparedOutputRoot;
|
|
386
386
|
redactScreenshots: boolean;
|
|
387
387
|
scrubKnownValues: (text: string) => string;
|
|
388
|
-
/** The study's shared spend ledger, present exactly when
|
|
388
|
+
/** The study's shared spend ledger, present exactly when caps.maxTotalUsd is set on a
|
|
389
389
|
* live run. Preflight already refused the cap on an unpriced model. */
|
|
390
390
|
runBudget?: CuaRunBudget;
|
|
391
391
|
/**
|
|
@@ -7,6 +7,7 @@ import { browserSurfaces } from "../../actors/scripted-browser/types.js";
|
|
|
7
7
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
8
8
|
import { isNonEmpty, planBase, provisionedSubject } from "../../study/plan-base.js";
|
|
9
9
|
import { cloneTargetValidationReason, desktopMediaValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
10
|
+
import { actorOf, surfaceCount, scenarioRefOf } from "../../study/study-fields.js";
|
|
10
11
|
// Default surface roster is 1 (desktop only): the defaults-table single-participant row governs;
|
|
11
12
|
// `count: 2` is the declared override that adds the mobile surface.
|
|
12
13
|
const DEFAULT_SURFACE_COUNT = 1;
|
|
@@ -42,7 +43,7 @@ export function planScriptedStudy(config, input) {
|
|
|
42
43
|
const cloneTargetReason = cloneTargetValidationReason(config);
|
|
43
44
|
if (cloneTargetReason)
|
|
44
45
|
return refuse("HUMANISH_SCRIPTED_SUBJECT_UNSAFE", cloneTargetReason);
|
|
45
|
-
const actorType = config
|
|
46
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
46
47
|
const descriptor = actorRegistry[actorType];
|
|
47
48
|
if (!descriptor || !isScriptedBrowserActorDescriptor(descriptor))
|
|
48
49
|
return refuse("HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered scripted-browser actor.`);
|
|
@@ -63,7 +64,7 @@ export function planScriptedStudy(config, input) {
|
|
|
63
64
|
subject = { kind: "loopback", appUrl };
|
|
64
65
|
}
|
|
65
66
|
// The steps are the actor, so there is no built-in journey to fall back on.
|
|
66
|
-
const scenarioRef = config
|
|
67
|
+
const scenarioRef = scenarioRefOf(config);
|
|
67
68
|
if (!scenarioRef?.trim())
|
|
68
69
|
return refuse("HUMANISH_SCRIPTED_SCENARIO_INVALID", "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.", { actor, appUrl: evidenceAppUrlOf(subject) });
|
|
69
70
|
const requirements = [];
|
|
@@ -75,7 +76,7 @@ export function planScriptedStudy(config, input) {
|
|
|
75
76
|
// The browser runs on this machine on both subjects; a clone's is pointed at its getHost URL.
|
|
76
77
|
if (!input.dryRun && input.injectedBrowser !== true)
|
|
77
78
|
requirements.push({ kind: "host-browser" });
|
|
78
|
-
const persona = config
|
|
79
|
+
const persona = actorOf(config)?.persona;
|
|
79
80
|
const timeoutMs = config.execution?.timeoutMs;
|
|
80
81
|
return {
|
|
81
82
|
ok: true,
|
|
@@ -88,7 +89,7 @@ export function planScriptedStudy(config, input) {
|
|
|
88
89
|
actor,
|
|
89
90
|
subject,
|
|
90
91
|
scenarioRef,
|
|
91
|
-
surfaces: browserSurfaces.slice(0, config
|
|
92
|
+
surfaces: browserSurfaces.slice(0, surfaceCount(config) ?? DEFAULT_SURFACE_COUNT),
|
|
92
93
|
...(persona === undefined ? {} : { personaId: persona }),
|
|
93
94
|
...(timeoutMs === undefined ? {} : { sessionTimeoutMs: timeoutMs }),
|
|
94
95
|
requirements,
|