humanish 0.115.0 → 0.117.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 +41 -0
- package/README.md +7 -6
- package/TELEMETRY.md +6 -0
- package/dist/actors/codex/app-server-ui.js +11 -4
- package/dist/actors/codex/restricted-participant-policy.d.ts +0 -2
- package/dist/actors/codex/restricted-participant-policy.js +4 -32
- package/dist/actors/codex/restricted-participant.d.ts +5 -0
- package/dist/actors/codex/restricted-participant.js +3 -2
- package/dist/actors/computer-use/actor.d.ts +4 -0
- package/dist/actors/computer-use/actor.js +2 -0
- package/dist/actors/computer-use/loop/actions.d.ts +4 -4
- package/dist/actors/computer-use/loop/actions.js +25 -18
- package/dist/actors/computer-use/loop/debrief.js +61 -27
- package/dist/actors/computer-use/loop/session.d.ts +3 -0
- package/dist/actors/computer-use/loop/session.js +9 -1
- package/dist/actors/computer-use/loop/trace.js +1 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -14
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/wait.d.ts +74 -0
- package/dist/actors/computer-use/wait.js +100 -0
- package/dist/actors/contract.d.ts +14 -0
- 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/browser-control/protocol.d.ts +86 -6
- package/dist/browser-control/protocol.js +13 -6
- 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-executor.js +3 -1
- 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 +5 -5
- 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/e2b-desktop/prepare.js +40 -31
- 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/local-vm.js +5 -2
- package/dist/routes/computer-use/participant-execution.d.ts +3 -2
- package/dist/routes/computer-use/participant-model.js +9 -0
- 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 +18 -13
- 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 +5 -42
- 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 +25 -4
- 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/live-sandbox.d.ts +1 -1
- package/dist/routes/terminal/live-sandbox.js +14 -11
- package/dist/routes/terminal/plan.d.ts +3 -0
- package/dist/routes/terminal/plan.js +8 -5
- package/dist/routes/terminal/sandbox.d.ts +4 -3
- package/dist/routes/terminal/sandbox.js +5 -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/config.js +4 -1
- package/dist/study/discover.js +9 -6
- package/dist/study/init-paths.js +7 -5
- package/dist/study/keys.d.ts +2 -0
- package/dist/study/keys.js +2 -0
- package/dist/study/migrate/v2.js +2 -1
- package/dist/study/parse/actors.js +8 -0
- package/dist/study/persona-resolve.d.ts +2 -2
- package/dist/study/persona-resolve.js +9 -5
- package/dist/study/plan-participants.d.ts +4 -0
- package/dist/study/plan-participants.js +4 -0
- 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/types.d.ts +14 -0
- package/dist/study/validation.d.ts +5 -0
- package/dist/study/validation.js +8 -0
- package/dist/study/warnings.d.ts +19 -12
- package/dist/study/warnings.js +50 -17
- package/dist/subject/serve.d.ts +3 -0
- package/dist/subject/serve.js +4 -1
- package/dist/substrates/detached.js +14 -7
- 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 -5
- package/dist/substrates/e2b/desktop-executor.js +45 -12
- 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 +150 -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/browser-control.md +10 -4
- 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 +46 -29
- package/docs/contracts/study-analysis.md +10 -1
- package/docs/product/automatic-analysis.md +7 -1
- package/package.json +2 -2
|
@@ -24,7 +24,10 @@ export declare function inventorySandboxIds(files: readonly {
|
|
|
24
24
|
export declare function holdsKeyedSandboxId(file: string, text: string): boolean;
|
|
25
25
|
/**
|
|
26
26
|
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
27
|
-
* failed;
|
|
27
|
+
* failed; none from the journal when it is missing or malformed, or when a folder is where it goes
|
|
28
|
+
* (a failed append can leave one, and the process still holds those ids). A journal refused for
|
|
29
|
+
* any other reason, a link or one too large to read say, throws ContainedReadRefusedError: every
|
|
30
|
+
* caller would otherwise write or grade a run's files without the ids it may hold.
|
|
28
31
|
*/
|
|
29
32
|
export declare function readRunSandboxIds(root: PreparedOutputRoot): Promise<string[]>;
|
|
30
33
|
/**
|
|
@@ -41,7 +44,9 @@ export declare function scrubSandboxIdBytes(file: string, bytes: Buffer, ids: re
|
|
|
41
44
|
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
42
45
|
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
43
46
|
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
44
|
-
* Best effort: verify grades a file this misses.
|
|
47
|
+
* Best effort for writes and listings: verify grades a file this misses. A text file it refuses
|
|
48
|
+
* to read, or a journal it refuses, throws once the sweep ends, naming each, since such a file
|
|
49
|
+
* may still hold an id.
|
|
45
50
|
*/
|
|
46
51
|
export declare function scrubRunSandboxIds(paths: PreparedRunArtifactPaths): Promise<void>;
|
|
47
52
|
/**
|
package/dist/run/sandbox-ids.js
CHANGED
|
@@ -6,7 +6,7 @@ import { opendir, realpath } from "node:fs/promises";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
8
8
|
import { collectSandboxIds, redactSandboxIds, sandboxIdDigest, REDACTED_SANDBOX_ID, } from "../evidence/redaction.js";
|
|
9
|
-
import { readContainedRegularFile, writeContainedOutputFile, } from "./contained-output.js";
|
|
9
|
+
import { ContainedReadRefusedError, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, writeContainedOutputFile, } from "./contained-output.js";
|
|
10
10
|
import { bindExistingRunArtifactPaths, isSafeRunIdSegment, validatePreparedRunRootIdentity, } from "./paths.js";
|
|
11
11
|
import { appendedSandboxIds, parseSandboxReceipts, SANDBOX_RECEIPTS_ARTIFACT, } from "./sandbox-receipts.js";
|
|
12
12
|
/** How text names a sandbox: the marker with the id's digest. */
|
|
@@ -96,14 +96,19 @@ export function holdsKeyedSandboxId(file, text) {
|
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* The raw sandbox ids a run's receipts journal, with any this process receipted whose append
|
|
99
|
-
* failed;
|
|
99
|
+
* failed; none from the journal when it is missing or malformed, or when a folder is where it goes
|
|
100
|
+
* (a failed append can leave one, and the process still holds those ids). A journal refused for
|
|
101
|
+
* any other reason, a link or one too large to read say, throws ContainedReadRefusedError: every
|
|
102
|
+
* caller would otherwise write or grade a run's files without the ids it may hold.
|
|
100
103
|
*/
|
|
101
104
|
export async function readRunSandboxIds(root) {
|
|
105
|
+
const read = await readContainedRegularFile(root, SANDBOX_RECEIPTS_ARTIFACT, RUN_ARTIFACT_MAX_BYTES);
|
|
106
|
+
if (read.status === "refused" && read.reason !== "directory")
|
|
107
|
+
throw new ContainedReadRefusedError(SANDBOX_RECEIPTS_ARTIFACT, read);
|
|
102
108
|
let journaled = [];
|
|
103
109
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
journaled = receiptSandboxIds(bytes);
|
|
110
|
+
if (read.status === "read")
|
|
111
|
+
journaled = receiptSandboxIds(read.bytes);
|
|
107
112
|
}
|
|
108
113
|
catch {
|
|
109
114
|
// The ids this process receipted still apply.
|
|
@@ -138,7 +143,9 @@ const MAX_SWEPT_ENTRIES = 10_000;
|
|
|
138
143
|
* Rewrite each file of a finished run that names one of its raw sandbox ids, apart from the
|
|
139
144
|
* receipts, with the label in place of the id. Writers such as a participant's actor.json can
|
|
140
145
|
* quote an SDK error that names the sandbox. Run.finish calls it before analysis reads the run.
|
|
141
|
-
* Best effort: verify grades a file this misses.
|
|
146
|
+
* Best effort for writes and listings: verify grades a file this misses. A text file it refuses
|
|
147
|
+
* to read, or a journal it refuses, throws once the sweep ends, naming each, since such a file
|
|
148
|
+
* may still hold an id.
|
|
142
149
|
*/
|
|
143
150
|
export async function scrubRunSandboxIds(paths) {
|
|
144
151
|
const ids = await readRunSandboxIds(paths);
|
|
@@ -147,12 +154,15 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
147
154
|
const valid = await validatePreparedRunRootIdentity(paths).then(() => true, () => false);
|
|
148
155
|
if (!valid)
|
|
149
156
|
return;
|
|
157
|
+
const refused = [];
|
|
150
158
|
const scrub = async (file) => {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
159
|
+
const read = await readContainedRegularFile(paths, file, RUN_ARTIFACT_MAX_BYTES);
|
|
160
|
+
if (read.status === "refused")
|
|
161
|
+
refused.push(refusalText(file, read));
|
|
162
|
+
if (read.status !== "read")
|
|
153
163
|
return;
|
|
154
|
-
const scrubbed = scrubSandboxIdBytes(file, bytes, ids);
|
|
155
|
-
if (scrubbed !== bytes)
|
|
164
|
+
const scrubbed = scrubSandboxIdBytes(file, read.bytes, ids);
|
|
165
|
+
if (scrubbed !== read.bytes)
|
|
156
166
|
await writeContainedOutputFile(paths, file, scrubbed);
|
|
157
167
|
};
|
|
158
168
|
let seen = 0;
|
|
@@ -179,6 +189,8 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
179
189
|
await walk("").catch(() => {
|
|
180
190
|
// A directory that changed while it was listed ends the sweep; verify names what it missed.
|
|
181
191
|
});
|
|
192
|
+
if (refused.length > 0)
|
|
193
|
+
throw new Error(`The sandbox id sweep could not read every run file, so these may still name a sandbox: ${refused.join("; ")}.`);
|
|
182
194
|
}
|
|
183
195
|
/**
|
|
184
196
|
* A run's result as it may be returned or printed: the same object when it names no sandbox,
|
|
@@ -187,12 +199,16 @@ export async function scrubRunSandboxIds(paths) {
|
|
|
187
199
|
export async function publicRunResult(result, cwd) {
|
|
188
200
|
let ids = [];
|
|
189
201
|
if (typeof result.runId === "string" && isSafeRunIdSegment(result.runId)) {
|
|
202
|
+
let paths;
|
|
190
203
|
try {
|
|
191
|
-
|
|
204
|
+
paths = await bindExistingRunArtifactPaths(cwd, result.runId);
|
|
192
205
|
}
|
|
193
206
|
catch {
|
|
194
207
|
// A run that never created its directory has no receipts; the keys are still redacted.
|
|
195
208
|
}
|
|
209
|
+
// A journal that is there and refused throws: the result cannot be printed without its ids.
|
|
210
|
+
if (paths !== undefined)
|
|
211
|
+
ids = await readRunSandboxIds(paths);
|
|
196
212
|
}
|
|
197
213
|
return publicSandboxView(result, ids);
|
|
198
214
|
}
|
package/dist/run/secrets.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export declare class RunSecrets {
|
|
|
11
11
|
* Replaces every held value with the marker, as written, in each encoded form, and where a URL
|
|
12
12
|
* carries it partly percent-encoded or a terminal escape sequence splits it. It reads the values
|
|
13
13
|
* on each call, so a value added after the scrub was handed to a participant or a subject is
|
|
14
|
-
* scrubbed from then on.
|
|
14
|
+
* scrubbed from then on. If the result still holds a value in any readingsOf, as when a marker
|
|
15
|
+
* written before `x` spells the value `SECRET]x`, the whole text becomes the marker.
|
|
15
16
|
*/
|
|
16
17
|
readonly scrub: (text: string) => string;
|
|
17
18
|
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
package/dist/run/secrets.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// replaced as written and in the encoded forms a URL, a JSON body or a log line gives it;
|
|
5
5
|
// redactText runs after the scrub and finds secrets by shape.
|
|
6
6
|
import { scrubLiterals } from "../evidence/redaction.js";
|
|
7
|
-
import { encodedForms } from "../evidence/secret-scrub.js";
|
|
7
|
+
import { encodedForms, holdsSecretValue } from "../evidence/secret-scrub.js";
|
|
8
|
+
import { escapeSequences } from "./escape-sequences.js";
|
|
8
9
|
export class RunSecrets {
|
|
9
10
|
// Private fields stay out of JSON.stringify and object spread, so a Run that reaches a writer
|
|
10
11
|
// whole cannot carry the values with it.
|
|
@@ -12,11 +13,14 @@ export class RunSecrets {
|
|
|
12
13
|
/** Every held value as written and encoded, longest first. */
|
|
13
14
|
#forms = [];
|
|
14
15
|
#minLength;
|
|
16
|
+
/** holdsSecretValue of the held values, built at its first use after `add` holds a new one. */
|
|
17
|
+
#holds;
|
|
15
18
|
/**
|
|
16
19
|
* Replaces every held value with the marker, as written, in each encoded form, and where a URL
|
|
17
20
|
* carries it partly percent-encoded or a terminal escape sequence splits it. It reads the values
|
|
18
21
|
* on each call, so a value added after the scrub was handed to a participant or a subject is
|
|
19
|
-
* scrubbed from then on.
|
|
22
|
+
* scrubbed from then on. If the result still holds a value in any readingsOf, as when a marker
|
|
23
|
+
* written before `x` spells the value `SECRET]x`, the whole text becomes the marker.
|
|
20
24
|
*/
|
|
21
25
|
scrub;
|
|
22
26
|
/** `seed` keeps its order; values shorter than the floor are dropped. */
|
|
@@ -30,7 +34,9 @@ export class RunSecrets {
|
|
|
30
34
|
const literal = scrubLiterals(this.#forms, marker);
|
|
31
35
|
this.scrub = (text) => {
|
|
32
36
|
const scrubbed = literal(text);
|
|
33
|
-
|
|
37
|
+
const result = replaceSpans(scrubbed, viewSpans(scrubbed, this.#forms), marker);
|
|
38
|
+
this.#holds ??= holdsSecretValue(this.#held);
|
|
39
|
+
return this.#holds(result) ? marker : result;
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
/** Holds each value the run learns while it runs, unless it is under the floor or already held. */
|
|
@@ -39,6 +45,7 @@ export class RunSecrets {
|
|
|
39
45
|
if (value.length < this.#minLength || this.#held.includes(value))
|
|
40
46
|
continue;
|
|
41
47
|
this.#held.push(value);
|
|
48
|
+
this.#holds = undefined;
|
|
42
49
|
for (const form of encodedForms(value))
|
|
43
50
|
if (!this.#forms.includes(form))
|
|
44
51
|
this.#forms.push(form);
|
|
@@ -69,20 +76,9 @@ export class RunSecrets {
|
|
|
69
76
|
return mergeSpans(found);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
// What a terminal draws with (operating-system commands, control sequences and two-byte escapes,
|
|
73
|
-
// as written or JSON-escaped, as a JSON event carries a command's colored output) and what a URL
|
|
74
|
-
// encodes with (runs of percent escapes). The view a value is looked for in drops the first and
|
|
75
|
-
// decodes the second.
|
|
76
|
-
const VIEW_SEQUENCE = new RegExp([
|
|
77
|
-
"\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)",
|
|
78
|
-
"\\x1b\\[[0-?]*[ -/]*[@-~]",
|
|
79
|
-
"\\x1b[78=>]",
|
|
80
|
-
"\\\\u001b\\][^\\\\]*(?:\\\\u0007|\\\\u001b\\\\\\\\)",
|
|
81
|
-
"\\\\u001b\\[[0-?]*[ -/]*[@-~]",
|
|
82
|
-
"(?:%[0-9A-Fa-f]{2})+",
|
|
83
|
-
].join("|"), "g");
|
|
84
79
|
/**
|
|
85
|
-
* Where a value is found in the text's view
|
|
80
|
+
* Where a value is found in the text's view, which drops terminal escape sequences and decodes
|
|
81
|
+
* percent escapes (escapeSequences). A browser encodes a space or a quote in a URL path
|
|
86
82
|
* and leaves a `/` or a `:` as written, so a value with both matches no single encoded form, and a
|
|
87
83
|
* color code inside a value splits it. Each found value maps back to the span of `text` it came
|
|
88
84
|
* from, so the text keeps every other character as written.
|
|
@@ -104,21 +100,21 @@ function viewSpans(text, values) {
|
|
|
104
100
|
};
|
|
105
101
|
let cursor = 0;
|
|
106
102
|
let changed = false;
|
|
107
|
-
for (const
|
|
103
|
+
for (const [start, end] of escapeSequences(text)) {
|
|
108
104
|
let plain = "";
|
|
109
|
-
if (
|
|
105
|
+
if (text[start] === "%") {
|
|
110
106
|
try {
|
|
111
|
-
plain = decodeURIComponent(
|
|
107
|
+
plain = decodeURIComponent(text.slice(start, end));
|
|
112
108
|
}
|
|
113
109
|
catch {
|
|
114
110
|
// Not UTF-8: the run stays as written and is copied with the text after it.
|
|
115
111
|
continue;
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
|
-
copy(cursor,
|
|
119
|
-
cursor =
|
|
114
|
+
copy(cursor, start);
|
|
115
|
+
cursor = end;
|
|
120
116
|
for (let unit = 0; unit < plain.length; unit += 1) {
|
|
121
|
-
starts.push(
|
|
117
|
+
starts.push(start);
|
|
122
118
|
ends.push(cursor);
|
|
123
119
|
}
|
|
124
120
|
view += plain;
|
package/dist/run/stored-runs.js
CHANGED
|
@@ -2,12 +2,12 @@ import { lstat, readdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { redactText } from "../evidence/redaction.js";
|
|
4
4
|
import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, resolveRunsRoot, } from "./paths.js";
|
|
5
|
-
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, } from "./contained-output.js";
|
|
5
|
+
import { assertPreparedSelectedOutputDirectory, bindExistingManagedHumanishOutputDirectory, refusalText, } from "./contained-output.js";
|
|
6
6
|
import { RUN_BUNDLE_FILE } from "./bundle.js";
|
|
7
7
|
import { isReviewSummary, isRunOutcome } from "./bundle-shape.js";
|
|
8
8
|
import { bundleDisplayFacts, displayedReview, runDisplay, } from "./display.js";
|
|
9
9
|
import { RUN_STATUS_FILE } from "./status.js";
|
|
10
|
-
import { readLatest, readRunJsonIfExists, resolveRunPath } from "./locate.js";
|
|
10
|
+
import { readLatest, readRunJsonIfExists, resolveRunPath, runJsonValue } from "./locate.js";
|
|
11
11
|
import { withCuaReviewProvenance } from "./outcomes.js";
|
|
12
12
|
import { isNodeError, isRecord } from "./type-guards.js";
|
|
13
13
|
import { invalidRunStorageVerifyResult, verifyResolvedRun, } from "../verify/verify.js";
|
|
@@ -70,7 +70,8 @@ export async function listRuns(cwdInput) {
|
|
|
70
70
|
if (runsRoot && entryRunPaths.physicalRunsRoot !== runsRoot.physicalPath) {
|
|
71
71
|
return runsUnavailableResult(cwd, new Error("humanish runs root changed physical destination."));
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
// A listing shows what it can read: a run.json it refuses lists like a missing one.
|
|
74
|
+
const bundle = runJsonValue(await readRunJsonIfExists(entryRunPaths, RUN_BUNDLE_FILE));
|
|
74
75
|
runs.push({
|
|
75
76
|
runId: entryName,
|
|
76
77
|
createdAt: isRecord(bundle) && typeof bundle.createdAt === "string" ? bundle.createdAt : null,
|
|
@@ -105,7 +106,7 @@ async function listedRunDisplay(runPaths, bundle) {
|
|
|
105
106
|
if (displayed === undefined)
|
|
106
107
|
return {};
|
|
107
108
|
const status = displayed.outcome === undefined
|
|
108
|
-
? await readRunJsonIfExists(runPaths, RUN_STATUS_FILE)
|
|
109
|
+
? runJsonValue(await readRunJsonIfExists(runPaths, RUN_STATUS_FILE))
|
|
109
110
|
: undefined;
|
|
110
111
|
return { display: runDisplay(bundleDisplayFacts(displayed, status)) };
|
|
111
112
|
}
|
|
@@ -155,18 +156,24 @@ export async function readReview(cwdInput, runInput) {
|
|
|
155
156
|
if (!verified.ok || !verified.bundlePath) {
|
|
156
157
|
return verified;
|
|
157
158
|
}
|
|
158
|
-
const
|
|
159
|
+
const reviewRead = runPaths ? await readRunJsonIfExists(runPaths, "review.json") : undefined;
|
|
160
|
+
const review = reviewRead === undefined ? null : runJsonValue(reviewRead);
|
|
159
161
|
if (!isReviewSummary(review)) {
|
|
160
162
|
return {
|
|
161
163
|
...verified,
|
|
162
164
|
ok: false,
|
|
163
165
|
error: {
|
|
164
166
|
code: "HUMANISH_INVALID_RUN_BUNDLE",
|
|
165
|
-
message:
|
|
167
|
+
message: reviewRead?.status === "refused"
|
|
168
|
+
? `${refusalText("review.json", reviewRead)}.`
|
|
169
|
+
: "review.json is missing or invalid.",
|
|
166
170
|
},
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
|
-
|
|
173
|
+
// The display reads what it can: a run.json refused here shows as a missing one does.
|
|
174
|
+
const bundle = runPaths
|
|
175
|
+
? runJsonValue(await readRunJsonIfExists(runPaths, RUN_BUNDLE_FILE))
|
|
176
|
+
: null;
|
|
170
177
|
const projected = isRecord(bundle) && Array.isArray(bundle.streams)
|
|
171
178
|
? withCuaReviewProvenance(review, bundle.streams.filter(isRecord))
|
|
172
179
|
: review;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { REDACTION_MARKERS } from "../evidence/redaction.js";
|
|
3
|
-
import { holdsSecretValue, scrubSecretValues } from "../evidence/secret-scrub.js";
|
|
4
|
-
import { escapeRegExp } from "./text.js";
|
|
3
|
+
import { holdsSecretValue, scrubSecretValues, scrubValuesAsWritten, } from "../evidence/secret-scrub.js";
|
|
5
4
|
const scopes = new AsyncLocalStorage();
|
|
6
5
|
const MAX_VALUES = 8192;
|
|
7
6
|
const MAX_BYTES = 1024 * 1024;
|
|
@@ -15,7 +14,7 @@ function usable(scope) {
|
|
|
15
14
|
function fail(scope) {
|
|
16
15
|
scope.failed = true;
|
|
17
16
|
scope.values.clear();
|
|
18
|
-
delete scope.
|
|
17
|
+
delete scope.literal;
|
|
19
18
|
throw new Error("TRANSIENT_NARRATION_SECRET_LIMIT");
|
|
20
19
|
}
|
|
21
20
|
/** Enclose both participant execution and its automatic analysis in one scope. Nested runs isolate too. */
|
|
@@ -30,7 +29,7 @@ export async function withTransientCommsSecrets(work) {
|
|
|
30
29
|
scope.closed = true;
|
|
31
30
|
scope.values.clear();
|
|
32
31
|
scope.bytes = 0;
|
|
33
|
-
delete scope.
|
|
32
|
+
delete scope.literal;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
export function registerTransientCommsSecrets(values) {
|
|
@@ -50,7 +49,7 @@ export function registerTransientCommsSecrets(values) {
|
|
|
50
49
|
fail(scope);
|
|
51
50
|
scope.values.add(value);
|
|
52
51
|
scope.bytes += bytes;
|
|
53
|
-
delete scope.
|
|
52
|
+
delete scope.literal;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
/** Literal, longest-first replacement in one pass; replacement text is never matched recursively. */
|
|
@@ -59,18 +58,8 @@ export function scrubTransientCommsText(text) {
|
|
|
59
58
|
if (!scope)
|
|
60
59
|
return text;
|
|
61
60
|
usable(scope);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
scope.pattern ??= new RegExp([...scope.values]
|
|
66
|
-
.sort((a, b) => b.length - a.length)
|
|
67
|
-
.map(escapeRegExp)
|
|
68
|
-
.join("|"), "g");
|
|
69
|
-
return text.replace(scope.pattern, REDACTION_MARKERS.secret);
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return fail(scope);
|
|
73
|
-
}
|
|
61
|
+
scope.literal ??= scrubValuesAsWritten([...scope.values]);
|
|
62
|
+
return scope.literal(text);
|
|
74
63
|
}
|
|
75
64
|
const REDACTED = REDACTION_MARKERS.secret;
|
|
76
65
|
/**
|
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/config.js
CHANGED
|
@@ -40,7 +40,7 @@ import { isComputerUseComposition, isSharedWorldComposition, routeOf } from "./r
|
|
|
40
40
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
41
41
|
import { declaredParticipantCount, participantList } from "./study-fields.js";
|
|
42
42
|
import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
|
|
43
|
-
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
|
|
43
|
+
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, waitLimitValidationReason, } from "./validation.js";
|
|
44
44
|
import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
|
|
45
45
|
import { plural } from "../run/text.js";
|
|
46
46
|
/**
|
|
@@ -228,6 +228,9 @@ function checkStudyConfig(config) {
|
|
|
228
228
|
const outputLimitReason = outputTokenLimitValidationReason(config);
|
|
229
229
|
if (outputLimitReason)
|
|
230
230
|
return invalid(outputLimitReason);
|
|
231
|
+
const waitLimitReason = waitLimitValidationReason(config);
|
|
232
|
+
if (waitLimitReason)
|
|
233
|
+
return invalid(waitLimitReason);
|
|
231
234
|
// All-parallel default: a multi-participant study that does not declare execution.concurrency
|
|
232
235
|
// runs every participant at once; the declared field is a cap the author chose, never a mode.
|
|
233
236
|
// Independent computer-use participants resolve that default from the final participant count
|
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);
|
package/dist/study/keys.d.ts
CHANGED
package/dist/study/keys.js
CHANGED
package/dist/study/migrate/v2.js
CHANGED
|
@@ -95,7 +95,8 @@ const V2_KEYS = {
|
|
|
95
95
|
lanes: PARTICIPANT_ENTRY,
|
|
96
96
|
// Roster groups are participants with a count; the parser expanded them into `lanes[]`.
|
|
97
97
|
roster: { ...PARTICIPANT_ENTRY, count: true },
|
|
98
|
-
|
|
98
|
+
// maxWaitMs and idleWaitMs arrived after v2, so a v2 file cannot carry them.
|
|
99
|
+
...without(ACTOR, "type", "maxWaitMs", "idleWaitMs"),
|
|
99
100
|
laneFocus: { id: true, label: true, instruction: true },
|
|
100
101
|
},
|
|
101
102
|
execution: withAfter(EXECUTION, "egressAllow", { caps: CAPS }),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEVICE_PRESET_NAMES, isDevicePresetName } from "../device-presets.js";
|
|
2
2
|
import { isReasoningEffort, reasoningEffortNames } from "../../actors/reasoning-effort.js";
|
|
3
3
|
import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
4
|
+
import { waitFieldsReason } from "../../actors/computer-use/wait.js";
|
|
4
5
|
import { isHttpUrl } from "./subject.js";
|
|
5
6
|
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
6
7
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
@@ -54,6 +55,13 @@ export function parseActorFields(entry, path) {
|
|
|
54
55
|
return invalid(`${path}.maxOutputTokens must be a positive safe integer.`);
|
|
55
56
|
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
56
57
|
}
|
|
58
|
+
const waitReason = waitFieldsReason(entry, `${path}.`);
|
|
59
|
+
if (waitReason)
|
|
60
|
+
return invalid(waitReason);
|
|
61
|
+
if (typeof entry.maxWaitMs === "number")
|
|
62
|
+
actor.maxWaitMs = entry.maxWaitMs;
|
|
63
|
+
if (typeof entry.idleWaitMs === "number")
|
|
64
|
+
actor.idleWaitMs = entry.idleWaitMs;
|
|
57
65
|
const localAgent = str(entry.localAgent);
|
|
58
66
|
if (entry.localAgent !== undefined) {
|
|
59
67
|
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
@@ -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"));
|
|
@@ -36,6 +36,10 @@ interface DesktopParticipant extends Participant {
|
|
|
36
36
|
readonly dwell?: DwellWindow;
|
|
37
37
|
readonly reasoningEffort?: ReasoningEffort;
|
|
38
38
|
readonly maxOutputTokens?: number;
|
|
39
|
+
/** actor.maxWaitMs: the longest one wait action lasts. */
|
|
40
|
+
readonly maxWaitMs?: number;
|
|
41
|
+
/** actor.idleWaitMs: how long a wait with no duration lasts while the participant only waits. */
|
|
42
|
+
readonly idleWaitMs?: number;
|
|
39
43
|
};
|
|
40
44
|
}
|
|
41
45
|
/** Only independent computer-use participants consume a task protocol and their own target. */
|