humanish 0.110.3 → 0.110.4
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 +14 -0
- 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 +4 -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/plan.js +6 -5
- package/dist/routes/computer-use/route.js +2 -1
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.js +5 -1
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/shared-world/participant-specs.js +5 -1
- package/dist/routes/shared-world/plan.js +7 -6
- package/dist/routes/shared-world/route.js +3 -3
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.js +2 -1
- package/dist/routes/terminal/session.js +1 -0
- 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/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/study/composition-rules.js +32 -32
- package/dist/study/config.d.ts +9 -2
- package/dist/study/config.js +14 -5
- package/dist/study/doctor.js +5 -4
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +29 -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 +68 -0
- package/dist/study/migrate/v2.js +480 -0
- package/dist/study/parse/actors.d.ts +46 -1
- package/dist/study/parse/actors.js +116 -69
- package/dist/study/parse/execution.js +2 -2
- package/dist/study/parse/study-v3.d.ts +26 -0
- package/dist/study/parse/study-v3.js +53 -35
- package/dist/study/parse/study.d.ts +38 -0
- package/dist/study/parse/study.js +225 -0
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +2 -1
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -4
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan.js +4 -3
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.js +2 -2
- package/dist/study/routing.js +12 -15
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-fields.d.ts +28 -0
- package/dist/study/study-fields.js +51 -0
- package/dist/study/summary.js +13 -17
- package/dist/study/types.d.ts +40 -0
- package/dist/study/url-credentials.js +2 -1
- package/dist/study/validation.d.ts +2 -0
- package/dist/study/validation.js +46 -41
- package/dist/study/warnings.js +5 -4
- package/dist/substrates/local/runtime-config.js +9 -9
- package/dist/tui-app.js +3 -3
- package/docs/contracts/schemas.md +13 -4
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ 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.4: Scripted runs reclaim clean, review says interrupted, Chrome under a long TMPDIR (2026-10-05)
|
|
12
|
+
|
|
13
|
+
humanish 0.110.4 fixes four findings of the 0.110.3 live smoke. `humanish reclaim --check` reports
|
|
14
|
+
a live scripted run against an `app-url` subject `clean`, with reason `no-sandbox`, and exits 0.
|
|
15
|
+
`humanish review` on a computer-use run stopped by Ctrl-C after its desktop started says the run
|
|
16
|
+
was interrupted. A run that failed before any participant started no longer prints "Participants
|
|
17
|
+
finished; preparing analysis…". The scripted route launches Chrome when `TMPDIR` is too long for
|
|
18
|
+
Chrome's socket path. Planner refusals for a library config built without `parseStudy` name v3
|
|
19
|
+
keys, and so does `humanish migrate` when one of those checks refuses a v2 file. Inside, a v3
|
|
20
|
+
parser runs beside `parseStudy`, src reads the study through accessors, and `humanish migrate`
|
|
21
|
+
reads v2 files through its own front end; the library API is unchanged.
|
|
22
|
+
|
|
23
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.110.4)
|
|
24
|
+
|
|
11
25
|
## 0.110.3: Ctrl-C at the watch prompt, review says analysis is off, E2B docs links kept (2026-10-05)
|
|
12
26
|
|
|
13
27
|
humanish 0.110.3 runs the `watch` and `observe` shutdown, and prints `watch stopped` or `observe
|
|
@@ -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 { actorsOf } from "../../study/study-fields.js";
|
|
7
8
|
export function registerStudyCommands(parent, io) {
|
|
8
9
|
const study = parent
|
|
9
10
|
.command("study")
|
|
@@ -114,7 +115,9 @@ function formatStudyShowHuman(result) {
|
|
|
114
115
|
`id: ${config.id}`,
|
|
115
116
|
`subject: ${config.subject.source}`,
|
|
116
117
|
...(config.execution?.target ? [`execution: ${config.execution.target}`] : []),
|
|
117
|
-
`actors: ${config
|
|
118
|
+
`actors: ${actorsOf(config)
|
|
119
|
+
.map((actor) => actor.type)
|
|
120
|
+
.join(", ")}`,
|
|
118
121
|
...(config.title ? [`title: ${config.title}`] : []),
|
|
119
122
|
...(config.description ? [`description: ${config.description}`] : []),
|
|
120
123
|
...(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,
|
|
@@ -7,7 +7,7 @@ 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
13
|
import { cloneTargetValidationReason, computerUseValidationReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
@@ -16,6 +16,7 @@ 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];
|
|
@@ -88,7 +89,7 @@ function unsupportedDeclarationReason(config, hasRunSession, driving, { inProces
|
|
|
88
89
|
scenarioCapsValidationReason(config);
|
|
89
90
|
if (reason)
|
|
90
91
|
return invalid(reason);
|
|
91
|
-
if (config
|
|
92
|
+
if (actorOf(config)?.maxOutputTokens !== undefined &&
|
|
92
93
|
(hasRunSession || driving.createProvider || driving.inProcess))
|
|
93
94
|
return invalid("maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.");
|
|
94
95
|
const receivingReason = receivingEmailValidationReason(config);
|
|
@@ -220,7 +221,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
220
221
|
if (tasksReason)
|
|
221
222
|
return refuse("before-scope", "HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
222
223
|
// The parser checks the actor too; a library caller skips the parser.
|
|
223
|
-
const actorType = config
|
|
224
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
224
225
|
const descriptor = actorRegistry[actorType];
|
|
225
226
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
226
227
|
return refuse("in-scope", "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
@@ -231,7 +232,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
231
232
|
// A shared world runs every participant against one app; this route would run them as separate
|
|
232
233
|
// participants. It comes after the rules above, so a shared-world config that breaks one of them,
|
|
233
234
|
// which runStudyWith sends here, still gets that rule's message.
|
|
234
|
-
if (config
|
|
235
|
+
if (declaresSharedWorld(config))
|
|
235
236
|
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
237
|
// The in-process route drives subject.appUrl on this machine and creates no desktop, so it would
|
|
237
238
|
// skip the subject a clone, local-tree or desktop-cli study declares.
|
|
@@ -292,7 +293,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
292
293
|
concurrency: boundedConcurrency(declared, n),
|
|
293
294
|
sessionBudgetMs: config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config),
|
|
294
295
|
sandboxMs: resolveParticipantSandboxMs(config),
|
|
295
|
-
caps:
|
|
296
|
+
caps: planCaps(config, "computer-use"),
|
|
296
297
|
...(input.rerun === undefined ? {} : { rerun: rerunPlan(input.rerun) }),
|
|
297
298
|
requirements: base.dryRun || runner.desktop === "in-process"
|
|
298
299
|
? []
|
|
@@ -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",
|
|
@@ -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,
|
|
@@ -31,6 +31,7 @@ import { ScriptedSubject } from "./subject.js";
|
|
|
31
31
|
import { runScriptedSessions, writeSurfaceTraces } from "./surface-sessions.js";
|
|
32
32
|
import {} from "./types.js";
|
|
33
33
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
34
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
34
35
|
/**
|
|
35
36
|
* A refused scripted study's result, at the refusal's stage: a before-scope refusal has its own field
|
|
36
37
|
* order and no analysis record; the others come after the output directory checks.
|
|
@@ -38,7 +39,7 @@ import { studyResultIdentity } from "../../run/study-result.js";
|
|
|
38
39
|
export async function scriptedStudyRefusal(options, refusal) {
|
|
39
40
|
const { config, dryRun } = options;
|
|
40
41
|
const cwd = path.resolve(options.cwd);
|
|
41
|
-
const actorType = config
|
|
42
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
42
43
|
if (refusal.beforeScope)
|
|
43
44
|
return {
|
|
44
45
|
...studyResultIdentity("scripted", config.id),
|
|
@@ -94,6 +95,8 @@ async function runScriptedPlanInScope(plan, input, scope) {
|
|
|
94
95
|
mintRunId: makeScriptedRunId,
|
|
95
96
|
mode: dryRun ? "dry-run" : "live",
|
|
96
97
|
study: plan.study,
|
|
98
|
+
// Only a clone subject is served from a sandbox; an app-url run drives a local browser.
|
|
99
|
+
sandboxes: setup.clone ? undefined : "none",
|
|
97
100
|
renderReview: renderScriptedReviewMarkdown,
|
|
98
101
|
observer: { open: input.open === true, render: setup.deps.renderObserver },
|
|
99
102
|
});
|
|
@@ -144,6 +147,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
|
|
|
144
147
|
deps: setup.deps,
|
|
145
148
|
browserCommand: setup.browserCommand,
|
|
146
149
|
runPaths,
|
|
150
|
+
onSessionStart: () => run.participantStarted(),
|
|
147
151
|
});
|
|
148
152
|
}
|
|
149
153
|
catch (error) {
|
|
@@ -8,6 +8,8 @@ export declare function runScriptedSessions(session: Required<Pick<ScriptedBrows
|
|
|
8
8
|
deps: StudyDeps;
|
|
9
9
|
browserCommand: string | undefined;
|
|
10
10
|
runPaths: PreparedRunArtifactPaths;
|
|
11
|
+
/** Called as each surface's session starts, before it can throw or return. */
|
|
12
|
+
onSessionStart: () => void;
|
|
11
13
|
}): Promise<ScriptedBrowserSessionResult[]>;
|
|
12
14
|
/** Writes each surface's actor-<surface>.json, as the computer-use route writes actor.json. */
|
|
13
15
|
export declare function writeSurfaceTraces(runPaths: PreparedRunArtifactPaths, results: readonly ScriptedBrowserSessionResult[]): Promise<void>;
|
|
@@ -7,9 +7,10 @@ import { validateScriptedSessionResult } from "./session-result.js";
|
|
|
7
7
|
/** One session per surface, in parallel. */
|
|
8
8
|
export function runScriptedSessions(session, run) {
|
|
9
9
|
const { appUrl, evidenceAppUrl, urlPolicy, journey, persona, timeoutMs, artifactRoot } = session;
|
|
10
|
-
const { surfaces, deps, browserCommand, runPaths } = run;
|
|
10
|
+
const { surfaces, deps, browserCommand, runPaths, onSessionStart } = run;
|
|
11
11
|
const runSession = deps.runScriptedSession;
|
|
12
12
|
return Promise.all(surfaces.map((surface) => {
|
|
13
|
+
onSessionStart();
|
|
13
14
|
const sessionOptions = {
|
|
14
15
|
appUrl,
|
|
15
16
|
evidenceAppUrl,
|
|
@@ -167,7 +167,11 @@ export function participantRunDeps(ctx, live, scrubKnownValues) {
|
|
|
167
167
|
studyCwd: ctx.cwd,
|
|
168
168
|
redactScreenshots: ctx.redactScreenshots,
|
|
169
169
|
scrubKnownValues,
|
|
170
|
-
|
|
170
|
+
// A session that throws returns no trace; the run still records that it started.
|
|
171
|
+
runSession: (options) => {
|
|
172
|
+
ctx.run.participantStarted();
|
|
173
|
+
return ctx.runSession(options);
|
|
174
|
+
},
|
|
171
175
|
// A local-agent brain runs each participant on the operator's signed-in agent, as on computer
|
|
172
176
|
// use.
|
|
173
177
|
brain: ctx.plan.brain,
|
|
@@ -6,12 +6,13 @@ import { DEFAULT_OPENAI_CU_MODEL } from "../../actors/computer-use/openai-provid
|
|
|
6
6
|
import { studyUrlCredentialReason } from "../../study/url-credentials.js";
|
|
7
7
|
import { actorRegistry, isCuaActorDescriptor, } from "../../actors/registry.js";
|
|
8
8
|
import { resolveAutomaticAnalysis } from "../../analysis/automatic-config.js";
|
|
9
|
-
import { brainOf,
|
|
9
|
+
import { brainOf, planCaps, desktopRequirements, isNonEmpty, planBase, } from "../../study/plan-base.js";
|
|
10
10
|
import { sharedWorldParticipants } from "../../study/plan-participants.js";
|
|
11
11
|
import { PUBLIC_TARGET_OWNER_PATTERN } from "../../study/parse/subject.js";
|
|
12
12
|
import { REPO_SLUG_PATTERN } from "../../study/parse/values.js";
|
|
13
13
|
import { concurrentSharedWorldValidationReason, desktopMediaValidationReason, externalPublicSharedWorldValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, scenarioCapsValidationReason, taskProtocolValidationReason, } from "../../study/validation.js";
|
|
14
14
|
import { MODEL_RATES, unpricedCapMessage } from "../../run/pricing.js";
|
|
15
|
+
import { actorOf, capsOf } from "../../study/study-fields.js";
|
|
15
16
|
/** The registered descriptor for a planned actor; planSharedWorldStudy checked the registry. */
|
|
16
17
|
export function sharedWorldDescriptorOf(actorType) {
|
|
17
18
|
const descriptor = actorRegistry[actorType];
|
|
@@ -38,7 +39,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
38
39
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
39
40
|
if (tasksReason)
|
|
40
41
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
41
|
-
const actorType = config
|
|
42
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
42
43
|
const descriptor = actorRegistry[actorType];
|
|
43
44
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
44
45
|
return refuse("HUMANISH_SHARED_WORLD_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
@@ -59,11 +60,11 @@ export function planSharedWorldStudy(config, input) {
|
|
|
59
60
|
: concurrentSharedWorldValidationReason(config));
|
|
60
61
|
if (invalidReason)
|
|
61
62
|
return refuse(invalid, invalidReason, actor);
|
|
62
|
-
if (config
|
|
63
|
+
if (actorOf(config)?.maxOutputTokens !== undefined && input.hasRunSession === true)
|
|
63
64
|
return refuse(invalid, "maxOutputTokens cannot be enforced by a custom runSession.", actor);
|
|
64
|
-
const caps = config
|
|
65
|
+
const caps = capsOf(config, "shared-world");
|
|
65
66
|
if (!input.dryRun && (caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
|
|
66
|
-
const model = (config
|
|
67
|
+
const model = (actorOf(config)?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
|
|
67
68
|
if (!MODEL_RATES[model])
|
|
68
69
|
return refuse("HUMANISH_SHARED_WORLD_UNPRICED_CAP", unpricedCapMessage(model), actor);
|
|
69
70
|
}
|
|
@@ -106,7 +107,7 @@ export function planSharedWorldStudy(config, input) {
|
|
|
106
107
|
? {}
|
|
107
108
|
: { sessionTimeoutMs: config.execution.timeoutMs }),
|
|
108
109
|
brain,
|
|
109
|
-
caps:
|
|
110
|
+
caps: planCaps(config, "shared-world"),
|
|
110
111
|
requirements: base.dryRun
|
|
111
112
|
? []
|
|
112
113
|
: [
|
|
@@ -48,7 +48,7 @@ import { prepareConcurrentRun } from "./setup.js";
|
|
|
48
48
|
import { browserRouteScorer } from "../../study/adapter-scorer-loader.js";
|
|
49
49
|
import { withLateScorer } from "../../study/route-inputs.js";
|
|
50
50
|
import {} from "./types.js";
|
|
51
|
-
import {
|
|
51
|
+
import { actorOf, participantList } from "../../study/study-fields.js";
|
|
52
52
|
/** The shared-world code for each local-agent refusal, kind for kind with computer-use. */
|
|
53
53
|
const LOCAL_AGENT_REFUSAL_CODES = {
|
|
54
54
|
"agent-missing": "HUMANISH_SHARED_WORLD_AGENT_MISSING",
|
|
@@ -59,11 +59,11 @@ const LOCAL_AGENT_REFUSAL_CODES = {
|
|
|
59
59
|
/** A refused shared-world study's result: the route's envelope, and a refusal's analysis record. */
|
|
60
60
|
export function sharedWorldStudyRefusal(options, refusal) {
|
|
61
61
|
const { config, dryRun } = options;
|
|
62
|
-
const declared =
|
|
62
|
+
const declared = participantList(config) ?? [];
|
|
63
63
|
const fail = concurrentStudyFailure({
|
|
64
64
|
cwd: path.resolve(options.cwd),
|
|
65
65
|
studyId: config.id,
|
|
66
|
-
actor: config
|
|
66
|
+
actor: actorOf(config)?.type ?? "",
|
|
67
67
|
participantCount: declared.length,
|
|
68
68
|
// The parser fills concurrency for multi-participant studies, so this fallback serves only
|
|
69
69
|
// library callers: every declared participant runs at once unless the author declared a cap.
|
|
@@ -13,6 +13,7 @@ import { NODE_BOOTSTRAP_TIMEOUT_MS } from "../../subject/node-bootstrap.js";
|
|
|
13
13
|
import { terminalSandboxTimeoutMs } from "./lifetime.js";
|
|
14
14
|
import { isExactRuntimeVersion, TERMINAL_RUNTIME_VERSION_TIMEOUT_MS } from "./runtime.js";
|
|
15
15
|
import { PRODUCT_SETUP_TIMEOUT_MS, TERMINAL_SANDBOX_TIMEOUT_BUFFER_MS, } from "./types.js";
|
|
16
|
+
import { actorOf, capsOf } from "../../study/study-fields.js";
|
|
16
17
|
/**
|
|
17
18
|
* Plan a terminal-product study. It is called for any config handed to the terminal runner, not
|
|
18
19
|
* only one routeOf sends here, so a config for another route gets this route's refusal.
|
|
@@ -40,12 +41,12 @@ export function planTerminalStudy(config, input) {
|
|
|
40
41
|
const tasksReason = taskProtocolValidationReason(config, false);
|
|
41
42
|
if (tasksReason)
|
|
42
43
|
return refuse("HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
43
|
-
const actorType = config
|
|
44
|
+
const actorType = actorOf(config)?.type ?? "";
|
|
44
45
|
const descriptor = actorRegistry[actorType];
|
|
45
46
|
if (!descriptor || !isTerminalActorDescriptor(descriptor))
|
|
46
47
|
return refuse("HUMANISH_TERMINAL_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered terminal actor.`);
|
|
47
48
|
const runtimeVersion = config.execution?.runtime?.version;
|
|
48
|
-
const actor = config
|
|
49
|
+
const actor = actorOf(config);
|
|
49
50
|
if ((config.execution?.runtime !== undefined && !isExactRuntimeVersion(runtimeVersion)) ||
|
|
50
51
|
(actor?.model !== undefined &&
|
|
51
52
|
(typeof actor.model !== "string" || actor.model.trim().length === 0)) ||
|
|
@@ -83,7 +84,7 @@ export function planTerminalStudy(config, input) {
|
|
|
83
84
|
...(egressAllow === undefined ? {} : { egressAllow }),
|
|
84
85
|
...(stdin === undefined ? {} : { stdin }),
|
|
85
86
|
};
|
|
86
|
-
const caps = config
|
|
87
|
+
const caps = capsOf(config, "terminal");
|
|
87
88
|
if (input.dryRun)
|
|
88
89
|
return { ok: true, plan: { ...shared, dryRun: true, ...(caps ? { caps } : {}) } };
|
|
89
90
|
// The live route injects the runtime key into one command only, and only a registered actor
|
|
@@ -54,6 +54,7 @@ import { terminalRouteScorer } from "../../study/adapter-scorer-loader.js";
|
|
|
54
54
|
import { withLateScorer } from "../../study/route-inputs.js";
|
|
55
55
|
import {} from "./types.js";
|
|
56
56
|
import { studyResultIdentity } from "../../run/study-result.js";
|
|
57
|
+
import { actorOf } from "../../study/study-fields.js";
|
|
57
58
|
/** A refused terminal study's result: the route's envelope, and the analysis record a refusal gets. */
|
|
58
59
|
export function terminalStudyRefusal(options, refusal) {
|
|
59
60
|
const { config, dryRun } = options;
|
|
@@ -61,7 +62,7 @@ export function terminalStudyRefusal(options, refusal) {
|
|
|
61
62
|
...studyResultIdentity("terminal", config.id),
|
|
62
63
|
ok: false,
|
|
63
64
|
cwd: path.resolve(options.cwd),
|
|
64
|
-
actor: refusal.actor ?? config
|
|
65
|
+
actor: refusal.actor ?? actorOf(config)?.type ?? "",
|
|
65
66
|
product: config.subject.product?.name ?? "",
|
|
66
67
|
dryRun,
|
|
67
68
|
runId: options.runId ?? "not-created",
|