humanish 0.111.0 → 0.111.2
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 +28 -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/observer-app.html +6 -6
- 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_]*$/;
|