humanish 0.111.0 → 0.111.1
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 +16 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/codex/restricted-participant-policy.d.ts +2 -0
- package/dist/actors/codex/restricted-participant-policy.js +30 -2
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/executor-diagnostic.d.ts +15 -0
- package/dist/actors/computer-use/executor-diagnostic.js +105 -0
- package/dist/actors/computer-use/executor-error.d.ts +18 -3
- package/dist/actors/computer-use/executor-error.js +30 -4
- package/dist/actors/computer-use/loop/actions.d.ts +6 -3
- package/dist/actors/computer-use/loop/actions.js +27 -7
- package/dist/actors/computer-use/loop/ending.js +9 -1
- package/dist/actors/computer-use/loop/types.d.ts +14 -0
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/openai-context.d.ts +8 -2
- package/dist/actors/computer-use/openai-context.js +91 -26
- package/dist/actors/computer-use/openai-provider.js +55 -12
- package/dist/actors/computer-use/openai-requests.d.ts +2 -1
- package/dist/actors/computer-use/openai-requests.js +4 -1
- package/dist/actors/contract.d.ts +7 -1
- package/dist/browser-control/client.js +5 -5
- package/dist/browser-control/protocol.d.ts +30 -1
- package/dist/browser-control/protocol.js +19 -2
- package/dist/browser-control/transport.d.ts +3 -2
- package/dist/browser-control/transport.js +12 -7
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +2 -2
- package/dist/cli/commands/study.js +1 -2
- package/dist/cli/doctor.js +11 -0
- package/dist/evidence/encoded-text.d.ts +3 -3
- package/dist/evidence/encoded-text.js +10 -5
- package/dist/evidence/redaction.d.ts +2 -2
- package/dist/evidence/redaction.js +18 -7
- package/dist/guest/browser-tools.js +5 -1
- package/dist/guest/chromium-text.js +42 -18
- package/dist/guest/desktop-executor.js +5 -1
- package/dist/keys/key-resolution.d.ts +3 -0
- package/dist/keys/key-resolution.js +15 -2
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
- package/dist/routes/computer-use/bundle-parts.js +7 -2
- package/dist/routes/computer-use/fanout-bundle.js +4 -2
- package/dist/routes/computer-use/local-vm.js +1 -2
- package/dist/routes/computer-use/participant-model.js +6 -1
- package/dist/routes/computer-use/plan.js +3 -4
- package/dist/routes/computer-use/route.js +1 -2
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/preview.js +1 -0
- package/dist/routes/scripted/plan.js +4 -5
- package/dist/routes/scripted/route.js +2 -2
- package/dist/routes/shared-world/plan.js +4 -5
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/routes/shared-world/setup.js +1 -0
- package/dist/routes/terminal/dry-run.js +1 -0
- package/dist/routes/terminal/live-finish.js +24 -5
- package/dist/routes/terminal/live-sandbox.js +5 -1
- package/dist/routes/terminal/plan.js +3 -4
- package/dist/routes/terminal/recorder.d.ts +6 -0
- package/dist/routes/terminal/recorder.js +61 -6
- package/dist/routes/terminal/route.js +1 -2
- package/dist/routes/terminal/session.js +2 -1
- package/dist/routes/terminal/token-usage.d.ts +3 -0
- package/dist/routes/terminal/token-usage.js +5 -0
- package/dist/routes/terminal/types.d.ts +2 -1
- package/dist/routes/terminal/types.js +2 -1
- package/dist/run/dry-run.js +1 -0
- package/dist/run/results.d.ts +2 -0
- package/dist/run/run.d.ts +2 -0
- package/dist/run/run.js +16 -2
- package/dist/run/terminal-contract.d.ts +10 -0
- package/dist/run/terminal-contract.js +27 -9
- package/dist/study/composition-rules.js +17 -18
- package/dist/study/doctor.js +4 -5
- package/dist/study/migrate/v2.d.ts +5 -0
- package/dist/study/migrate/v2.js +23 -0
- package/dist/study/parse/front.js +2 -21
- package/dist/study/parse/url-credentials.d.ts +3 -3
- package/dist/study/parse/url-credentials.js +155 -13
- package/dist/study/persona-resolve.js +2 -2
- package/dist/study/plan-base.js +3 -4
- package/dist/study/plan-participants.js +3 -3
- package/dist/study/plan-types.d.ts +2 -0
- package/dist/study/plan.js +11 -5
- package/dist/study/routing.js +11 -11
- package/dist/study/run-study-options.js +2 -2
- package/dist/study/study-fields.d.ts +1 -13
- package/dist/study/study-fields.js +0 -24
- package/dist/study/summary.js +7 -7
- package/dist/study/types.d.ts +1 -0
- package/dist/study/validation.js +11 -11
- package/dist/study/warnings.d.ts +8 -0
- package/dist/study/warnings.js +22 -7
- package/dist/substrates/local/runtime-config.js +6 -6
- package/docs/architecture/browser-control.md +12 -2
- package/docs/architecture/guest-desktop.md +7 -1
- package/docs/architecture/terminal-product-route.md +3 -2
- package/docs/contracts/schemas.md +16 -5
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@ export declare function decodeEscapes(text: string): string;
|
|
|
4
4
|
export interface EncodedTextScanOptions {
|
|
5
5
|
/** Do not count an encoded binary run as opaque. */
|
|
6
6
|
readonly allowOpaqueBase64?: boolean;
|
|
7
|
-
/**
|
|
8
|
-
readonly
|
|
7
|
+
/** What counts as sensitive in the text and in each decoding of it; containsSensitive by default. */
|
|
8
|
+
readonly matches?: (text: string) => boolean;
|
|
9
9
|
}
|
|
10
10
|
export interface EncodedTextScan {
|
|
11
11
|
/** A secret or private path, in the text or in a decoding of it. */
|
|
@@ -27,4 +27,4 @@ export declare function scanEncodedText(text: string, options?: EncodedTextScanO
|
|
|
27
27
|
* scan of the same string would give. Repeated verifies of an unchanged file, as serve admission
|
|
28
28
|
* and the Observer render do, skip the scan.
|
|
29
29
|
*/
|
|
30
|
-
export declare function scanEncodedTextCached(text: string, options?: EncodedTextScanOptions): EncodedTextScan;
|
|
30
|
+
export declare function scanEncodedTextCached(text: string, options?: Pick<EncodedTextScanOptions, "allowOpaqueBase64">): EncodedTextScan;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// judge the same decoded text.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { readPlainText } from "./plain-text.js";
|
|
7
|
-
import {
|
|
7
|
+
import { containsSensitive } from "./redaction.js";
|
|
8
8
|
// HTML5 named references that stand for printable ASCII. Letters and digits have only numeric
|
|
9
9
|
// references, which decodeEscapes handles.
|
|
10
10
|
const ASCII_ENTITIES = {
|
|
@@ -189,7 +189,7 @@ function decodeTransferEscapes(text) {
|
|
|
189
189
|
.replace(/=\r?\n/g, "")
|
|
190
190
|
.replace(/=([0-9A-F]{2})/g, (match, hex) => codePoint(Number.parseInt(hex, 16), match));
|
|
191
191
|
}
|
|
192
|
-
const matcherOf = (options) => options.
|
|
192
|
+
const matcherOf = (options) => options.matches ?? containsSensitive;
|
|
193
193
|
const CLEAN = Object.freeze({ sensitive: false, opaque: false });
|
|
194
194
|
const SENSITIVE = Object.freeze({ sensitive: true, opaque: false });
|
|
195
195
|
function inspectDecoded(bytes, run, options, depth) {
|
|
@@ -261,7 +261,13 @@ export function scanEncodedText(text, options = {}, depth = 0) {
|
|
|
261
261
|
for (const bytes of slashStarts)
|
|
262
262
|
if (inspectDecoded(bytes, "", { ...options, allowOpaqueBase64: true }, depth).sensitive)
|
|
263
263
|
return SENSITIVE;
|
|
264
|
-
|
|
264
|
+
// Hex is read before transfer escapes too: the quoted-printable pass reads `=68` in `state=6874...`
|
|
265
|
+
// as one byte, which shifts every hex pair after it.
|
|
266
|
+
const hexRuns = new Set([
|
|
267
|
+
...[...expanded.matchAll(HEX_RUN)].map(([match]) => match),
|
|
268
|
+
...(expanded === decoded ? [] : [...decoded.matchAll(HEX_RUN)].map(([match]) => match)),
|
|
269
|
+
]);
|
|
270
|
+
for (const match of hexRuns) {
|
|
265
271
|
const plain = readPlainText(Buffer.from(match, "hex"));
|
|
266
272
|
if (plain.ok && depth < MAX_DEPTH && scanEncodedText(plain.text, options, depth + 1).sensitive)
|
|
267
273
|
return SENSITIVE;
|
|
@@ -271,7 +277,7 @@ export function scanEncodedText(text, options = {}, depth = 0) {
|
|
|
271
277
|
// Bump when scanEncodedText, decodeEscapes or the sensitive patterns change what they return. The
|
|
272
278
|
// cache lives in one process, so the version guards results across a hot reload or a test that
|
|
273
279
|
// swaps the scanner.
|
|
274
|
-
const ENCODED_SCAN_VERSION =
|
|
280
|
+
const ENCODED_SCAN_VERSION = 3;
|
|
275
281
|
// Distinct files one process verifies in a burst (a run's files, a serve library's runs).
|
|
276
282
|
const SCAN_CACHE_LIMIT = 256;
|
|
277
283
|
const scanCache = new Map();
|
|
@@ -286,7 +292,6 @@ export function scanEncodedTextCached(text, options = {}) {
|
|
|
286
292
|
const key = [
|
|
287
293
|
ENCODED_SCAN_VERSION,
|
|
288
294
|
options.allowOpaqueBase64 === true ? "opaque-allowed" : "opaque-unscanned",
|
|
289
|
-
options.secretsOnly === true ? "secrets-only" : "secrets-and-paths",
|
|
290
295
|
createHash("sha256").update(Buffer.from(text, "utf16le")).digest("hex"),
|
|
291
296
|
].join(":");
|
|
292
297
|
const cached = scanCache.get(key);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Every pattern containsSensitive tests. A test pins each to ASCII, which scanEncodedText relies on. */
|
|
2
2
|
export declare function sensitivePatterns(): readonly RegExp[];
|
|
3
|
-
/** True if the text contains a
|
|
4
|
-
export declare function
|
|
3
|
+
/** True if the text contains a credential. Local paths and a URL on an E2B host alone do not count. */
|
|
4
|
+
export declare function containsCredential(text: string): boolean;
|
|
5
5
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
6
6
|
export declare function containsSensitive(text: string): boolean;
|
|
7
7
|
/**
|
|
@@ -68,6 +68,14 @@ const E2B_PUBLIC_PAGE = [
|
|
|
68
68
|
String.raw `(?:www\.)?e2b\.dev\/dashboard\/?(?:\?tab=[a-z]{1,16})?`,
|
|
69
69
|
String.raw `(?:docs\.e2b\.dev|(?:www\.)?e2b\.dev\/docs)(?:\/[a-z][a-z-]{0,39}){0,8}\/?`,
|
|
70
70
|
].join("|");
|
|
71
|
+
// Any URL on an E2B host: a sandbox host names its sandbox, and a stream URL carries its auth
|
|
72
|
+
// key. The slashes may be JSON-escaped (`https:\/\/`). User info runs to its last `@`, as a URL
|
|
73
|
+
// parser reads it, and may hold any character but a slash or space. The host part stops at a
|
|
74
|
+
// backslash, quote or angle bracket, so a URL followed by an escaped line break and an `E2B_...`
|
|
75
|
+
// variable in JSON text is not read as an E2B host. A public page followed by `)@` is user info.
|
|
76
|
+
// A stream URL's auth key rides its `password` parameter, 16 characters from the SDK, which the
|
|
77
|
+
// credential parameter pattern matches as well.
|
|
78
|
+
const SANDBOX_URL = new RegExp(String.raw `(?!https:\/\/(?:${E2B_PUBLIC_PAGE})[.,;:!?'")]*(?:\s|$))https?:\\?\/\\?\/(?:[^/\s@]*@)*[^/\s\\"'<>@]*e2b[^)\s]+`, "gi");
|
|
71
79
|
// A pattern that matches a credential by its context puts the context in a `keep` group, which
|
|
72
80
|
// redaction leaves in place: `?token=[REDACTED_SECRET]`. Context goes in a group and not in a
|
|
73
81
|
// lookbehind, because a pattern that starts with a lookbehind is tried at every position of the
|
|
@@ -115,12 +123,7 @@ const SECRET_PATTERNS = [
|
|
|
115
123
|
// such as humanish-egress-auth-placeholder and JSON numbers do not count. The lookaheads are
|
|
116
124
|
// bounded, so a long run of `TOKEN=` costs linear time.
|
|
117
125
|
new RegExp(String.raw `(?<keep>${CREDENTIAL_VARIABLE}${QUOTE}[ \t]{0,4}[:=][ \t]{0,4}${QUOTE})${notAPlaceholder(VARIABLE_END)}${NOT_A_NUMBER}(?=[^\s"'\\,;&]{0,255}[0-9])(?=[^\s"'\\,;&]{0,255}[A-Za-z])[^\s"'\\,;&]{16}[^\s"'\\,;&]*`, "g"),
|
|
118
|
-
|
|
119
|
-
// key. The slashes may be JSON-escaped (`https:\/\/`). User info runs to its last `@`, as a URL
|
|
120
|
-
// parser reads it, and may hold any character but a slash or space. The host part stops at a
|
|
121
|
-
// backslash, quote or angle bracket, so a URL followed by an escaped line break and an `E2B_...`
|
|
122
|
-
// variable in JSON text is not read as an E2B host. A public page followed by `)@` is user info.
|
|
123
|
-
new RegExp(String.raw `(?!https:\/\/(?:${E2B_PUBLIC_PAGE})[.,;:!?'")]*(?:\s|$))https?:\\?\/\\?\/(?:[^/\s@]*@)*[^/\s\\"'<>@]*e2b[^)\s]+`, "gi"),
|
|
126
|
+
SANDBOX_URL,
|
|
124
127
|
/BEGIN (RSA|OPENSSH|PRIVATE) KEY/gi,
|
|
125
128
|
];
|
|
126
129
|
const LOCAL_PATH_PATTERNS = [
|
|
@@ -148,9 +151,17 @@ function matchesPattern(pattern, text) {
|
|
|
148
151
|
return pattern.test(text);
|
|
149
152
|
}
|
|
150
153
|
/** True if the text contains a secret-shaped token. Local paths do not count. */
|
|
151
|
-
|
|
154
|
+
function containsSecret(text) {
|
|
152
155
|
return SECRET_PATTERNS.some((pattern) => matchesPattern(pattern, text));
|
|
153
156
|
}
|
|
157
|
+
// Every secret pattern but the sandbox URL. A URL on an E2B host names a sandbox, which verify
|
|
158
|
+
// keeps out of a shared run, but the host is no credential: a key, token, password or signature in
|
|
159
|
+
// such a URL matches one of these patterns on its own.
|
|
160
|
+
const CREDENTIAL_PATTERNS = SECRET_PATTERNS.filter((pattern) => pattern !== SANDBOX_URL);
|
|
161
|
+
/** True if the text contains a credential. Local paths and a URL on an E2B host alone do not count. */
|
|
162
|
+
export function containsCredential(text) {
|
|
163
|
+
return CREDENTIAL_PATTERNS.some((pattern) => matchesPattern(pattern, text));
|
|
164
|
+
}
|
|
154
165
|
/** True if the text contains any secret-shaped token or known local path. */
|
|
155
166
|
export function containsSensitive(text) {
|
|
156
167
|
return (containsSecret(text) || LOCAL_PATH_PATTERNS.some(([pattern]) => matchesPattern(pattern, text)));
|
|
@@ -60,7 +60,11 @@ export function createGuestBrowserTools(native, content) {
|
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
62
|
// Ctrl+L itself is input. No content fallback after that dispatch.
|
|
63
|
-
throw
|
|
63
|
+
throw isComputerUseExecutorError(error)
|
|
64
|
+
? new ComputerUseExecutorError(error.code, "outcome_uncertain", {
|
|
65
|
+
diagnostic: error.diagnostic,
|
|
66
|
+
})
|
|
67
|
+
: new ComputerUseExecutorError("execution_failed", "outcome_uncertain");
|
|
64
68
|
}
|
|
65
69
|
},
|
|
66
70
|
async close() {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { ComputerUseExecutorError, isComputerUseExecutorError, } from "../actors/computer-use/executor-error.js";
|
|
3
|
+
import { classifyExecutorFailure, } from "../actors/computer-use/executor-diagnostic.js";
|
|
3
4
|
const DEADLINE_MS = 5_000;
|
|
4
5
|
// These expressions execute only in this port's isolated world. Actor text is
|
|
5
6
|
// never interpolated into JavaScript, a selector, a world name, or a CDP method.
|
|
@@ -107,20 +108,26 @@ function checkScope(settings, state, expected) {
|
|
|
107
108
|
if (state.generation !== expected)
|
|
108
109
|
throw new ComputerUseExecutorError("session_revoked", "not_dispatched");
|
|
109
110
|
const pages = context.pages();
|
|
110
|
-
if (state.dialogSeen ||
|
|
111
|
-
page.isClosed() ||
|
|
112
|
-
page.context() !== context ||
|
|
113
|
-
pages.length !== 1 ||
|
|
114
|
-
pages[0] !== page)
|
|
111
|
+
if (state.dialogSeen || page.isClosed() || page.context() !== context || !pages.includes(page))
|
|
115
112
|
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
113
|
+
// Playwright emulates focus in every page it drives, so the focus probe passes in a background
|
|
114
|
+
// tab too and cannot say which tab is in front. The participant learns why, and can close the
|
|
115
|
+
// other tab.
|
|
116
|
+
if (pages.length !== 1)
|
|
117
|
+
throw new ComputerUseExecutorError("action_rejected", "not_dispatched", {
|
|
118
|
+
reason: "extra_tab",
|
|
119
|
+
});
|
|
116
120
|
}
|
|
117
121
|
/**
|
|
118
122
|
* One bounded operation: a 5 s deadline, the caller's abort, and any interrupt race every step.
|
|
119
|
-
* A failure after a dispatching step makes the port unusable and reports outcome_uncertain.
|
|
123
|
+
* A failure after a dispatching step makes the port unusable and reports outcome_uncertain. An
|
|
124
|
+
* undeclared error becomes transport_failed with a diagnostic naming the failing step and a
|
|
125
|
+
* category; its message is classified and dropped.
|
|
120
126
|
*/
|
|
121
127
|
async function runOperation(settings, state, signal, expected, body) {
|
|
122
128
|
let dispatched = false;
|
|
123
129
|
let stopped;
|
|
130
|
+
let phase;
|
|
124
131
|
const controller = new AbortController();
|
|
125
132
|
let rejectStop;
|
|
126
133
|
const stop = new Promise((_resolve, reject) => {
|
|
@@ -151,11 +158,13 @@ async function runOperation(settings, state, signal, expected, body) {
|
|
|
151
158
|
return await body({
|
|
152
159
|
signal: controller.signal,
|
|
153
160
|
check,
|
|
154
|
-
async step(call, dispatch = false) {
|
|
161
|
+
async step(name, call, dispatch = false) {
|
|
155
162
|
check();
|
|
156
163
|
if (dispatch)
|
|
157
164
|
dispatched = true;
|
|
165
|
+
phase = name;
|
|
158
166
|
const value = await Promise.race([call(), stop]);
|
|
167
|
+
phase = undefined;
|
|
159
168
|
check();
|
|
160
169
|
return value;
|
|
161
170
|
},
|
|
@@ -165,8 +174,18 @@ async function runOperation(settings, state, signal, expected, body) {
|
|
|
165
174
|
controller.abort();
|
|
166
175
|
if (dispatched)
|
|
167
176
|
state.unusable = true;
|
|
168
|
-
const
|
|
169
|
-
|
|
177
|
+
const declared = isComputerUseExecutorError(error);
|
|
178
|
+
const code = stopped ?? (declared ? error.code : "transport_failed");
|
|
179
|
+
const reason = !dispatched && stopped === undefined && declared ? error.reason : undefined;
|
|
180
|
+
const diagnostic = stopped !== undefined
|
|
181
|
+
? undefined
|
|
182
|
+
: declared
|
|
183
|
+
? error.diagnostic
|
|
184
|
+
: classifyExecutorFailure(phase ?? "text_port", error);
|
|
185
|
+
throw new ComputerUseExecutorError(code, dispatched ? "outcome_uncertain" : "not_dispatched", {
|
|
186
|
+
reason,
|
|
187
|
+
diagnostic,
|
|
188
|
+
});
|
|
170
189
|
}
|
|
171
190
|
finally {
|
|
172
191
|
clearTimeout(timer);
|
|
@@ -191,9 +210,11 @@ function sessionDetacher(state) {
|
|
|
191
210
|
}),
|
|
192
211
|
]);
|
|
193
212
|
}
|
|
194
|
-
catch {
|
|
213
|
+
catch (error) {
|
|
195
214
|
state.unusable = true;
|
|
196
|
-
throw new ComputerUseExecutorError("transport_failed", "not_dispatched"
|
|
215
|
+
throw new ComputerUseExecutorError("transport_failed", "not_dispatched", {
|
|
216
|
+
diagnostic: classifyExecutorFailure("cdp_detach", error),
|
|
217
|
+
});
|
|
197
218
|
}
|
|
198
219
|
finally {
|
|
199
220
|
clearTimeout(timer);
|
|
@@ -209,11 +230,11 @@ function sessionDetacher(state) {
|
|
|
209
230
|
/** The owner's native window check, as one step of an operation. */
|
|
210
231
|
async function assertOwnerReady(settings, op) {
|
|
211
232
|
const { assertFocusedWindow } = settings;
|
|
212
|
-
await op.step(() => assertFocusedWindow(op.signal));
|
|
233
|
+
await op.step("owner_window_check", () => assertFocusedWindow(op.signal));
|
|
213
234
|
}
|
|
214
235
|
/** One fixed probe in the isolated world; anything but a boolean true refuses the action. */
|
|
215
236
|
async function probeElement(op, session, contextId, expression) {
|
|
216
|
-
const result = await op.step(() => session.send("Runtime.evaluate", evaluateParams(expression, contextId)));
|
|
237
|
+
const result = await op.step("focus_probe", () => session.send("Runtime.evaluate", evaluateParams(expression, contextId)));
|
|
217
238
|
if (!probeAccepted(result))
|
|
218
239
|
throw new ComputerUseExecutorError("action_rejected", "not_dispatched");
|
|
219
240
|
}
|
|
@@ -224,17 +245,17 @@ async function probeElement(op, session, contextId, expression) {
|
|
|
224
245
|
*/
|
|
225
246
|
async function acquireIsolatedWorld(settings, state, detach, prepared, op) {
|
|
226
247
|
await assertOwnerReady(settings, op);
|
|
227
|
-
prepared.session = await op.step(() => settings.context.newCDPSession(settings.page).then((acquired) => {
|
|
248
|
+
prepared.session = await op.step("cdp_session", () => settings.context.newCDPSession(settings.page).then((acquired) => {
|
|
228
249
|
state.sessions.add(acquired);
|
|
229
250
|
if (op.signal.aborted || state.closed)
|
|
230
251
|
void detach(acquired).catch(() => { });
|
|
231
252
|
return acquired;
|
|
232
253
|
}));
|
|
233
|
-
const tree = await op.step(() => prepared.session.send("Page.getFrameTree"));
|
|
254
|
+
const tree = await op.step("frame_tree", () => prepared.session.send("Page.getFrameTree"));
|
|
234
255
|
const frameId = ownedFrameId(tree);
|
|
235
256
|
if (frameId === undefined)
|
|
236
257
|
throw new ComputerUseExecutorError("invalid_response", "not_dispatched");
|
|
237
|
-
const world = await op.step(() => prepared.session.send("Page.createIsolatedWorld", {
|
|
258
|
+
const world = await op.step("isolated_world", () => prepared.session.send("Page.createIsolatedWorld", {
|
|
238
259
|
frameId,
|
|
239
260
|
worldName: settings.worldName,
|
|
240
261
|
}));
|
|
@@ -255,7 +276,10 @@ function disposePrepared(state, detach, prepared, self) {
|
|
|
255
276
|
await detach(prepared.session);
|
|
256
277
|
}
|
|
257
278
|
catch (error) {
|
|
258
|
-
|
|
279
|
+
const declared = isComputerUseExecutorError(error);
|
|
280
|
+
throw new ComputerUseExecutorError(declared ? error.code : "transport_failed", prepared.dispatchedText ? "outcome_uncertain" : "not_dispatched", {
|
|
281
|
+
diagnostic: declared ? error.diagnostic : classifyExecutorFailure("cdp_detach", error),
|
|
282
|
+
});
|
|
259
283
|
}
|
|
260
284
|
finally {
|
|
261
285
|
state.busy = false;
|
|
@@ -281,7 +305,7 @@ function preparedHandle(settings, state, prepared, dispose) {
|
|
|
281
305
|
await probeElement(op, prepared.session, prepared.contextId, RECHECK);
|
|
282
306
|
await assertOwnerReady(settings, op);
|
|
283
307
|
op.check();
|
|
284
|
-
await op.step(() => {
|
|
308
|
+
await op.step("insert_text", () => {
|
|
285
309
|
prepared.dispatchedText = true;
|
|
286
310
|
return prepared.session.send("Input.insertText", { text: prepared.text });
|
|
287
311
|
}, true);
|
|
@@ -171,7 +171,11 @@ export function failureEndsSession(progress, error, signal) {
|
|
|
171
171
|
*/
|
|
172
172
|
export function failureError(dispatched, error, signal) {
|
|
173
173
|
if (dispatched)
|
|
174
|
-
return
|
|
174
|
+
return isComputerUseExecutorError(error)
|
|
175
|
+
? new ComputerUseExecutorError(error.code, "outcome_uncertain", {
|
|
176
|
+
diagnostic: error.diagnostic,
|
|
177
|
+
})
|
|
178
|
+
: new ComputerUseExecutorError("execution_failed", "outcome_uncertain");
|
|
175
179
|
if (signal.aborted)
|
|
176
180
|
return new ComputerUseExecutorError("session_revoked", "not_dispatched");
|
|
177
181
|
return isComputerUseExecutorError(error)
|
|
@@ -33,6 +33,9 @@ export interface KeySourceProbe {
|
|
|
33
33
|
source: string | null;
|
|
34
34
|
/** The command/path that would fill it when missing. */
|
|
35
35
|
hint: string;
|
|
36
|
+
/** Set when the env holds the inert value a terminal study's sandbox gets in place of a key, and
|
|
37
|
+
* no other source supplies one. `source` is then null. */
|
|
38
|
+
placeholder?: true;
|
|
36
39
|
}
|
|
37
40
|
/** The nearest fill instruction for a missing key. Doctor rows use it, and it is appended to
|
|
38
41
|
* *_KEYS_MISSING errors so the failure names the fix. */
|
|
@@ -22,6 +22,7 @@ import { homedir } from "node:os";
|
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { loadEnvFile } from "./env-file.js";
|
|
24
24
|
import { cli } from "../cli/invocation.js";
|
|
25
|
+
import { OPENAI_EGRESS_PLACEHOLDER } from "../routes/terminal/runtime-auth.js";
|
|
25
26
|
/** The only names implicit discovery may fill (and `humanish keys set` may store). Everything
|
|
26
27
|
* else in an overlay/store file is ignored-and-named: a repo-planted NODE_OPTIONS/LD_PRELOAD
|
|
27
28
|
* must never enter process env off a file the operator did not explicitly pass (an explicit
|
|
@@ -263,7 +264,14 @@ export function describeMissingKeys(names, env) {
|
|
|
263
264
|
* Runs the same chain against a scratch copy of env so nothing observable changes.
|
|
264
265
|
*/
|
|
265
266
|
export async function probeKeySources(names, args) {
|
|
267
|
+
// A terminal study's sandbox sets CODEX_API_KEY to an inert placeholder so Codex starts; the key
|
|
268
|
+
// itself stays in the host-side egress proxy. The placeholder is no key, so it counts as unset and
|
|
269
|
+
// discovery may still find a real one.
|
|
270
|
+
const placeholder = (name) => args.env[name]?.trim() === OPENAI_EGRESS_PLACEHOLDER;
|
|
266
271
|
const scratch = { ...args.env };
|
|
272
|
+
for (const name of names)
|
|
273
|
+
if (placeholder(name))
|
|
274
|
+
delete scratch[name];
|
|
267
275
|
const fills = await discoverProviderKeys({
|
|
268
276
|
cwd: args.cwd,
|
|
269
277
|
env: scratch,
|
|
@@ -272,7 +280,7 @@ export async function probeKeySources(names, args) {
|
|
|
272
280
|
});
|
|
273
281
|
const bySource = new Map(fills.map((fill) => [fill.name, fill.source]));
|
|
274
282
|
return names.map((name) => {
|
|
275
|
-
const inEnv = args.env[name] !== undefined && args.env[name]?.trim() !== "";
|
|
283
|
+
const inEnv = args.env[name] !== undefined && args.env[name]?.trim() !== "" && !placeholder(name);
|
|
276
284
|
// GH_TOKEN and GITHUB_TOKEN are one credential with two spellings; a doctor row that says
|
|
277
285
|
// "missing" while GITHUB_TOKEN sits in the env would be wrong.
|
|
278
286
|
const aliasInEnv = name === "GH_TOKEN" &&
|
|
@@ -285,7 +293,12 @@ export async function probeKeySources(names, args) {
|
|
|
285
293
|
: aliasInEnv
|
|
286
294
|
? "process env (GITHUB_TOKEN)"
|
|
287
295
|
: (bySource.get(name) ?? null);
|
|
288
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
name,
|
|
298
|
+
source,
|
|
299
|
+
hint: missingKeyHint(name),
|
|
300
|
+
...(source === null && placeholder(name) ? { placeholder: true } : {}),
|
|
301
|
+
};
|
|
289
302
|
});
|
|
290
303
|
}
|
|
291
304
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -43,6 +43,10 @@ export declare function providerResourcesForOutcome(args: {
|
|
|
43
43
|
ids: ParticipantIds;
|
|
44
44
|
participantId: string;
|
|
45
45
|
}): RunProviderResource[];
|
|
46
|
+
/**
|
|
47
|
+
* The app URL as a run records it: the URL, or its digest when verify would flag it. verify reads the
|
|
48
|
+
* URL decoded, so an E2B URL percent-encoded in a parameter gets the digest too.
|
|
49
|
+
*/
|
|
46
50
|
export declare function publicSafeAppUrlLabel(url: string): string;
|
|
47
51
|
/** Short id-safe suffix for a subject-phase RunEvent: drops the shared prefix/suffix so each
|
|
48
52
|
* phase gets a distinct bundle event id (e.g. "clone", "state-before-build"). */
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// public-safe app URL label, the URL digest and the subject-phase event id suffix. The builders are bundle.ts (the dispatcher and the judge),
|
|
4
4
|
// single-bundle.ts and fanout-bundle.ts.
|
|
5
5
|
import { feedbackProofCommands } from "../../feedback/proof.js";
|
|
6
|
-
import {
|
|
6
|
+
import { scanEncodedText } from "../../evidence/encoded-text.js";
|
|
7
|
+
import { digestText, redactText } from "../../evidence/redaction.js";
|
|
7
8
|
import {} from "../../run/bundle.js";
|
|
8
9
|
import { participantResourceIds } from "../../run/participant-records.js";
|
|
9
10
|
import { resolveSelfReportedFriction } from "./self-report.js";
|
|
@@ -169,8 +170,12 @@ export function providerResourcesForOutcome(args) {
|
|
|
169
170
|
},
|
|
170
171
|
];
|
|
171
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* The app URL as a run records it: the URL, or its digest when verify would flag it. verify reads the
|
|
175
|
+
* URL decoded, so an E2B URL percent-encoded in a parameter gets the digest too.
|
|
176
|
+
*/
|
|
172
177
|
export function publicSafeAppUrlLabel(url) {
|
|
173
|
-
return
|
|
178
|
+
return scanEncodedText(url).sensitive ? `[target-url:${digestUrl(url)}]` : url;
|
|
174
179
|
}
|
|
175
180
|
/** Short id-safe suffix for a subject-phase RunEvent: drops the shared prefix/suffix so each
|
|
176
181
|
* phase gets a distinct bundle event id (e.g. "clone", "state-before-build"). */
|
|
@@ -4,7 +4,7 @@ import { actorEnding } from "../../actors/stop-cause.js";
|
|
|
4
4
|
import { redactText } from "../../evidence/redaction.js";
|
|
5
5
|
import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
|
|
6
6
|
import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, tallyParticipantOutcomes, withCuaReviewProvenance, } from "../../run/outcomes.js";
|
|
7
|
-
import { participantFeedbackCandidates, providerResourcesForOutcome } from "./bundle-parts.js";
|
|
7
|
+
import { participantFeedbackCandidates, providerResourcesForOutcome, publicSafeAppUrlLabel, } from "./bundle-parts.js";
|
|
8
8
|
import { participantFactsOf } from "./participant-facts.js";
|
|
9
9
|
import { participantPassed, participantStatus } from "../../run/judge.js";
|
|
10
10
|
import { buildRunCostSummary, desktopSpanToMinutes } from "../../run/cost-summary.js";
|
|
@@ -72,10 +72,12 @@ function fanoutReview(args, streams) {
|
|
|
72
72
|
verdict,
|
|
73
73
|
...(participants === undefined ? {} : { participants }),
|
|
74
74
|
...(runTasks === undefined ? {} : { tasks: runTasks }),
|
|
75
|
+
// The app URL is named as the participant records name it: one on an E2B host names a
|
|
76
|
+
// sandbox, so the summary carries its digest, which verify accepts in a shared run.
|
|
75
77
|
summary: args.inProgress === true
|
|
76
78
|
? `Live computer-use fan-out is running (${specs.length} participants, one world each); terminal participant evidence has not been written yet.`
|
|
77
79
|
: args.dryRun
|
|
78
|
-
? `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Dry-run fan-out: ${specs.length} participants composed for ${args.descriptor.id} against ${args.appUrl}, one world each; no desktops launched, $0 spend.`
|
|
80
|
+
? `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Dry-run fan-out: ${specs.length} participants composed for ${args.descriptor.id} against ${publicSafeAppUrlLabel(args.appUrl)}, one world each; no desktops launched, $0 spend.`
|
|
79
81
|
: `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Computer-use fan-out (${specs.length} participants, one world each): ${passedParticipants}/${plural(specs.length, "participant")} reached a terminal, engaged verdict${participants ? `: ${formatParticipantOutcomes(participants, participantEndings)}` : ""}${runTasks ? `; tasks: ${formatRunTaskFunnel(runTasks)}` : ""}.`,
|
|
80
82
|
gaps: args.inProgress === true
|
|
81
83
|
? ["Live fan-out session is still running."]
|
|
@@ -10,7 +10,6 @@ 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";
|
|
14
13
|
function guestMedia(config) {
|
|
15
14
|
const declaredMedia = config.execution?.desktop?.media;
|
|
16
15
|
return declaredMedia === undefined
|
|
@@ -165,7 +164,7 @@ export function prepareLocalVmRun(options) {
|
|
|
165
164
|
const media = guestMedia(config);
|
|
166
165
|
// A caller-supplied provider replaces the Codex account participant, so the account is
|
|
167
166
|
// neither checked nor used.
|
|
168
|
-
const account =
|
|
167
|
+
const account = config.actor?.type === "local-agent" && options.createProvider === undefined;
|
|
169
168
|
const state = { sessions: [], participants: [], cleanupUnconfirmed: false };
|
|
170
169
|
const context = {
|
|
171
170
|
config,
|
|
@@ -228,10 +228,15 @@ export async function recordParticipantTrace(spec, deps, session, warnings) {
|
|
|
228
228
|
* carried it on every request. It says why, and how many early turns became a text summary.
|
|
229
229
|
*/
|
|
230
230
|
export function explicitContextWarning(conversation) {
|
|
231
|
+
const answer = conversation.rejection === "stored_item"
|
|
232
|
+
? " (a stored item the request referenced was not found)"
|
|
233
|
+
: conversation.rejection === "previous_response"
|
|
234
|
+
? " (the previous response was refused or not found)"
|
|
235
|
+
: "";
|
|
231
236
|
const why = conversation.explicitReason === "zdr_rejection"
|
|
232
237
|
? `The OpenAI organization rejected server-side conversation state (zero data retention)${conversation.switchedAtRequest === undefined
|
|
233
238
|
? ""
|
|
234
|
-
: ` on request ${conversation.switchedAtRequest}`}`
|
|
239
|
+
: ` on request ${conversation.switchedAtRequest}`}${answer}`
|
|
235
240
|
: "zeroDataRetention is set";
|
|
236
241
|
const summarized = conversation.summarizedTurns === 0
|
|
237
242
|
? ""
|
|
@@ -16,7 +16,6 @@ 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";
|
|
20
19
|
/** The registered descriptor for a planned actor id; planComputerUseStudy checked the registry. */
|
|
21
20
|
export function cuaDescriptorOf(actor) {
|
|
22
21
|
const descriptor = actorRegistry[actor];
|
|
@@ -87,7 +86,7 @@ function unsupportedDeclarationReason(config, hasRunSession, driving, { inProces
|
|
|
87
86
|
const reason = desktopMediaValidationReason(config) || outputTokenLimitValidationReason(config);
|
|
88
87
|
if (reason)
|
|
89
88
|
return invalid(reason);
|
|
90
|
-
if (
|
|
89
|
+
if (config.actor?.maxOutputTokens !== undefined &&
|
|
91
90
|
(hasRunSession || driving.createProvider || driving.inProcess))
|
|
92
91
|
return invalid("maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.");
|
|
93
92
|
const receivingReason = receivingEmailValidationReason(config);
|
|
@@ -219,7 +218,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
219
218
|
if (tasksReason)
|
|
220
219
|
return refuse("before-scope", "HUMANISH_STUDY_TASKS_UNSUPPORTED", tasksReason);
|
|
221
220
|
// The parser checks the actor too; a library caller skips the parser.
|
|
222
|
-
const actorType =
|
|
221
|
+
const actorType = config.actor?.type ?? "";
|
|
223
222
|
const descriptor = actorRegistry[actorType];
|
|
224
223
|
if (!descriptor || !isCuaActorDescriptor(descriptor))
|
|
225
224
|
return refuse("in-scope", "HUMANISH_COMPUTER_USE_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered computer-use actor.`);
|
|
@@ -230,7 +229,7 @@ export function planComputerUseStudy(config, input) {
|
|
|
230
229
|
// A shared world runs every participant against one app; this route would run them as separate
|
|
231
230
|
// participants. It comes after the rules above, so a shared-world config that breaks one of them,
|
|
232
231
|
// which runStudyWith sends here, still gets that rule's message.
|
|
233
|
-
if (
|
|
232
|
+
if (config.route === "shared-world")
|
|
234
233
|
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);
|
|
235
234
|
// The in-process route drives subject.appUrl on this machine and creates no desktop, so it would
|
|
236
235
|
// skip the subject a clone, local-tree or desktop-cli study declares.
|
|
@@ -32,7 +32,6 @@ 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";
|
|
36
35
|
/**
|
|
37
36
|
* A refused computer-use study's result, at the refusal's stage: a before-scope refusal has its own
|
|
38
37
|
* envelope and no analysis record; the others come after the cwd checks, and the participant cap after
|
|
@@ -45,7 +44,7 @@ export async function computerUseStudyRefusal(options, refusal) {
|
|
|
45
44
|
...studyResultIdentity("computer-use", config.id),
|
|
46
45
|
ok: false,
|
|
47
46
|
cwd: path.resolve(options.cwd),
|
|
48
|
-
actor:
|
|
47
|
+
actor: config.actor?.type ?? "",
|
|
49
48
|
dryRun,
|
|
50
49
|
runId: options.runId ?? "not-created",
|
|
51
50
|
appUrl: config.subject.appUrl ?? config.subject.serve?.url ?? "",
|
|
@@ -25,7 +25,6 @@ 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";
|
|
29
28
|
/** The physical project, bound before any caller hook runs. */
|
|
30
29
|
async function bindProject(cwd) {
|
|
31
30
|
// Capture the physical project before reading or invoking any caller hook. A supported
|
|
@@ -48,7 +47,7 @@ export async function refuseCuaStudy(options, refusal) {
|
|
|
48
47
|
...studyResultIdentity("computer-use", config.id),
|
|
49
48
|
ok: false,
|
|
50
49
|
cwd: projectRoot.physicalPath,
|
|
51
|
-
actor: refusal.actor ??
|
|
50
|
+
actor: refusal.actor ?? config.actor?.type ?? "",
|
|
52
51
|
appUrl: declaredAppUrl(config),
|
|
53
52
|
dryRun,
|
|
54
53
|
runId: options.runId ?? "not-created",
|
|
@@ -214,6 +213,7 @@ export async function startCuaRun(plan, input, admitted, scope) {
|
|
|
214
213
|
mintRunId: makeCuaRunId,
|
|
215
214
|
mode: dryRun ? "dry-run" : "live",
|
|
216
215
|
study: plan.study,
|
|
216
|
+
warnings: plan.warnings,
|
|
217
217
|
renderReview: renderCuaReviewMarkdown,
|
|
218
218
|
observer: { open: input.open === true, render: seams.renderObserver },
|
|
219
219
|
});
|
package/dist/routes/preview.js
CHANGED
|
@@ -30,6 +30,7 @@ export function admitPreviewPlan(plan, input) {
|
|
|
30
30
|
function runPreviewPlan(plan, input) {
|
|
31
31
|
return runDryRun({
|
|
32
32
|
...(plan.study === undefined ? {} : { study: plan.study }),
|
|
33
|
+
...(plan.warnings === undefined ? {} : { warnings: plan.warnings }),
|
|
33
34
|
cwd: input.cwd,
|
|
34
35
|
dryRun: true,
|
|
35
36
|
participantCount: plan.participantCount,
|
|
@@ -7,7 +7,6 @@ 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";
|
|
11
10
|
// Default surface roster is 1 (desktop only): the defaults-table single-participant row governs;
|
|
12
11
|
// `count: 2` is the declared override that adds the mobile surface.
|
|
13
12
|
const DEFAULT_SURFACE_COUNT = 1;
|
|
@@ -43,7 +42,7 @@ export function planScriptedStudy(config, input) {
|
|
|
43
42
|
const cloneTargetReason = cloneTargetValidationReason(config);
|
|
44
43
|
if (cloneTargetReason)
|
|
45
44
|
return refuse("HUMANISH_SCRIPTED_SUBJECT_UNSAFE", cloneTargetReason);
|
|
46
|
-
const actorType =
|
|
45
|
+
const actorType = config.actor?.type ?? "";
|
|
47
46
|
const descriptor = actorRegistry[actorType];
|
|
48
47
|
if (!descriptor || !isScriptedBrowserActorDescriptor(descriptor))
|
|
49
48
|
return refuse("HUMANISH_SCRIPTED_ACTOR_UNSUPPORTED", `actor.type "${actorType}" is not a registered scripted-browser actor.`);
|
|
@@ -64,7 +63,7 @@ export function planScriptedStudy(config, input) {
|
|
|
64
63
|
subject = { kind: "loopback", appUrl };
|
|
65
64
|
}
|
|
66
65
|
// The steps are the actor, so there is no built-in journey to fall back on.
|
|
67
|
-
const scenarioRef =
|
|
66
|
+
const scenarioRef = config.scenario;
|
|
68
67
|
if (!scenarioRef?.trim())
|
|
69
68
|
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) });
|
|
70
69
|
const requirements = [];
|
|
@@ -76,7 +75,7 @@ export function planScriptedStudy(config, input) {
|
|
|
76
75
|
// The browser runs on this machine on both subjects; a clone's is pointed at its getHost URL.
|
|
77
76
|
if (!input.dryRun && input.injectedBrowser !== true)
|
|
78
77
|
requirements.push({ kind: "host-browser" });
|
|
79
|
-
const persona =
|
|
78
|
+
const persona = config.actor?.persona;
|
|
80
79
|
const timeoutMs = config.execution?.timeoutMs;
|
|
81
80
|
return {
|
|
82
81
|
ok: true,
|
|
@@ -89,7 +88,7 @@ export function planScriptedStudy(config, input) {
|
|
|
89
88
|
actor,
|
|
90
89
|
subject,
|
|
91
90
|
scenarioRef,
|
|
92
|
-
surfaces: browserSurfaces.slice(0,
|
|
91
|
+
surfaces: browserSurfaces.slice(0, config.surfaces?.length ?? DEFAULT_SURFACE_COUNT),
|
|
93
92
|
...(persona === undefined ? {} : { personaId: persona }),
|
|
94
93
|
...(timeoutMs === undefined ? {} : { sessionTimeoutMs: timeoutMs }),
|
|
95
94
|
requirements,
|
|
@@ -31,7 +31,6 @@ 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";
|
|
35
34
|
/**
|
|
36
35
|
* A refused scripted study's result, at the refusal's stage: a before-scope refusal has its own field
|
|
37
36
|
* order and no analysis record; the others come after the output directory checks.
|
|
@@ -39,7 +38,7 @@ import { actorOf } from "../../study/study-fields.js";
|
|
|
39
38
|
export async function scriptedStudyRefusal(options, refusal) {
|
|
40
39
|
const { config, dryRun } = options;
|
|
41
40
|
const cwd = path.resolve(options.cwd);
|
|
42
|
-
const actorType =
|
|
41
|
+
const actorType = config.actor?.type ?? "";
|
|
43
42
|
if (refusal.beforeScope)
|
|
44
43
|
return {
|
|
45
44
|
...studyResultIdentity("scripted", config.id),
|
|
@@ -95,6 +94,7 @@ async function runScriptedPlanInScope(plan, input, scope) {
|
|
|
95
94
|
mintRunId: makeScriptedRunId,
|
|
96
95
|
mode: dryRun ? "dry-run" : "live",
|
|
97
96
|
study: plan.study,
|
|
97
|
+
warnings: plan.warnings,
|
|
98
98
|
// Only a clone subject is served from a sandbox; an app-url run drives a local browser.
|
|
99
99
|
sandboxes: setup.clone ? undefined : "none",
|
|
100
100
|
renderReview: renderScriptedReviewMarkdown,
|