pi-agent-browser-native 0.6.15 → 0.6.17
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 +27 -1
- package/README.md +22 -4
- package/dist/extensions/agent-browser/index.js +551 -437
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +21 -6
- package/dist/extensions/agent-browser/lib/execution-cwd.js +64 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +47 -11
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +7 -5
- package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +2 -1
- package/dist/extensions/agent-browser/lib/process.js +56 -20
- package/dist/extensions/agent-browser/lib/runtime.js +1 -1
- package/dist/extensions/agent-browser/lib/windows-stock-launcher.js +70 -0
- package/docs/ARCHITECTURE.md +14 -1
- package/docs/COMMAND_REFERENCE.md +2 -2
- package/docs/ELECTRON.md +4 -2
- package/docs/RELEASE.md +35 -1
- package/docs/SUPPORT_MATRIX.md +13 -3
- package/docs/TOOL_CONTRACT.md +11 -3
- package/docs/platform-smoke.md +5 -3
- package/package.json +10 -6
- package/scripts/config.mjs +1 -1
- package/scripts/doctor.mjs +2 -2
|
@@ -316,9 +316,9 @@ export function isSessionTabPinningExcludedCommand(command) {
|
|
|
316
316
|
export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
|
|
317
317
|
return hasCommandCapability(command, "excludedFromPostCommandCorrection");
|
|
318
318
|
}
|
|
319
|
-
export function
|
|
319
|
+
export function getRecordCommandOperandIndices(tokens) {
|
|
320
320
|
if (tokens[0] !== "record" || !["start", "restart"].includes(tokens[1] ?? ""))
|
|
321
|
-
return
|
|
321
|
+
return [];
|
|
322
322
|
const operands = [];
|
|
323
323
|
for (let index = 2; index < tokens.length && operands.length < 2; index += 1) {
|
|
324
324
|
// Native validates the range; bare/non-numeric --fps keeps its old literal meaning.
|
|
@@ -329,9 +329,13 @@ export function getRecordCommandOperands(tokens) {
|
|
|
329
329
|
else if (tokens[index] === "--contact-sheet-threshold" && tokens[index + 1] !== undefined)
|
|
330
330
|
index += 1;
|
|
331
331
|
else
|
|
332
|
-
operands.push(
|
|
332
|
+
operands.push(index);
|
|
333
333
|
}
|
|
334
|
-
return operands.length > 0 ?
|
|
334
|
+
return operands.length > 0 ? operands : [2, 3];
|
|
335
|
+
}
|
|
336
|
+
export function getRecordCommandOperands(tokens) {
|
|
337
|
+
const [path, url] = getRecordCommandOperandIndices(tokens);
|
|
338
|
+
return path === undefined ? {} : { path: tokens[path], url: url === undefined ? undefined : tokens[url] };
|
|
335
339
|
}
|
|
336
340
|
/** Starts conservatively invalidate refs because older supported natives replace the page, even on failure. Restarts invalidate only when they have a URL. */
|
|
337
341
|
export function isRecordPageTransitionCommand(tokens) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { lstat, rm } from "node:fs/promises";
|
|
3
|
+
import { win32 } from "node:path";
|
|
3
4
|
import { promisify } from "node:util";
|
|
4
5
|
import { fetchCdpJson, parseCdpTargets, parseCdpVersion } from "./cdp.js";
|
|
5
6
|
import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch.js";
|
|
@@ -53,21 +54,35 @@ export async function inspectElectronLaunchStatus(record, signal) {
|
|
|
53
54
|
version: cdp.version,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
57
|
+
function hasProcessExited(child, pid) {
|
|
58
|
+
// A Windows PID probe can report ESRCH during termination, before the
|
|
59
|
+
// tracked process has exited and released its executable/resources.
|
|
60
|
+
return child ? child.exitCode !== null || child.signalCode !== null : isPidAlive(pid) === false;
|
|
61
|
+
}
|
|
56
62
|
async function waitForProcessExit(child, pid, deadlineMs) {
|
|
57
63
|
while (Date.now() <= deadlineMs) {
|
|
58
|
-
if (
|
|
59
|
-
return true;
|
|
60
|
-
if (isPidAlive(pid) === false)
|
|
64
|
+
if (hasProcessExited(child, pid))
|
|
61
65
|
return true;
|
|
62
66
|
await sleep(ELECTRON_CLEANUP_POLL_INTERVAL_MS);
|
|
63
67
|
}
|
|
64
|
-
return
|
|
68
|
+
return hasProcessExited(child, pid);
|
|
65
69
|
}
|
|
66
70
|
async function readPidCommandLine(pid) {
|
|
67
71
|
if (!pid || !Number.isSafeInteger(pid) || pid <= 0)
|
|
68
72
|
return undefined;
|
|
69
73
|
try {
|
|
70
|
-
|
|
74
|
+
// Win32_Process.CommandLine is the native ownership evidence; process
|
|
75
|
+
// start time (used by daemon-policy locks) cannot prove profile ownership.
|
|
76
|
+
const systemRoot = process.env.SystemRoot;
|
|
77
|
+
const windowsRoot = systemRoot && win32.isAbsolute(systemRoot) ? systemRoot : "C:\\Windows";
|
|
78
|
+
const file = process.platform === "win32"
|
|
79
|
+
? win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
80
|
+
: "ps";
|
|
81
|
+
const args = process.platform === "win32"
|
|
82
|
+
? ["-NoProfile", "-NonInteractive", "-Command",
|
|
83
|
+
`[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $p = [wmi]"Win32_Process.Handle='${pid}'"; [Console]::WriteLine($p.CommandLine)`]
|
|
84
|
+
: ["-ww", "-p", String(pid), "-o", "command="];
|
|
85
|
+
const { stdout } = await execFileAsync(file, args, {
|
|
71
86
|
timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
|
|
72
87
|
});
|
|
73
88
|
return stdout.trim() || undefined;
|
|
@@ -117,7 +132,7 @@ function signalRestoredLaunchProcess(record, signal) {
|
|
|
117
132
|
async function cleanupProcess(record, child, deadlineMs) {
|
|
118
133
|
if (!record.pid)
|
|
119
134
|
return { resource: "process", state: "skipped" };
|
|
120
|
-
if (
|
|
135
|
+
if (hasProcessExited(child, record.pid))
|
|
121
136
|
return { resource: "process", state: "already-gone" };
|
|
122
137
|
if (!child) {
|
|
123
138
|
const verificationError = await getRestoredProcessVerificationError(record);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { isRecord } from "./parsing.js";
|
|
4
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
5
|
+
function isDirectoryOwner(source) {
|
|
6
|
+
if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
|
|
7
|
+
return true;
|
|
8
|
+
const directories = [source.baseDir, isAbsolute(source.path) ? dirname(source.path) : undefined];
|
|
9
|
+
return directories.some(directory => {
|
|
10
|
+
if (!directory || !isAbsolute(directory))
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).name === "pi-change-working-dir";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** The synchronous owner reply is captured before any browser policy, queue, or child await. */
|
|
21
|
+
export function resolveExecutionCwd(pi, ctx) {
|
|
22
|
+
const request = { sessionManager: ctx.sessionManager };
|
|
23
|
+
pi.events.emit("pi-change-working-dir:resolve-execution-cwd", request);
|
|
24
|
+
const result = request.result;
|
|
25
|
+
if (result !== undefined) {
|
|
26
|
+
const invalid = "pi-change-working-dir returned an invalid execution directory. Update the extension and restart Pi.";
|
|
27
|
+
if (!isRecord(result) || Array.isArray(result))
|
|
28
|
+
throw new Error(invalid);
|
|
29
|
+
if (result.error !== undefined)
|
|
30
|
+
throw new Error(typeof result.error === "string" && result.error.length > 0 ? result.error : invalid);
|
|
31
|
+
if (typeof result.cwd !== "string" || !isAbsolute(result.cwd) || result.cwd.includes("\0"))
|
|
32
|
+
throw new Error(invalid);
|
|
33
|
+
return result.cwd;
|
|
34
|
+
}
|
|
35
|
+
if (pi.getAllTools().some(tool => tool.name === "change_dir" && isDirectoryOwner(tool.sourceInfo))
|
|
36
|
+
|| pi.getCommands().some(command => /^cwd(?::\d+)?$/.test(command.name) && isDirectoryOwner(command.sourceInfo))) {
|
|
37
|
+
throw new Error("Update pi-change-working-dir and restart Pi to use agent_browser with the selected working directory.");
|
|
38
|
+
}
|
|
39
|
+
return ctx.cwd;
|
|
40
|
+
}
|
|
41
|
+
export function getBrowserCwdError(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
if (statSync(cwd).isDirectory())
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return `Browser launch directory is unavailable: ${cwd}. Restore that directory, or explicitly use sessionMode: "fresh" without --session, or --config from the selected execution directory. The existing browser was left untouched.`;
|
|
51
|
+
}
|
|
52
|
+
export function restoreManagedSessionCwd(branch, sessionName, namespace, fallback) {
|
|
53
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
54
|
+
for (const entry of [...branch].reverse()) {
|
|
55
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message) || entry.message.toolName !== "agent_browser")
|
|
56
|
+
continue;
|
|
57
|
+
const details = entry.message.details;
|
|
58
|
+
if (!isRecord(details) || typeof details.sessionName !== "string" || typeof details.managedSessionCwd !== "string" || !isAbsolute(details.managedSessionCwd))
|
|
59
|
+
continue;
|
|
60
|
+
if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key)
|
|
61
|
+
return details.managedSessionCwd;
|
|
62
|
+
}
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
@@ -232,7 +232,7 @@ export async function analyzeSourceLookupResults(data, compiled, cwd, context) {
|
|
|
232
232
|
const electronContext = status === "no-candidates" ? context?.electronContext : undefined;
|
|
233
233
|
const workspaceRoot = context?.workspaceRoot ?? cwd;
|
|
234
234
|
if (electronContext) {
|
|
235
|
-
limitations.push(`Workspace source scan is limited to the
|
|
235
|
+
limitations.push(`Workspace source scan is limited to the captured execution directory: ${workspaceRoot}.`, "Packaged Electron app code may live inside installed app resources or app.asar outside the workspace; the wrapper does not unpack asar files or scan app bundle resources.");
|
|
236
236
|
}
|
|
237
237
|
return {
|
|
238
238
|
candidates,
|
|
@@ -164,16 +164,29 @@ async function ownerAlive(owner) {
|
|
|
164
164
|
const current = await readProcessStartIdentity(owner.pid);
|
|
165
165
|
return current === undefined ? undefined : processStartIdentitiesMatch(owner.startIdentity, current);
|
|
166
166
|
}
|
|
167
|
-
async function removeClaimOwnedBy(path, token) {
|
|
168
|
-
const current = await readClaim(path);
|
|
169
|
-
if (current?.owner.token !== token)
|
|
170
|
-
return false;
|
|
167
|
+
async function removeClaimOwnedBy(path, token, deadline = 0) {
|
|
171
168
|
const movedPath = join(dirname(path), `.pi-agent-browser-policy-remove-${token}-${randomUUID()}`);
|
|
172
|
-
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
while (true) {
|
|
170
|
+
const current = await readClaim(path);
|
|
171
|
+
if (current?.owner.token !== token)
|
|
172
|
+
return false;
|
|
173
|
+
try {
|
|
174
|
+
await rename(path, movedPath);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
const code = error.code;
|
|
179
|
+
if (code === "ENOENT")
|
|
180
|
+
return true;
|
|
181
|
+
// Windows may refuse a directory move while a contender reads its
|
|
182
|
+
// metadata. Revalidate before retrying. Owned-claim cleanup gets
|
|
183
|
+
// its own bounded window, independent of the acquisition wait.
|
|
184
|
+
if (process.platform !== "win32" || code !== "EPERM" || Date.now() >= deadline)
|
|
185
|
+
return false;
|
|
186
|
+
await waitForRetry();
|
|
187
|
+
if (Date.now() >= deadline)
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
177
190
|
}
|
|
178
191
|
const moved = await readClaim(movedPath);
|
|
179
192
|
if (moved?.owner.token !== token) {
|
|
@@ -209,6 +222,12 @@ function claimPrecedes(left, right) {
|
|
|
209
222
|
return false;
|
|
210
223
|
return left.ticket < right.ticket || (left.ticket === right.ticket && left.owner.token < right.owner.token);
|
|
211
224
|
}
|
|
225
|
+
async function hasPublishedLaterTicket(claim, ownClaim) {
|
|
226
|
+
if (claim.ticket !== null)
|
|
227
|
+
return false;
|
|
228
|
+
const current = await readClaim(claim.path);
|
|
229
|
+
return current?.owner.token === claim.owner.token && !claimPrecedes(current, ownClaim);
|
|
230
|
+
}
|
|
212
231
|
function waitForRetry(signal) {
|
|
213
232
|
return new Promise((resolve) => {
|
|
214
233
|
if (signal?.aborted)
|
|
@@ -266,18 +285,35 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
266
285
|
for (const claim of claims) {
|
|
267
286
|
if (claim.owner.token === token || !claimPrecedes(claim, ownClaim))
|
|
268
287
|
continue;
|
|
288
|
+
// Choosing is transient: a later published ticket must not make
|
|
289
|
+
// its predecessor wait on it using an earlier null snapshot.
|
|
290
|
+
if (await hasPublishedLaterTicket(claim, ownClaim))
|
|
291
|
+
continue;
|
|
269
292
|
const alive = await ownerAlive(claim.owner);
|
|
270
293
|
if (alive === false) {
|
|
271
294
|
await removeClaimOwnedBy(claim.path, claim.owner.token);
|
|
272
295
|
continue;
|
|
273
296
|
}
|
|
297
|
+
// A native identity query can outlive the owner's entire critical
|
|
298
|
+
// section. Its result (including unknown after exit) does not make
|
|
299
|
+
// an already released immutable claim a current blocker.
|
|
300
|
+
try {
|
|
301
|
+
await lstat(claim.path);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error.code === "ENOENT")
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// Publication can also complete during the native identity query.
|
|
308
|
+
if (await hasPublishedLaterTicket(claim, ownClaim))
|
|
309
|
+
continue;
|
|
274
310
|
blocked = true;
|
|
275
311
|
break;
|
|
276
312
|
}
|
|
277
313
|
if (!blocked) {
|
|
278
314
|
await cleanDeadPolicyArtifacts(directory);
|
|
279
315
|
lockAcquired = true;
|
|
280
|
-
return { release: async () => { await removeClaimOwnedBy(claimPath, token); } };
|
|
316
|
+
return { release: async () => { await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS); } };
|
|
281
317
|
}
|
|
282
318
|
if (Date.now() >= deadline)
|
|
283
319
|
return undefined;
|
|
@@ -291,6 +327,6 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
291
327
|
finally {
|
|
292
328
|
await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
|
|
293
329
|
if (claimPublished && !lockAcquired)
|
|
294
|
-
await removeClaimOwnedBy(claimPath, token);
|
|
330
|
+
await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS);
|
|
295
331
|
}
|
|
296
332
|
}
|
|
@@ -2,7 +2,7 @@ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
4
|
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
5
|
-
import { getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
5
|
+
import { getRecordCommandOperandIndices, getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
6
6
|
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
7
7
|
function isSingleScreenshotPathToken(token) {
|
|
8
8
|
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
@@ -38,20 +38,25 @@ export function getScreenshotPathTokenIndex(commandTokens) {
|
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
41
|
-
function
|
|
42
|
-
|
|
41
|
+
export function getDiffFilePathIndices(commandTokens) {
|
|
42
|
+
if (commandTokens[0] !== "diff" || !["snapshot", "screenshot"].includes(commandTokens[1]))
|
|
43
|
+
return {};
|
|
44
|
+
const valueFlags = commandTokens[1] === "screenshot" ? DIFF_SCREENSHOT_VALUE_FLAGS : new Set(["-b", "--baseline", "-s", "--selector", "-d", "--depth"]);
|
|
45
|
+
const paths = {};
|
|
43
46
|
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
44
47
|
const token = commandTokens[index];
|
|
45
|
-
if (!
|
|
48
|
+
if (!valueFlags.has(token))
|
|
46
49
|
continue;
|
|
47
50
|
const value = commandTokens[index + 1];
|
|
48
51
|
if (value === undefined)
|
|
49
|
-
return
|
|
52
|
+
return {};
|
|
50
53
|
if (token === "-o" || token === "--output")
|
|
51
|
-
|
|
54
|
+
paths.output = index + 1;
|
|
55
|
+
if (token === "-b" || token === "--baseline")
|
|
56
|
+
paths.baseline = index + 1;
|
|
52
57
|
index += 1;
|
|
53
58
|
}
|
|
54
|
-
return
|
|
59
|
+
return paths;
|
|
55
60
|
}
|
|
56
61
|
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
57
62
|
let cursor = absolutePath;
|
|
@@ -102,28 +107,30 @@ export function getRecordContactSheetDestination(commandTokens) {
|
|
|
102
107
|
const extension = extname(path);
|
|
103
108
|
return extension ? `${path.slice(0, -extension.length)}.contact-sheet.png` : undefined;
|
|
104
109
|
}
|
|
105
|
-
export function
|
|
110
|
+
export function getExplicitArtifactDestinationIndex(commandTokens) {
|
|
106
111
|
const command = commandTokens[0];
|
|
107
112
|
const subcommand = commandTokens[1];
|
|
108
|
-
if (command === "screenshot")
|
|
109
|
-
|
|
110
|
-
return index === undefined ? undefined : commandTokens[index];
|
|
111
|
-
}
|
|
113
|
+
if (command === "screenshot")
|
|
114
|
+
return getScreenshotPathTokenIndex(commandTokens);
|
|
112
115
|
if (command === "download")
|
|
113
|
-
return
|
|
116
|
+
return 2;
|
|
114
117
|
if (command === "pdf")
|
|
115
|
-
return
|
|
118
|
+
return 1;
|
|
116
119
|
if (command === "wait")
|
|
117
|
-
return parseWaitCommandTokens(commandTokens).
|
|
120
|
+
return parseWaitCommandTokens(commandTokens).downloadPathIndex;
|
|
118
121
|
if (command === "state" && subcommand === "save")
|
|
119
|
-
return
|
|
122
|
+
return 2;
|
|
120
123
|
if (command === "diff" && subcommand === "screenshot")
|
|
121
|
-
return
|
|
124
|
+
return getDiffFilePathIndices(commandTokens).output;
|
|
122
125
|
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
123
|
-
return
|
|
126
|
+
return 3;
|
|
124
127
|
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
125
|
-
return
|
|
128
|
+
return 2;
|
|
126
129
|
if (command === "record")
|
|
127
|
-
return
|
|
130
|
+
return getRecordCommandOperandIndices(commandTokens)[0];
|
|
128
131
|
return undefined;
|
|
129
132
|
}
|
|
133
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
134
|
+
const index = getExplicitArtifactDestinationIndex(commandTokens);
|
|
135
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
136
|
+
}
|
|
@@ -863,7 +863,7 @@ export async function collectTimeoutPartialProgress(options) {
|
|
|
863
863
|
if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
|
|
864
864
|
return undefined;
|
|
865
865
|
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.commandTokens, options.stdin);
|
|
866
|
-
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
866
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.operationCwd ?? options.cwd, rawSteps);
|
|
867
867
|
const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
|
|
868
868
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
869
869
|
const titleData = recoveredUrl
|
|
@@ -147,6 +147,7 @@ function applyBatchNetworkRouteState(options) {
|
|
|
147
147
|
}
|
|
148
148
|
export async function processBrowserOutput(input) {
|
|
149
149
|
const { ctx, cwd, electronPostCommandStatusSettleMs, implicitSessionCloseTimeoutMs, sessionPageStateUpdate, signal, state } = input;
|
|
150
|
+
const operationCwd = input.operationCwd ?? cwd;
|
|
150
151
|
const { prepared, processResult } = input;
|
|
151
152
|
const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
|
|
152
153
|
let artifactManifest = state.artifactManifest;
|
|
@@ -395,7 +396,7 @@ export async function processBrowserOutput(input) {
|
|
|
395
396
|
let fillVerificationDiagnostic;
|
|
396
397
|
let selectorTextVisibilityDiagnostics = [];
|
|
397
398
|
let electronBroadGetTextScopeDiagnostics = [];
|
|
398
|
-
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
399
|
+
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, operationCwd: input.operationCwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
399
400
|
if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
|
|
400
401
|
currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
|
|
401
402
|
}
|
|
@@ -574,7 +575,8 @@ export async function processBrowserOutput(input) {
|
|
|
574
575
|
}
|
|
575
576
|
let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: commandClosesSession ? "close" : prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, currentSessionNamespace: managedSessionNamespace, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, replacedSessionNamespace: priorManagedSessionNamespace, sessionMode: prepared.sessionMode, succeeded: managedTransitionSucceeded });
|
|
576
577
|
if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
|
|
577
|
-
|
|
578
|
+
if (!priorManagedSessionActive || replacedManagedSessionName)
|
|
579
|
+
managedSessionCwd = cwd;
|
|
578
580
|
managedSessionNamespace = prepared.executionPlan.namespace;
|
|
579
581
|
}
|
|
580
582
|
if (sessionStateKey && succeeded) {
|
|
@@ -681,7 +683,7 @@ export async function processBrowserOutput(input) {
|
|
|
681
683
|
}
|
|
682
684
|
if (plainTextUpgrade && errorText)
|
|
683
685
|
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
684
|
-
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
686
|
+
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
685
687
|
if (recordingStopRecovery)
|
|
686
688
|
presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
|
|
687
689
|
const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
|
|
@@ -745,8 +747,8 @@ export async function processBrowserOutput(input) {
|
|
|
745
747
|
? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
|
|
746
748
|
: undefined;
|
|
747
749
|
const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
|
|
748
|
-
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup,
|
|
749
|
-
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup,
|
|
750
|
+
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, operationCwd, { electronContext: sourceLookupElectronContext, workspaceRoot: operationCwd }) : undefined;
|
|
751
|
+
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, operationCwd)) : undefined;
|
|
750
752
|
if (networkSourceLookup && presentation.content[0]?.type === "text")
|
|
751
753
|
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
752
754
|
else if (networkSourceLookup)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { projectUpstreamGlobalFlags, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
3
|
+
import { getDiffFilePathIndices, getExplicitArtifactDestinationIndex } from "./browser-run/artifact-paths.js";
|
|
4
|
+
import { parseBatchCommandArgument, parseUserBatchStdin } from "./batch-stdin.js";
|
|
5
|
+
/** Bind caller file operands without moving the native process/config or interpreting literal form data. */
|
|
6
|
+
export function resolveOperationPaths(args, stdin, cwd, batchStep = false) {
|
|
7
|
+
let resolved = args;
|
|
8
|
+
const setPath = (index, prefix = "") => {
|
|
9
|
+
const path = index === undefined ? undefined : args[index]?.slice(prefix.length);
|
|
10
|
+
if (index === undefined || !path || isAbsolute(path))
|
|
11
|
+
return;
|
|
12
|
+
if (resolved === args)
|
|
13
|
+
resolved = [...args];
|
|
14
|
+
resolved[index] = prefix + resolve(cwd, path);
|
|
15
|
+
};
|
|
16
|
+
const { tokens, indices } = batchStep ? { tokens: args, indices: args.map((_, index) => index) } : projectUpstreamGlobalFlags(args);
|
|
17
|
+
if (!batchStep) {
|
|
18
|
+
for (const flag of ["--config", "--ca-cert", "--executable-path", "--extension", "--init-script", "--download-path", "--screenshot-dir", "--action-policy", "--state"]) {
|
|
19
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, flag)) {
|
|
20
|
+
if (flag === "--state" && tokens[0] === "wait" && occurrence.index > indices[0])
|
|
21
|
+
continue;
|
|
22
|
+
setPath(occurrence.index + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--profile")) {
|
|
26
|
+
// Native Chrome profile names are identities, not workspace file paths.
|
|
27
|
+
if (occurrence.value && /[\\/]/.test(occurrence.value) && !occurrence.value.startsWith("~"))
|
|
28
|
+
setPath(occurrence.index + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (tokens[0] === "batch") {
|
|
32
|
+
if (tokens.slice(1).some(token => token !== "--bail")) {
|
|
33
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
34
|
+
if (tokens[index] === "--bail")
|
|
35
|
+
continue;
|
|
36
|
+
const row = parseBatchCommandArgument(tokens[index]).step;
|
|
37
|
+
if (!row)
|
|
38
|
+
continue;
|
|
39
|
+
const bound = resolveOperationPaths(row, undefined, cwd, true).args;
|
|
40
|
+
if (bound === row)
|
|
41
|
+
continue;
|
|
42
|
+
if (resolved === args)
|
|
43
|
+
resolved = [...args];
|
|
44
|
+
resolved[indices[index]] = bound.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
|
|
45
|
+
}
|
|
46
|
+
return { args: resolved, stdin }; // Native ignores stdin when raw rows exist.
|
|
47
|
+
}
|
|
48
|
+
const rows = parseUserBatchStdin(stdin).steps;
|
|
49
|
+
if (!rows?.length)
|
|
50
|
+
return { args: resolved, stdin };
|
|
51
|
+
const bound = rows.map(row => resolveOperationPaths(row, undefined, cwd, true).args);
|
|
52
|
+
return { args: resolved, stdin: bound.some((row, index) => row !== rows[index]) ? JSON.stringify(bound) : stdin };
|
|
53
|
+
}
|
|
54
|
+
setPath(indices[getExplicitArtifactDestinationIndex(tokens) ?? -1]);
|
|
55
|
+
if (tokens[0] === "upload")
|
|
56
|
+
for (let index = 2; index < tokens.length; index++)
|
|
57
|
+
setPath(indices[index]);
|
|
58
|
+
if (tokens[0] === "state" && tokens[1] === "load")
|
|
59
|
+
setPath(indices[2]);
|
|
60
|
+
setPath(indices[getDiffFilePathIndices(tokens).baseline ?? -1]);
|
|
61
|
+
if (tokens[0] === "cookies" && tokens[1] === "set") {
|
|
62
|
+
const index = tokens.indexOf("--curl");
|
|
63
|
+
if (index >= 0)
|
|
64
|
+
setPath(indices[index + 1]);
|
|
65
|
+
}
|
|
66
|
+
if (tokens[0] === "webmcp" && tokens[1] === "invoke") {
|
|
67
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
68
|
+
if (!["--params", "--frame", "--timeout"].includes(tokens[index]))
|
|
69
|
+
continue;
|
|
70
|
+
if (tokens[index] === "--params" && tokens[index + 1]?.startsWith("@"))
|
|
71
|
+
setPath(indices[index + 1], "@");
|
|
72
|
+
index++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { args: resolved, stdin };
|
|
76
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { dirname, join, win32 } from "node:path";
|
|
3
3
|
const WINDOWS_PROCESS_START_IDENTITY_PREFIX = "win32-powershell-ticks-v1:";
|
|
4
|
-
|
|
4
|
+
// Native Windows PowerShell startup can exhaust the former five-second probe budget.
|
|
5
|
+
const PROCESS_START_IDENTITY_TIMEOUT_MS = process.platform === "win32" ? 10_000 : 5_000;
|
|
5
6
|
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
6
7
|
export function buildProcessStartIdentityCommand(pid, platform = process.platform) {
|
|
7
8
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
@@ -11,6 +11,7 @@ import { getPageTargetValidationError, } from "./page-target-validation.js";
|
|
|
11
11
|
import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
|
|
12
12
|
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
13
13
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
14
|
+
import { resolveWindowsStockLauncher } from "./windows-stock-launcher.js";
|
|
14
15
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
16
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
16
17
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
@@ -52,11 +53,17 @@ export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent
|
|
|
52
53
|
normalized[occurrence.index + 1] = customArgs;
|
|
53
54
|
return normalized;
|
|
54
55
|
}
|
|
55
|
-
function terminateSpawnedChild(child, signal) {
|
|
56
|
+
async function terminateSpawnedChild(child, signal) {
|
|
56
57
|
if (processPlatform === "win32" && child.pid) {
|
|
57
|
-
|
|
58
|
-
killer
|
|
59
|
-
|
|
58
|
+
// Keep the shell alive until taskkill has traversed its descendants, and
|
|
59
|
+
// observe the killer before allowing the browser call to finish.
|
|
60
|
+
const killed = await new Promise((resolve) => {
|
|
61
|
+
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
62
|
+
killer.once("error", () => resolve(false));
|
|
63
|
+
killer.once("close", (code) => resolve(code === 0));
|
|
64
|
+
});
|
|
65
|
+
if (killed)
|
|
66
|
+
return;
|
|
60
67
|
}
|
|
61
68
|
child.kill(signal);
|
|
62
69
|
}
|
|
@@ -224,9 +231,29 @@ async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 })
|
|
|
224
231
|
}
|
|
225
232
|
return true;
|
|
226
233
|
}
|
|
227
|
-
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
234
|
+
export async function getAgentBrowserSocketDirValidationError(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined, platform = processPlatform) {
|
|
228
235
|
if (!isAbsolute(socketDir))
|
|
229
236
|
return "the path is not absolute";
|
|
237
|
+
// Windows uses native ACLs and named pipes, not POSIX uid/mode metadata.
|
|
238
|
+
// Still require a real directory; never accept a file or a redirected root.
|
|
239
|
+
if (platform === "win32") {
|
|
240
|
+
try {
|
|
241
|
+
try {
|
|
242
|
+
await mkdir(socketDir);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error.code !== "EEXIST")
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
const metadata = await lstat(socketDir);
|
|
249
|
+
if (metadata.isSymbolicLink())
|
|
250
|
+
return "the directory is a symlink";
|
|
251
|
+
return metadata.isDirectory() ? undefined : "the path is not a directory";
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
return `the directory could not be inspected (${error.code ?? "unknown error"})`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
230
257
|
if (typeof uid !== "number")
|
|
231
258
|
return "POSIX ownership metadata is unavailable";
|
|
232
259
|
try {
|
|
@@ -378,6 +405,8 @@ export async function runAgentBrowserProcess(options) {
|
|
|
378
405
|
}
|
|
379
406
|
effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
|
|
380
407
|
}
|
|
408
|
+
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
409
|
+
const stockLauncher = resolveWindowsStockLauncher(cwd, childEnv);
|
|
381
410
|
if (signal?.aborted) {
|
|
382
411
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
383
412
|
}
|
|
@@ -395,6 +424,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
395
424
|
let stdoutSpillPending = false;
|
|
396
425
|
let pendingStdoutWrite = Promise.resolve();
|
|
397
426
|
let stdoutSpillError;
|
|
427
|
+
let pendingTermination;
|
|
398
428
|
let killTimer;
|
|
399
429
|
let timeoutTimer;
|
|
400
430
|
let abortListener;
|
|
@@ -444,15 +474,16 @@ export async function runAgentBrowserProcess(options) {
|
|
|
444
474
|
if (settled)
|
|
445
475
|
return;
|
|
446
476
|
settled = true;
|
|
477
|
+
removeAbortListener();
|
|
478
|
+
if (killTimer) {
|
|
479
|
+
clearTimeout(killTimer);
|
|
480
|
+
}
|
|
481
|
+
if (timeoutTimer) {
|
|
482
|
+
clearTimeout(timeoutTimer);
|
|
483
|
+
}
|
|
484
|
+
completionWatcher?.clear();
|
|
447
485
|
void pendingStdoutWrite.finally(async () => {
|
|
448
|
-
|
|
449
|
-
if (killTimer) {
|
|
450
|
-
clearTimeout(killTimer);
|
|
451
|
-
}
|
|
452
|
-
if (timeoutTimer) {
|
|
453
|
-
clearTimeout(timeoutTimer);
|
|
454
|
-
}
|
|
455
|
-
completionWatcher?.clear();
|
|
486
|
+
await pendingTermination;
|
|
456
487
|
if (stdoutSpillHandle) {
|
|
457
488
|
await stdoutSpillHandle.close().catch(() => undefined);
|
|
458
489
|
}
|
|
@@ -478,14 +509,13 @@ export async function runAgentBrowserProcess(options) {
|
|
|
478
509
|
});
|
|
479
510
|
});
|
|
480
511
|
};
|
|
481
|
-
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
482
512
|
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, options.browserIndependentReadConfirmation);
|
|
483
513
|
if (spawnPolicyError) {
|
|
484
514
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
485
515
|
return;
|
|
486
516
|
}
|
|
487
|
-
const spawnBrowser = processPlatform === "win32" ? crossSpawn : spawn;
|
|
488
|
-
const child = spawnBrowser("agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
|
|
517
|
+
const spawnBrowser = processPlatform === "win32" && !stockLauncher ? crossSpawn : spawn;
|
|
518
|
+
const child = spawnBrowser(stockLauncher ?? "agent-browser", prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession, chromeStartupArgsContext.getStore()), {
|
|
489
519
|
cwd,
|
|
490
520
|
env: childEnv,
|
|
491
521
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -505,10 +535,16 @@ export async function runAgentBrowserProcess(options) {
|
|
|
505
535
|
else {
|
|
506
536
|
timedOut = true;
|
|
507
537
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
538
|
+
if (pendingTermination)
|
|
539
|
+
return;
|
|
540
|
+
pendingTermination = terminateSpawnedChild(child, "SIGTERM");
|
|
541
|
+
// Windows taskkill already forces the entire tree. A concurrent direct
|
|
542
|
+
// kill would remove its root before traversal finishes.
|
|
543
|
+
if (processPlatform !== "win32") {
|
|
544
|
+
killTimer = setTimeout(() => {
|
|
545
|
+
pendingTermination = terminateSpawnedChild(child, "SIGKILL");
|
|
546
|
+
}, 2_000);
|
|
547
|
+
}
|
|
512
548
|
};
|
|
513
549
|
const recordStdinError = (error) => {
|
|
514
550
|
const stdinError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -251,7 +251,7 @@ function redactBearerCredentials(text) {
|
|
|
251
251
|
return match;
|
|
252
252
|
return formatRedactedCredential(label, credential, trailing);
|
|
253
253
|
})
|
|
254
|
-
.replace(/\b(Bearer)\s+([^\s"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
|
|
254
|
+
.replace(/\b(Bearer)\s+([^\s\\"',)\[\]]+)([),.]?)/gi, (match, label, credential, trailing) => {
|
|
255
255
|
// Without a credential field/header, require a bearer-token shape, not prose, HTML or a URL.
|
|
256
256
|
const token = credential.slice(0, credential.length - credentialTrailingPunctuation(credential).length);
|
|
257
257
|
if (!/^[A-Za-z0-9._~+/-]+=*$/.test(token) || !/[0-9._~+/=-]/.test(token))
|