humanish 0.115.0 → 0.116.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 +22 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/scripted-browser/steps.js +6 -5
- package/dist/analysis/analysis-limits.d.ts +35 -0
- package/dist/analysis/analysis-limits.js +41 -0
- package/dist/analysis/validation.js +14 -21
- package/dist/cli/commands/analyze.js +2 -1
- package/dist/cli/commands/notes.js +2 -1
- package/dist/cli/commands/run-signals.js +5 -1
- package/dist/cli/commands/runs.js +2 -1
- package/dist/cli/commands/setup.js +3 -3
- package/dist/cli/commands/study-format.d.ts +5 -2
- package/dist/cli/commands/study-format.js +29 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-scripted.js +1 -1
- package/dist/cli/commands/tui.js +59 -17
- package/dist/cli/doctor.js +9 -2
- package/dist/cli/findings-text.d.ts +5 -0
- package/dist/cli/findings-text.js +169 -0
- package/dist/cli/findings.d.ts +0 -4
- package/dist/cli/findings.js +5 -167
- package/dist/cli/io.js +6 -1
- package/dist/cli/update-check.js +4 -1
- package/dist/comms/connections.js +4 -3
- package/dist/comms/setup.js +18 -12
- package/dist/evidence/redaction.js +9 -1
- package/dist/evidence/secret-scrub.d.ts +15 -5
- package/dist/evidence/secret-scrub.js +67 -9
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/export-bundle.js +18 -14
- package/dist/feedback/feedback.js +25 -7
- package/dist/guest/desktop-keys.js +59 -5
- package/dist/keys/key-resolution.d.ts +9 -0
- package/dist/keys/key-resolution.js +60 -48
- package/dist/keys/key-status.d.ts +2 -0
- package/dist/keys/key-status.js +9 -16
- package/dist/observer/pinned-files.js +1 -1
- package/dist/observer/render.js +3 -2
- package/dist/observer-app.html +7 -7
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -4
- package/dist/routes/computer-use/bundle-parts.js +2 -2
- package/dist/routes/computer-use/bundle.d.ts +4 -5
- package/dist/routes/computer-use/bundle.js +1 -3
- package/dist/routes/computer-use/fanout-records.js +10 -11
- package/dist/routes/computer-use/live-phase.d.ts +1 -1
- package/dist/routes/computer-use/live-phase.js +0 -1
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-runs.d.ts +6 -2
- package/dist/routes/computer-use/participant-runs.js +9 -5
- package/dist/routes/computer-use/plan.d.ts +3 -0
- package/dist/routes/computer-use/plan.js +14 -11
- package/dist/routes/computer-use/rerun-selection.js +2 -2
- package/dist/routes/computer-use/result.js +3 -6
- package/dist/routes/computer-use/setup.d.ts +4 -6
- package/dist/routes/computer-use/setup.js +2 -6
- package/dist/routes/computer-use/single-bundle.d.ts +4 -5
- package/dist/routes/computer-use/single-bundle.js +22 -25
- package/dist/routes/computer-use/subject-projection.d.ts +4 -8
- package/dist/routes/computer-use/subject-projection.js +9 -31
- package/dist/routes/computer-use/types.d.ts +4 -41
- package/dist/routes/scripted/scenario.js +28 -12
- package/dist/routes/scripted/session-result.js +6 -3
- package/dist/routes/shared-world/bundle-records.js +2 -8
- package/dist/routes/shared-world/participant-specs.d.ts +0 -2
- package/dist/routes/shared-world/participant-specs.js +1 -20
- package/dist/routes/shared-world/plan.d.ts +3 -0
- package/dist/routes/shared-world/plan.js +23 -3
- package/dist/routes/shared-world/setup.js +2 -2
- package/dist/routes/terminal/ledger.d.ts +9 -9
- package/dist/routes/terminal/ledger.js +63 -40
- package/dist/routes/terminal/live-finish.js +7 -10
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/token-usage.d.ts +1 -2
- package/dist/routes/terminal/token-usage.js +4 -6
- package/dist/routes/terminal/types.d.ts +9 -1
- package/dist/run/bundle-shape.js +6 -1
- package/dist/run/bundle.d.ts +5 -3
- package/dist/run/contained-output.d.ts +53 -5
- package/dist/run/contained-output.js +141 -42
- package/dist/run/detail.d.ts +2 -1
- package/dist/run/detail.js +12 -7
- package/dist/run/escape-sequences.d.ts +11 -0
- package/dist/run/escape-sequences.js +93 -0
- package/dist/run/evidence-files.d.ts +8 -8
- package/dist/run/evidence-files.js +19 -23
- package/dist/run/locate.d.ts +32 -3
- package/dist/run/locate.js +45 -40
- package/dist/run/notes.d.ts +0 -8
- package/dist/run/notes.js +5 -51
- package/dist/run/outcomes.d.ts +24 -0
- package/dist/run/outcomes.js +19 -0
- package/dist/run/paths.d.ts +2 -0
- package/dist/run/paths.js +5 -1
- package/dist/run/preflight-receipts.js +13 -8
- package/dist/run/project.js +7 -4
- package/dist/run/reclaim-outcomes.d.ts +8 -6
- package/dist/run/reclaim-outcomes.js +10 -6
- package/dist/run/reclaim.d.ts +5 -0
- package/dist/run/reclaim.js +43 -16
- package/dist/run/run-clock.d.ts +50 -0
- package/dist/run/run-clock.js +66 -0
- package/dist/run/run.js +5 -2
- package/dist/run/sandbox-ids.d.ts +7 -2
- package/dist/run/sandbox-ids.js +27 -11
- package/dist/run/secrets.d.ts +2 -1
- package/dist/run/secrets.js +18 -22
- package/dist/run/stored-runs.js +14 -7
- package/dist/run/transient-comms-secrets.js +6 -17
- package/dist/run-study.js +22 -13
- package/dist/study/adapter-scorer-loader.js +6 -4
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-types.d.ts +2 -2
- package/dist/study/plan.d.ts +5 -3
- package/dist/study/plan.js +18 -9
- package/dist/study/preflight-probes.js +11 -3
- package/dist/study/preflight.js +9 -5
- package/dist/study/summary.d.ts +8 -2
- package/dist/study/summary.js +20 -22
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +38 -17
- package/dist/substrates/e2b/connection.d.ts +12 -0
- package/dist/substrates/e2b/connection.js +35 -0
- package/dist/substrates/e2b/desktop-executor.d.ts +1 -3
- package/dist/substrates/e2b/desktop-executor.js +42 -9
- package/dist/substrates/e2b/lifetime.d.ts +24 -4
- package/dist/substrates/e2b/lifetime.js +41 -4
- package/dist/substrates/e2b/sandbox.d.ts +4 -3
- package/dist/substrates/e2b/sandbox.js +13 -18
- package/dist/substrates/e2b/sdk.d.ts +20 -9
- package/dist/substrates/local/capacity.d.ts +18 -2
- package/dist/substrates/local/capacity.js +21 -2
- package/dist/substrates/local/runtime-host.js +21 -14
- package/dist/substrates/local/runtime.js +3 -11
- package/dist/tui/contract.d.ts +30 -5
- package/dist/tui/contract.js +4 -1
- package/dist/tui-app.js +149 -147
- package/dist/verify/artifacts.d.ts +9 -2
- package/dist/verify/artifacts.js +37 -13
- package/dist/verify/liveness.js +19 -5
- package/dist/verify/verify.js +54 -27
- package/docs/architecture/guest-desktop.md +7 -2
- package/docs/architecture/state-driven-executor.md +4 -2
- package/docs/contracts/run-bundle.md +18 -10
- package/docs/contracts/schemas.md +27 -18
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
package/dist/run-study.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// which runs the route's local checks that need no scorer (keys, subject env, the local agent) and
|
|
5
5
|
// returns the route's run. prepareStudy is the same path in two steps, so the CLI can present either
|
|
6
6
|
// refusal before it loads a declared review scorer.
|
|
7
|
-
import { participantList } from "./study/study-fields.js";
|
|
8
7
|
import { personaBackgroundWarnings } from "./study/warnings.js";
|
|
9
8
|
import { resolveCommittedPersonasForCwd, studyPersonaIds } from "./study/persona-resolve.js";
|
|
10
9
|
import { computerUseInput, scriptedInput, sharedWorldInput, terminalInput, } from "./study/route-inputs.js";
|
|
@@ -73,8 +72,12 @@ export async function prepareStudy(config, options, deps = {}) {
|
|
|
73
72
|
outcome.result.warnings.push(...normalized.warnings);
|
|
74
73
|
return { ok: false, outcome };
|
|
75
74
|
}
|
|
76
|
-
|
|
77
|
-
const
|
|
75
|
+
// The run records the background warnings in its bundle with the plan's own.
|
|
76
|
+
const backgroundWarnings = await planBackgroundWarnings(planned.planned.plan, study, options.cwd);
|
|
77
|
+
const plan = {
|
|
78
|
+
...planned.planned.plan,
|
|
79
|
+
warnings: [...(planned.planned.plan.warnings ?? []), ...backgroundWarnings],
|
|
80
|
+
};
|
|
78
81
|
const admitted = await admitPlan(study, planning, plan, deps, normalized.emit);
|
|
79
82
|
if (!admitted.ok) {
|
|
80
83
|
await vm?.close();
|
|
@@ -97,16 +100,7 @@ export async function prepareStudy(config, options, deps = {}) {
|
|
|
97
100
|
return outcome;
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
|
-
//
|
|
101
|
-
const personas = route === "computer-use" || route === "shared-world"
|
|
102
|
-
? (await resolveCommittedPersonasForCwd(options.cwd, studyPersonaIds(study))).personas
|
|
103
|
-
: new Map();
|
|
104
|
-
const backgroundWarnings = personaBackgroundWarnings(route === "computer-use" &&
|
|
105
|
-
options.count !== undefined &&
|
|
106
|
-
participantList(study) === undefined
|
|
107
|
-
? { ...study, participants: options.count }
|
|
108
|
-
: study, personas);
|
|
109
|
-
runWarnings.splice(0, runWarnings.length, ...(planned.planned.plan.warnings ?? []), ...backgroundWarnings);
|
|
103
|
+
// A run's result lists the background warnings; a refusal before the run does not.
|
|
110
104
|
normalized.warnings.push(...backgroundWarnings);
|
|
111
105
|
// The route layers the scorer over the inputs it admitted, and the local study is not
|
|
112
106
|
// rebuilt for it. The admitted runSession, provider and participant desktop belong to this
|
|
@@ -125,6 +119,21 @@ export async function prepareStudy(config, options, deps = {}) {
|
|
|
125
119
|
},
|
|
126
120
|
};
|
|
127
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* A warning for each of the plan's participants whose persona has no background. Only
|
|
124
|
+
* computer-use and shared-world plans have participants; the committed personas are read for them.
|
|
125
|
+
*/
|
|
126
|
+
async function planBackgroundWarnings(plan, study, cwd) {
|
|
127
|
+
const participants = plan.route === "computer-use"
|
|
128
|
+
? plan.runner.participants
|
|
129
|
+
: plan.route === "shared-world"
|
|
130
|
+
? plan.plane.participants
|
|
131
|
+
: [];
|
|
132
|
+
if (participants.length === 0)
|
|
133
|
+
return [];
|
|
134
|
+
const { personas } = await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(study));
|
|
135
|
+
return personaBackgroundWarnings(plan.studyId, participants, personas);
|
|
136
|
+
}
|
|
128
137
|
/** The route's admit function for a plan, with the route's input from the run's options. */
|
|
129
138
|
async function admitPlan(config, options, plan, deps, emit) {
|
|
130
139
|
switch (plan.route) {
|
|
@@ -18,7 +18,7 @@ import { realpath } from "node:fs/promises";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
20
|
import { digestText, redactText } from "../evidence/redaction.js";
|
|
21
|
-
import { prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
21
|
+
import { prepareSelectedOutputDirectory, readContainedRegularFile, refusalText, PROJECT_FILE_MAX_BYTES, } from "../run/contained-output.js";
|
|
22
22
|
/**
|
|
23
23
|
* A scorer written for computer use and shared world, as `RunStudyOptions.scorer` takes it. Those
|
|
24
24
|
* routes pass the browser context; another route that calls it passes a context it was not written
|
|
@@ -96,11 +96,13 @@ export async function loadAdapterScorer(args) {
|
|
|
96
96
|
const relPosix = relative.split(path.sep).join("/");
|
|
97
97
|
// Fail-closed containment gate on the entry file only: rejects symlink, nlink>1, realpath-escape,
|
|
98
98
|
// TOCTOU. Its returned bytes are the digest input.
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
99
|
+
const read = await readContainedRegularFile(root, relPosix, PROJECT_FILE_MAX_BYTES);
|
|
100
|
+
if (read.status === "refused" && read.reason === "too-large")
|
|
101
|
+
return fail("HUMANISH_STUDY_SCORER_NOT_FOUND", `review.scorer.ref "${trimmed}" could not be read: ${refusalText(relPosix, read)}.`);
|
|
102
|
+
if (read.status !== "read") {
|
|
101
103
|
return fail("HUMANISH_STUDY_SCORER_NOT_FOUND", `review.scorer.ref "${trimmed}" could not be read as a regular file inside the project (${relPosix}). Use a plain file there: not a symlink or a hard link, and not a path that resolves outside the project.`);
|
|
102
104
|
}
|
|
103
|
-
const digest = digestText(bytes.toString("utf8"));
|
|
105
|
+
const digest = digestText(read.bytes.toString("utf8"));
|
|
104
106
|
// Import the entry module in a broad try/catch (ERR_MODULE_NOT_FOUND / SyntaxError / ERR_REQUIRE_ESM
|
|
105
107
|
// / top-level throw). import() executes the transitive graph, and the cwd clamp guards only the entry
|
|
106
108
|
// file; out-of-tree/transitive code is acceptable only because of the trust boundary above.
|
package/dist/study/discover.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { personaBackgroundWarnings } from "./warnings.js";
|
|
1
|
+
import { personaBackgroundWarnings, plannedParticipants } from "./warnings.js";
|
|
2
2
|
import { studyPersonaIds, resolveCommittedPersonasForCwd } from "./persona-resolve.js";
|
|
3
3
|
import { personaBrief, PersonaConfigError } from "./persona.js";
|
|
4
4
|
import { constants } from "node:fs";
|
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { parse } from "yaml";
|
|
8
8
|
import { parseStudy } from "./config.js";
|
|
9
9
|
import { V2_SCHEMA } from "./migrate/v2.js";
|
|
10
|
-
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
10
|
+
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareSelectedOutputDirectory, readContainedRegularFile, refusalText, PROJECT_FILE_MAX_BYTES, } from "../run/contained-output.js";
|
|
11
11
|
import { isNodeError } from "../run/type-guards.js";
|
|
12
12
|
import { studyFileCandidates, STUDY_DIRECTORIES } from "./files.js";
|
|
13
13
|
const STUDY_LIST_SCHEMA = "humanish.study-list.v1";
|
|
@@ -197,7 +197,7 @@ export async function inspectStudyManifest(cwd, study) {
|
|
|
197
197
|
warnings: [
|
|
198
198
|
...resolved.warnings,
|
|
199
199
|
...personaResolution.warnings,
|
|
200
|
-
...personaBackgroundWarnings(resolved.config, personaResolution.personas),
|
|
200
|
+
...personaBackgroundWarnings(resolved.config.id, plannedParticipants(resolved.config), personaResolution.personas),
|
|
201
201
|
],
|
|
202
202
|
};
|
|
203
203
|
}
|
|
@@ -279,14 +279,17 @@ async function readManagedManifest(projectRoot, relativePath) {
|
|
|
279
279
|
if (inspected.status !== "ok") {
|
|
280
280
|
return inspected;
|
|
281
281
|
}
|
|
282
|
-
const contents = await readContainedRegularFile(projectRoot, relativePath.replace(/\\/g, "/"));
|
|
283
|
-
if (
|
|
282
|
+
const contents = await readContainedRegularFile(projectRoot, relativePath.replace(/\\/g, "/"), PROJECT_FILE_MAX_BYTES);
|
|
283
|
+
if (contents.status === "refused" && contents.reason === "too-large")
|
|
284
|
+
return { status: "unsafe", message: `${refusalText("The study file", contents)}.` };
|
|
285
|
+
// Gone or swapped since it was inspected above.
|
|
286
|
+
if (contents.status !== "read") {
|
|
284
287
|
return {
|
|
285
288
|
status: "unsafe",
|
|
286
289
|
message: "The study file changed or failed containment validation.",
|
|
287
290
|
};
|
|
288
291
|
}
|
|
289
|
-
return { status: "ok", contents: contents.toString("utf8") };
|
|
292
|
+
return { status: "ok", contents: contents.bytes.toString("utf8") };
|
|
290
293
|
}
|
|
291
294
|
async function readExplicitManifest(projectRoot, requestedPath) {
|
|
292
295
|
try {
|
package/dist/study/init-paths.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { lstat } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { runtimeDirectories, starterFiles } from "./init-templates.js";
|
|
7
|
-
import { assertPreparedSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
7
|
+
import { assertPreparedSelectedOutputDirectory, ContainedReadRefusedError, readContainedRegularFile, PROJECT_FILE_MAX_BYTES, } from "../run/contained-output.js";
|
|
8
8
|
import { isPathInside } from "../run/paths.js";
|
|
9
9
|
import { isNodeError } from "../run/type-guards.js";
|
|
10
10
|
export async function validateInitProjectPaths(cwd) {
|
|
@@ -54,10 +54,12 @@ function unsafeProjectPath(relativePath) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
export async function readTextIfExists(projectRoot, relativePath) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
return bytes.toString("utf8");
|
|
60
|
-
|
|
57
|
+
const read = await readContainedRegularFile(projectRoot, relativePath, PROJECT_FILE_MAX_BYTES);
|
|
58
|
+
if (read.status === "read")
|
|
59
|
+
return read.bytes.toString("utf8");
|
|
60
|
+
// A file over the limit is a size problem, not an unsafe path.
|
|
61
|
+
if (read.status === "refused" && read.reason === "too-large")
|
|
62
|
+
throw new ContainedReadRefusedError(relativePath, read);
|
|
61
63
|
const target = path.join(projectRoot.physicalPath, relativePath);
|
|
62
64
|
try {
|
|
63
65
|
await lstat(target);
|
|
@@ -5,8 +5,8 @@ import type { StudyConfig } from "./types.js";
|
|
|
5
5
|
export declare function personaTitleFromId(personaId: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* Resolve one persona from `humanish/personas/`, then the ignored `.humanish/local/personas/`.
|
|
8
|
-
* Returns `null` with a warning when the id is unsafe or no file exists. Invalid rich backgrounds
|
|
9
|
-
* reject the study before execution.
|
|
8
|
+
* Returns `null` with a warning when the id is unsafe or no file exists. Invalid rich backgrounds,
|
|
9
|
+
* and a persona file that is there and cannot be read, reject the study before execution.
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveCommittedPersona(projectRoot: PreparedSelectedOutputDirectory, personaId: string): Promise<{
|
|
12
12
|
persona: ResolvedPersona | null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseResolvedPersona, PersonaConfigError } from "./persona.js";
|
|
5
|
-
import { prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
5
|
+
import { prepareSelectedOutputDirectory, readContainedRegularFile, PROJECT_FILE_MAX_BYTES, refusalText, } from "../run/contained-output.js";
|
|
6
6
|
import { digestText, redactText } from "../evidence/redaction.js";
|
|
7
7
|
import { realpath } from "node:fs/promises";
|
|
8
8
|
import { participantList } from "./study-fields.js";
|
|
@@ -23,8 +23,8 @@ const PERSONA_DIRECTORIES = [
|
|
|
23
23
|
];
|
|
24
24
|
/**
|
|
25
25
|
* Resolve one persona from `humanish/personas/`, then the ignored `.humanish/local/personas/`.
|
|
26
|
-
* Returns `null` with a warning when the id is unsafe or no file exists. Invalid rich backgrounds
|
|
27
|
-
* reject the study before execution.
|
|
26
|
+
* Returns `null` with a warning when the id is unsafe or no file exists. Invalid rich backgrounds,
|
|
27
|
+
* and a persona file that is there and cannot be read, reject the study before execution.
|
|
28
28
|
*/
|
|
29
29
|
export async function resolveCommittedPersona(projectRoot, personaId) {
|
|
30
30
|
if (!PERSONA_ID_PATTERN.test(personaId)) {
|
|
@@ -37,9 +37,13 @@ export async function resolveCommittedPersona(projectRoot, personaId) {
|
|
|
37
37
|
path.posix.join(directory, `${personaId}.yaml`),
|
|
38
38
|
path.posix.join(directory, `${personaId}.yml`),
|
|
39
39
|
])) {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
40
|
+
const read = await readContainedRegularFile(projectRoot, candidate, PROJECT_FILE_MAX_BYTES);
|
|
41
|
+
if (read.status === "missing")
|
|
42
42
|
continue;
|
|
43
|
+
// A candidate that is there and refused stops resolution: the next file is another persona.
|
|
44
|
+
if (read.status === "refused")
|
|
45
|
+
throw new PersonaConfigError(`Persona ${redactText(personaId)}: ${refusalText(candidate, read)}. Fix or remove that file; a lower-priority persona file is not used in its place.`);
|
|
46
|
+
const bytes = read.bytes;
|
|
43
47
|
let raw;
|
|
44
48
|
try {
|
|
45
49
|
raw = parseYaml(bytes.toString("utf8"));
|
|
@@ -196,8 +196,8 @@ export interface SharedWorldPlan extends PlanBase {
|
|
|
196
196
|
readonly plane: SharedWorldPlane;
|
|
197
197
|
/** At least 2. */
|
|
198
198
|
readonly concurrency: number;
|
|
199
|
-
/**
|
|
200
|
-
readonly sessionTimeoutMs
|
|
199
|
+
/** Each participant's session budget: execution.timeoutMs, else the plane's derived default. */
|
|
200
|
+
readonly sessionTimeoutMs: number;
|
|
201
201
|
readonly brain: Extract<Brain, {
|
|
202
202
|
kind: "openai" | "local-agent";
|
|
203
203
|
}>;
|
package/dist/study/plan.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export { routeOf, type StudyRoute } from "./routing.js";
|
|
|
6
6
|
/** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
|
|
7
7
|
export declare function resolveStudyDryRun(config: StudyConfig, override: boolean | undefined, fallback: boolean | undefined): boolean | undefined;
|
|
8
8
|
/**
|
|
9
|
-
* The plan a study runs under, built without reading files
|
|
10
|
-
*
|
|
11
|
-
* route's
|
|
9
|
+
* The plan a study runs under, built without reading files or the network. From the environment
|
|
10
|
+
* (options.env, else process.env, as the routes read it) it reads one value, the E2B sandbox
|
|
11
|
+
* ceiling, which no study file can know. Each route's planner makes every refusal that route
|
|
12
|
+
* makes, in the route's order and with its codes and messages; the route's exported runner calls
|
|
13
|
+
* the same planner.
|
|
12
14
|
*/
|
|
13
15
|
export declare function planStudy(config: StudyConfig, options: InternalRunStudyOptions, deps?: StudyDeps): PlanResult;
|
package/dist/study/plan.js
CHANGED
|
@@ -6,13 +6,14 @@ import { callerDrivingOf, planComputerUseStudy } from "../routes/computer-use/pl
|
|
|
6
6
|
import { injectedBrowser, planScriptedStudy } from "../routes/scripted/plan.js";
|
|
7
7
|
import { planSharedWorldStudy } from "../routes/shared-world/plan.js";
|
|
8
8
|
import { planTerminalStudy } from "../routes/terminal/plan.js";
|
|
9
|
+
import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
|
|
9
10
|
import { localBrowserDefaults } from "../substrates/local/runtime-config.js";
|
|
10
11
|
import { THIS_REPO_DRY_RUN_ONLY } from "./composition-rules.js";
|
|
11
12
|
import { planBase } from "./plan-base.js";
|
|
12
13
|
import { routeOf } from "./routing.js";
|
|
13
14
|
import { automaticAnalysisRouteReason, taskProtocolValidationReason } from "./validation.js";
|
|
14
15
|
import { declaredParticipantCount } from "./study-fields.js";
|
|
15
|
-
import {
|
|
16
|
+
import { recordedStudyWarnings } from "./warnings.js";
|
|
16
17
|
export { routeOf } from "./routing.js";
|
|
17
18
|
/** Resolve dry-run: explicit override wins, else the scenario mode, else the given fallback. */
|
|
18
19
|
export function resolveStudyDryRun(config, override, fallback) {
|
|
@@ -63,20 +64,22 @@ function planPreview(config, options, input) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
/**
|
|
66
|
-
* The plan a study runs under, built without reading files
|
|
67
|
-
*
|
|
68
|
-
* route's
|
|
67
|
+
* The plan a study runs under, built without reading files or the network. From the environment
|
|
68
|
+
* (options.env, else process.env, as the routes read it) it reads one value, the E2B sandbox
|
|
69
|
+
* ceiling, which no study file can know. Each route's planner makes every refusal that route
|
|
70
|
+
* makes, in the route's order and with its codes and messages; the route's exported runner calls
|
|
71
|
+
* the same planner.
|
|
69
72
|
*/
|
|
70
73
|
export function planStudy(config, options, deps = {}) {
|
|
71
74
|
const study = localBrowserDefaults(config);
|
|
72
75
|
const input = { dryRun: resolveStudyDryRun(study, options.dryRun, true) ?? true };
|
|
73
|
-
const
|
|
76
|
+
const ceiling = sandboxCeiling(options.env ?? process.env);
|
|
77
|
+
const result = planRoute(routeOf(config), study, options, input, deps, ceiling);
|
|
74
78
|
if (!result.ok)
|
|
75
79
|
return result;
|
|
76
80
|
// The manifest the CLI resolved enters the plan here and nowhere else; the routes read
|
|
77
81
|
// plan.study for the run's status record and bundle, and plan.warnings for its bundle events.
|
|
78
|
-
const
|
|
79
|
-
const warnings = [...(warning === undefined ? [] : [warning]), ...scriptedMissionWarnings(study)];
|
|
82
|
+
const warnings = recordedStudyWarnings(study);
|
|
80
83
|
const plan = {
|
|
81
84
|
...result.plan,
|
|
82
85
|
...(options.study === undefined ? {} : { study: options.study }),
|
|
@@ -84,7 +87,7 @@ export function planStudy(config, options, deps = {}) {
|
|
|
84
87
|
};
|
|
85
88
|
return { ok: true, planned: { plan } };
|
|
86
89
|
}
|
|
87
|
-
function planRoute(route, study, options, input, deps) {
|
|
90
|
+
function planRoute(route, study, options, input, deps, sandboxCeiling) {
|
|
88
91
|
switch (route) {
|
|
89
92
|
case "preview":
|
|
90
93
|
return planPreview(study, options, input);
|
|
@@ -95,14 +98,20 @@ function planRoute(route, study, options, input, deps) {
|
|
|
95
98
|
driving: callerDrivingOf(options),
|
|
96
99
|
...(options.count === undefined ? {} : { countOverride: options.count }),
|
|
97
100
|
...(options.rerun === undefined ? {} : { rerun: options.rerun }),
|
|
101
|
+
sandboxCeiling,
|
|
98
102
|
});
|
|
99
103
|
case "shared-world":
|
|
100
104
|
return planSharedWorldStudy(study, {
|
|
101
105
|
...input,
|
|
102
106
|
hasRunSession: deps.runSession !== undefined,
|
|
107
|
+
sandboxCeiling,
|
|
103
108
|
});
|
|
104
109
|
case "terminal":
|
|
105
|
-
return planTerminalStudy(study, {
|
|
110
|
+
return planTerminalStudy(study, {
|
|
111
|
+
...input,
|
|
112
|
+
hasCostProbe: deps.costProbe !== undefined,
|
|
113
|
+
sandboxCeiling,
|
|
114
|
+
});
|
|
106
115
|
case "scripted":
|
|
107
116
|
return planScriptedStudy(study, { ...input, injectedBrowser: injectedBrowser(deps) });
|
|
108
117
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// the study and dispatches here.
|
|
5
5
|
import { cloneProvisioningBudgetMs, provisionCloneSubject } from "../subject/clone.js";
|
|
6
6
|
import { CUA_ACTOR_STUDY_PROVIDER_METADATA } from "../routes/computer-use/e2b-desktop/prepare.js";
|
|
7
|
-
import {
|
|
7
|
+
import { sandboxCeiling } from "../substrates/e2b/lifetime.js";
|
|
8
8
|
import { abandonPreflightJournal, discardPreflightJournal, openPreflightJournal, } from "../run/preflight-receipts.js";
|
|
9
9
|
import { probeUrl } from "../substrates/detached.js";
|
|
10
10
|
import { loadE2BDesktopModule } from "../substrates/e2b/sdk.js";
|
|
@@ -147,8 +147,16 @@ async function withPreflightSandbox(ctx, args, callback) {
|
|
|
147
147
|
let allocation;
|
|
148
148
|
let failureMessage;
|
|
149
149
|
// The lease is sized to the probe's work, never longer than a declared sandbox timeout (the run
|
|
150
|
-
// gets no more than that either) or
|
|
151
|
-
const
|
|
150
|
+
// gets no more than that either) or the sandbox ceiling.
|
|
151
|
+
const ceiling = sandboxCeiling(ctx.env);
|
|
152
|
+
if (!ceiling.ok)
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
result: fail(ctx, "HUMANISH_STUDY_PREFLIGHT_PROVISION_FAILED", ceiling.message, [
|
|
156
|
+
{ name: "sandbox preflight", ok: false, message: ceiling.message },
|
|
157
|
+
]),
|
|
158
|
+
};
|
|
159
|
+
const timeoutMs = Math.min(args.leaseMs, ctx.config.execution?.desktop?.sandboxTimeoutMs ?? ceiling.ms);
|
|
152
160
|
// The receipt goes to a journal under .humanish/preflight, so `humanish reclaim --preflight`
|
|
153
161
|
// can kill the probe if this process dies before the finally block does.
|
|
154
162
|
let journal;
|
package/dist/study/preflight.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { personaBackgroundWarnings } from "./warnings.js";
|
|
1
|
+
import { personaBackgroundWarnings, plannedParticipants } from "./warnings.js";
|
|
2
2
|
import { resolveCommittedPersonasForCwd, studyPersonaIds } from "./persona-resolve.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
@@ -37,9 +37,10 @@ export async function runStudyPreflight(options) {
|
|
|
37
37
|
error: resolved.error,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const participants = plannedParticipants(resolved.config);
|
|
41
|
+
const personas = participants.length === 0
|
|
42
|
+
? new Map()
|
|
43
|
+
: (await resolveCommittedPersonasForCwd(cwd, studyPersonaIds(resolved.config))).personas;
|
|
43
44
|
const route = routeOf(resolved.config);
|
|
44
45
|
const ctx = {
|
|
45
46
|
cwd,
|
|
@@ -59,7 +60,10 @@ export async function runStudyPreflight(options) {
|
|
|
59
60
|
],
|
|
60
61
|
targets: collectTargets(resolved.config),
|
|
61
62
|
sandbox: { created: false },
|
|
62
|
-
warnings: [
|
|
63
|
+
warnings: [
|
|
64
|
+
...resolved.warnings,
|
|
65
|
+
...personaBackgroundWarnings(resolved.config.id, participants, personas),
|
|
66
|
+
],
|
|
63
67
|
};
|
|
64
68
|
const machine = machineCheck(ctx.config);
|
|
65
69
|
// A local study's app and desktops run on this machine, so the hosted probes do not apply.
|
package/dist/study/summary.d.ts
CHANGED
|
@@ -38,9 +38,15 @@ export interface StudySummary {
|
|
|
38
38
|
* default. A study variable you cannot see is one nobody chose.
|
|
39
39
|
*/
|
|
40
40
|
reasoningEffort?: string;
|
|
41
|
-
caps
|
|
41
|
+
/** Absent on routes whose dollar caps the summary does not show; `{}` when none is declared. */
|
|
42
|
+
caps?: StudyCaps;
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* What `humanish run <study>` runs this file as: its `mode`, a dry run when it sets none. The TUI
|
|
45
|
+
* cannot start a live run of a dry-run file, because no `run` flag overrides the file to live.
|
|
46
|
+
*/
|
|
47
|
+
mode: "live" | "dry-run";
|
|
48
|
+
/**
|
|
49
|
+
* Whether the keys a live run of this study needs resolve, whatever its mode. This checks key
|
|
44
50
|
* presence, not local CLI authentication or provider validity. Undefined when not checked.
|
|
45
51
|
*/
|
|
46
52
|
keysReady?: boolean;
|
package/dist/study/summary.js
CHANGED
|
@@ -7,10 +7,9 @@ import { localRuntimeStatus } from "../substrates/local/runtime.js";
|
|
|
7
7
|
// what a stakeholder reads on the screen where they decide whether to press Start.
|
|
8
8
|
//
|
|
9
9
|
// Resolved analysis defaults are shown independently of declared participant caps. A cap that is not
|
|
10
|
-
// declared is not "unlimited" and not "$0"
|
|
11
|
-
import { resolveStudyDryRun } from "./plan.js";
|
|
12
|
-
import {
|
|
13
|
-
import { localCodexParticipantCheck, planCliRun } from "./doctor.js";
|
|
10
|
+
// declared is not "unlimited" and not "$0": the screen says that no cap is set.
|
|
11
|
+
import { planStudy, resolveStudyDryRun, routeOf } from "./plan.js";
|
|
12
|
+
import { localCodexParticipantCheck } from "./doctor.js";
|
|
14
13
|
import { requiredKeys, requiredSubjectEnv } from "./requirements.js";
|
|
15
14
|
import { automaticAnalysisBudget, formatAutomaticAnalysisBudget, } from "../analysis/automatic-config.js";
|
|
16
15
|
import { DEFAULT_OPENAI_CU_MODEL, DEFAULT_OPENAI_CU_REASONING_EFFORT, } from "../actors/computer-use/openai-provider.js";
|
|
@@ -70,7 +69,7 @@ function participantsOf(config) {
|
|
|
70
69
|
/** The computer-use caps. Other routes draw none. */
|
|
71
70
|
function summaryCapsOf(config) {
|
|
72
71
|
if (!isComputerUseComposition(config))
|
|
73
|
-
return
|
|
72
|
+
return undefined;
|
|
74
73
|
const caps = config.caps;
|
|
75
74
|
return {
|
|
76
75
|
...(typeof caps?.maxUsd === "number" ? { laneUsd: caps.maxUsd } : {}),
|
|
@@ -89,18 +88,17 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
89
88
|
const route = routeOf(inspected.config);
|
|
90
89
|
let keysReady;
|
|
91
90
|
let missingKeys;
|
|
92
|
-
const
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
91
|
+
const mode = resolveStudyDryRun(inspected.config, undefined, true) === true ? "dry-run" : "live";
|
|
92
|
+
// The keys are the live plan's, whatever the file's mode: the screen offers a live run of every
|
|
93
|
+
// study, and a dry run needs no keys to report. A study the planner refuses live has none to
|
|
94
|
+
// check, so the summary reports the refusal in place of its keys.
|
|
95
|
+
const planned = options.checkKeys === true ? planStudy(inspected.config, { cwd, dryRun: false }) : undefined;
|
|
96
96
|
const planRefusal = planned?.ok === false ? planned.refusal.message : undefined;
|
|
97
|
-
if (
|
|
98
|
-
const requirements = planned
|
|
97
|
+
if (planned?.ok === true) {
|
|
98
|
+
const requirements = planned.planned.plan.requirements;
|
|
99
99
|
const subjectKeys = requiredSubjectEnv(requirements);
|
|
100
100
|
const email = inspected.config.comms?.email;
|
|
101
|
-
const receivingKey =
|
|
102
|
-
? await receivingRequiredKey(cwd, email.connection)
|
|
103
|
-
: undefined;
|
|
101
|
+
const receivingKey = email?.kind === "real" ? await receivingRequiredKey(cwd, email.connection) : undefined;
|
|
104
102
|
const candidates = [
|
|
105
103
|
"OPENAI_API_KEY",
|
|
106
104
|
"CODEX_API_KEY",
|
|
@@ -108,13 +106,11 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
108
106
|
...subjectKeys,
|
|
109
107
|
...(receivingKey ? [receivingKey] : []),
|
|
110
108
|
];
|
|
111
|
-
const probes =
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
|
|
117
|
-
}).catch(() => []);
|
|
109
|
+
const probes = await probeKeySources(candidates, {
|
|
110
|
+
cwd,
|
|
111
|
+
env: options.env ?? process.env,
|
|
112
|
+
...(options.keyDeps === undefined ? {} : { deps: options.keyDeps }),
|
|
113
|
+
}).catch(() => []);
|
|
118
114
|
const present = new Set(probes.filter((probe) => probe.source !== null).map((probe) => probe.name));
|
|
119
115
|
const required = requiredKeys(requirements, (name) => present.has(name));
|
|
120
116
|
const missing = [
|
|
@@ -130,6 +126,7 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
130
126
|
const analysis = automaticAnalysisBudget(inspected.config.review?.analysis, route, computerUseParticipantCount(inspected.config));
|
|
131
127
|
const subject = subjectOf(config);
|
|
132
128
|
const participants = participantsOf(inspected.config);
|
|
129
|
+
const caps = summaryCapsOf(inspected.config);
|
|
133
130
|
const runtime = options.checkKeys === true && isLocalBrowserStudy(inspected.config)
|
|
134
131
|
? await localRuntimeStatus({
|
|
135
132
|
...(options.env ? { env: options.env } : {}),
|
|
@@ -170,7 +167,8 @@ export async function readStudySummary(cwd, study, options = {}) {
|
|
|
170
167
|
...(participants === undefined ? {} : { participants }),
|
|
171
168
|
model: inspected.config.actor?.model ?? DEFAULT_OPENAI_CU_MODEL,
|
|
172
169
|
reasoningEffort: reasoningEffortOf(inspected.config),
|
|
173
|
-
caps:
|
|
170
|
+
...(caps === undefined ? {} : { caps }),
|
|
171
|
+
mode,
|
|
174
172
|
...(keysReady === undefined ? {} : { keysReady }),
|
|
175
173
|
...(missingKeys === undefined ? {} : { missingKeys }),
|
|
176
174
|
...(planRefusal === undefined ? {} : { planRefusal }),
|
package/dist/study/warnings.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedPersona } from "./persona.js";
|
|
2
2
|
import type { StudyConfig } from "./types.js";
|
|
3
|
+
import { type Participant } from "./plan-participants.js";
|
|
3
4
|
/**
|
|
4
5
|
* The fields a config sets that its route does not read, each as `field` or `field (reason)`.
|
|
5
6
|
* parseStudy refuses a study for any of them.
|
|
@@ -10,17 +11,23 @@ export declare function inertFieldLabels(config: StudyConfig): string[];
|
|
|
10
11
|
* `participants[].entry` or `execution.timeoutMs`. `humanish migrate` drops each one.
|
|
11
12
|
*/
|
|
12
13
|
export declare function inertFieldPaths(config: StudyConfig): string[];
|
|
14
|
+
export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
|
|
13
15
|
/**
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
|
|
19
|
-
* route.
|
|
16
|
+
* The warnings about a study's own fields that a run records in its bundle as `study.warning`
|
|
17
|
+
* events: `execution.egressAllow` off the terminal route, and participant text that reads like a
|
|
18
|
+
* script. parseStudy reports them too, after its other warnings, so `study check` and every run
|
|
19
|
+
* print them.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
export declare function recordedStudyWarnings(config: StudyConfig): string[];
|
|
22
|
+
/**
|
|
23
|
+
* A warning for each participant whose persona has no background, which gives a participant a
|
|
24
|
+
* reason to act beyond the assigned task. `participants` are the records the planner builds: a
|
|
25
|
+
* computer-use or shared-world plan's participants, or `plannedParticipants` before a plan exists.
|
|
26
|
+
*/
|
|
27
|
+
export declare function personaBackgroundWarnings(studyId: string, participants: readonly Pick<Participant, "id" | "personaId">[], personas: ReadonlyMap<string, ResolvedPersona>): string[];
|
|
28
|
+
/**
|
|
29
|
+
* The participant records planStudy builds for a study's declared participants, for `study check`
|
|
30
|
+
* and `study show`, which run no plan. Only computer-use and shared-world studies have them; a run's
|
|
31
|
+
* `--count` is not applied.
|
|
32
|
+
*/
|
|
33
|
+
export declare function plannedParticipants(config: StudyConfig): readonly Participant[];
|