pi-agent-browser-native 0.2.72 → 0.2.75
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 +33 -0
- package/README.md +14 -12
- package/dist/extensions/agent-browser/index.js +148 -16
- package/dist/extensions/agent-browser/lib/argv-grammar.js +122 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +11 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +2 -2
- package/dist/extensions/agent-browser/lib/electron/launch.js +48 -12
- package/dist/extensions/agent-browser/lib/input-modes/params.js +96 -98
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +88 -2
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +22 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +432 -0
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +374 -0
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +367 -0
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +589 -0
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +299 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +35 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +9 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +40 -22
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +15 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +54 -33
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +182 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +629 -430
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +148 -57
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +30 -40
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +102 -19
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +13 -1
- package/dist/extensions/agent-browser/lib/playbook.js +9 -8
- package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
- package/dist/extensions/agent-browser/lib/process.js +271 -34
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +35 -12
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
- package/dist/extensions/agent-browser/lib/runtime.js +114 -107
- package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
- package/dist/extensions/agent-browser/lib/temp.js +13 -25
- package/docs/ARCHITECTURE.md +10 -9
- package/docs/COMMAND_REFERENCE.md +43 -23
- package/docs/ELECTRON.md +10 -10
- package/docs/RELEASE.md +3 -2
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +19 -18
- package/docs/TOOL_CONTRACT.md +29 -26
- package/docs/platform-smoke.md +2 -2
- package/package.json +1 -1
- package/platform-smoke.config.mjs +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +11 -3
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
|
|
3
|
-
* Responsibilities:
|
|
3
|
+
* Responsibilities: Validate POSIX socket storage, spawn the agent-browser subprocess, forward parent environment variables plus wrapper overrides, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
|
|
4
4
|
* Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
|
|
5
5
|
* Usage: Called by the extension tool after argument validation and session planning are complete.
|
|
6
6
|
* Invariants/Assumptions: The binary name is always `agent-browser`; Windows routes through PowerShell to invoke npm launchers with escaped argv; callers handle semantic success/error interpretation.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
|
-
import {
|
|
9
|
+
import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
10
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
10
11
|
import { env as processEnv, platform as processPlatform } from "node:process";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
12
|
+
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
13
|
+
import { needsManagedSession } from "./command-policy.js";
|
|
14
|
+
import { isKnownCommandToken } from "./command-taxonomy.js";
|
|
15
|
+
import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
|
|
16
|
+
import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
17
|
+
import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "./managed-session-state-policy.js";
|
|
18
|
+
import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
|
|
13
19
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
14
20
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
15
21
|
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
16
22
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
17
23
|
const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
|
|
18
24
|
const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
|
|
25
|
+
const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
|
|
19
26
|
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
20
27
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
21
28
|
const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
|
|
@@ -24,6 +31,7 @@ export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
|
|
|
24
31
|
const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
25
32
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
26
33
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
34
|
+
const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
|
|
27
35
|
function appendTail(text, addition, maxChars) {
|
|
28
36
|
const combined = text + addition;
|
|
29
37
|
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
@@ -31,37 +39,90 @@ function appendTail(text, addition, maxChars) {
|
|
|
31
39
|
function quoteWindowsPowerShellArg(value) {
|
|
32
40
|
return `'${value.replace(/'/g, "''")}'`;
|
|
33
41
|
}
|
|
34
|
-
const WINDOWS_LEADING_GLOBAL_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
|
|
35
42
|
/** Exported for unit tests that lock Windows launcher argv ordering. */
|
|
36
43
|
export function reorderWindowsLeadingGlobalArgs(args) {
|
|
37
44
|
const leadingGlobals = [];
|
|
38
|
-
let index = 0;
|
|
39
|
-
while (index < args.length && args[index]?.startsWith("-")) {
|
|
45
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
40
46
|
const token = args[index];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
if (isKnownCommandToken(token)) {
|
|
48
|
+
return index === 0 ? args : [token, ...leadingGlobals, ...args.slice(index + 1)];
|
|
49
|
+
}
|
|
50
|
+
if (!token.startsWith("-"))
|
|
51
|
+
return args;
|
|
52
|
+
if (token.startsWith("--restore=")) {
|
|
53
|
+
leadingGlobals.push(token);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (token === "--restore") {
|
|
57
|
+
const value = args[index + 1];
|
|
58
|
+
if (optionalGlobalValueFlagConsumesNext(token, value)) {
|
|
59
|
+
leadingGlobals.push(`--restore=${value}`);
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
leadingGlobals.push(token);
|
|
64
|
+
}
|
|
47
65
|
continue;
|
|
48
66
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
67
|
+
if (token.includes("="))
|
|
68
|
+
return args;
|
|
69
|
+
const flag = getFlagName(token);
|
|
70
|
+
if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(flag)) {
|
|
71
|
+
leadingGlobals.push(token);
|
|
72
|
+
if (["true", "false"].includes(args[index + 1] ?? "")) {
|
|
73
|
+
leadingGlobals.push(args[index + 1]);
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (GLOBAL_VALUE_FLAGS.includes(flag)) {
|
|
79
|
+
const value = args[index + 1];
|
|
80
|
+
if (value === undefined)
|
|
81
|
+
return args;
|
|
82
|
+
leadingGlobals.push(token, value);
|
|
51
83
|
index += 1;
|
|
84
|
+
continue;
|
|
52
85
|
}
|
|
53
|
-
}
|
|
54
|
-
if (leadingGlobals.length === 0 || index >= args.length)
|
|
55
86
|
return args;
|
|
56
|
-
|
|
87
|
+
}
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
export function pinAgentBrowserFileAccessDisabled(args) {
|
|
91
|
+
const filtered = [];
|
|
92
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
93
|
+
const token = args[index];
|
|
94
|
+
if (token.startsWith("--allow-file-access="))
|
|
95
|
+
continue;
|
|
96
|
+
if (token === "--allow-file-access") {
|
|
97
|
+
if (["false", "true"].includes(args[index + 1] ?? ""))
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
filtered.push(token);
|
|
102
|
+
}
|
|
103
|
+
return ["--args", "", "--allow-file-access", "false", ...filtered];
|
|
57
104
|
}
|
|
58
105
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
59
106
|
if (platform !== "win32") {
|
|
60
107
|
return { command: "agent-browser", args };
|
|
61
108
|
}
|
|
62
|
-
const
|
|
109
|
+
const invocationArgs = reorderWindowsLeadingGlobalArgs(args).map(quoteWindowsPowerShellArg).join(" ");
|
|
110
|
+
const commandLine = [
|
|
111
|
+
"$agentBrowser = Get-Command agent-browser.cmd -ErrorAction SilentlyContinue;",
|
|
112
|
+
`if (-not $agentBrowser) { [Console]::Error.WriteLine('${WINDOWS_AGENT_BROWSER_MISSING_MARKER}'); exit 127 };`,
|
|
113
|
+
`& $agentBrowser.Source ${invocationArgs}`.trimEnd(),
|
|
114
|
+
].join(" ");
|
|
63
115
|
return { command: "powershell.exe", args: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", commandLine] };
|
|
64
116
|
}
|
|
117
|
+
export function isWindowsAgentBrowserCommandMissing(stderr) {
|
|
118
|
+
const normalized = stderr.toLowerCase();
|
|
119
|
+
return normalized.includes(WINDOWS_AGENT_BROWSER_MISSING_MARKER.toLowerCase()) || (normalized.includes("agent-browser.cmd") && (normalized.includes("commandnotfoundexception") ||
|
|
120
|
+
normalized.includes("not recognized as the name of a cmdlet") ||
|
|
121
|
+
normalized.includes("not recognized as an internal or external command")));
|
|
122
|
+
}
|
|
123
|
+
export function shouldCommitManagedRestoreAfterWindowsProcess(input) {
|
|
124
|
+
return !input.spawnError && !(input.exitCode !== 0 && isWindowsAgentBrowserCommandMissing(input.stderr));
|
|
125
|
+
}
|
|
65
126
|
function terminateSpawnedChild(child, signal) {
|
|
66
127
|
if (processPlatform === "win32" && child.pid) {
|
|
67
128
|
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
@@ -154,13 +215,76 @@ export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeo
|
|
|
154
215
|
if (platform === "win32") {
|
|
155
216
|
return undefined;
|
|
156
217
|
}
|
|
157
|
-
|
|
218
|
+
const prefix = platform === "darwin" ? "/private/tmp/piab" : DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX;
|
|
219
|
+
return `${prefix}${typeof uid === "number" ? `-${uid}` : ""}`;
|
|
158
220
|
}
|
|
159
|
-
async function
|
|
221
|
+
async function hasTrustedSocketDirAncestry(socketDir, uid) {
|
|
222
|
+
for (let current = dirname(socketDir);;) {
|
|
223
|
+
const metadata = await lstat(current);
|
|
224
|
+
if (metadata.isSymbolicLink()) {
|
|
225
|
+
if (metadata.uid !== 0)
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
else if (!metadata.isDirectory()) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
if (!metadata.isSymbolicLink()) {
|
|
232
|
+
const mode = metadata.mode & 0o7777;
|
|
233
|
+
if (metadata.uid === uid) {
|
|
234
|
+
if ((mode & 0o022) !== 0)
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
else if (metadata.uid !== 0 || ((mode & 0o022) !== 0 && (mode & 0o1000) === 0)) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const parent = dirname(current);
|
|
242
|
+
if (parent === current)
|
|
243
|
+
return true;
|
|
244
|
+
current = parent;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function socketDirEntriesAreOwned(socketDir, uid, visited = { count: 0 }) {
|
|
248
|
+
for (const name of await readdir(socketDir)) {
|
|
249
|
+
if ((visited.count += 1) > 16_384)
|
|
250
|
+
return false;
|
|
251
|
+
try {
|
|
252
|
+
const path = join(socketDir, name);
|
|
253
|
+
const metadata = await lstat(path);
|
|
254
|
+
if (metadata.uid !== uid || metadata.isSymbolicLink())
|
|
255
|
+
return false;
|
|
256
|
+
if (metadata.isDirectory()) {
|
|
257
|
+
if (!await socketDirEntriesAreOwned(path, uid, visited))
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
else if (!metadata.isFile() && !metadata.isSocket()) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
if (error.code !== "ENOENT")
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
272
|
+
if (!isAbsolute(socketDir) || typeof uid !== "number")
|
|
273
|
+
return false;
|
|
160
274
|
try {
|
|
161
|
-
await
|
|
162
|
-
|
|
163
|
-
|
|
275
|
+
if (!await hasTrustedSocketDirAncestry(socketDir, uid))
|
|
276
|
+
return false;
|
|
277
|
+
try {
|
|
278
|
+
await mkdir(socketDir, { mode: 0o700 });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error.code !== "EEXIST")
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const metadata = await lstat(socketDir);
|
|
285
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || metadata.uid !== uid || (metadata.mode & 0o777) !== 0o700)
|
|
286
|
+
return false;
|
|
287
|
+
return await hasTrustedSocketDirAncestry(socketDir, uid) && await socketDirEntriesAreOwned(socketDir, uid);
|
|
164
288
|
}
|
|
165
289
|
catch {
|
|
166
290
|
return false;
|
|
@@ -183,21 +307,115 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
|
|
|
183
307
|
clampUpstreamDefaultTimeout(childEnv);
|
|
184
308
|
return childEnv;
|
|
185
309
|
}
|
|
310
|
+
function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessionTarget = false, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false, trustedPinnedEmptyConfig = false) {
|
|
311
|
+
const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
|
|
312
|
+
const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
|
|
313
|
+
if (managedSessionTargetError)
|
|
314
|
+
return managedSessionTargetError;
|
|
315
|
+
if (!validateManagedSessionRestoreContextForSpawn(options)) {
|
|
316
|
+
return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
|
|
317
|
+
}
|
|
318
|
+
return getManagedSessionStateAccessValidationError({
|
|
319
|
+
args: options.args,
|
|
320
|
+
currentPageUrl,
|
|
321
|
+
cwd: options.cwd,
|
|
322
|
+
env: effectiveEnv ?? options.env,
|
|
323
|
+
pageUrlUnknown,
|
|
324
|
+
parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
|
|
325
|
+
stdin: options.stdin,
|
|
326
|
+
trustedFirstBatchTabSelection,
|
|
327
|
+
trustedPinnedEmptyConfig,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
186
330
|
export async function runAgentBrowserProcess(options) {
|
|
187
|
-
const {
|
|
331
|
+
const { allowManagedSessionTarget, cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
|
|
332
|
+
const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
|
|
333
|
+
const args = canonicalizeOwnedManagedSessionCloseArgs({
|
|
334
|
+
args: options.args,
|
|
335
|
+
cwd,
|
|
336
|
+
env,
|
|
337
|
+
ownedManagedSession,
|
|
338
|
+
restoreState: managedSessionRestoreState,
|
|
339
|
+
stdin,
|
|
340
|
+
});
|
|
188
341
|
const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
|
|
342
|
+
if (signal?.aborted) {
|
|
343
|
+
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
344
|
+
}
|
|
345
|
+
const managedSessionRestoreOptions = {
|
|
346
|
+
args,
|
|
347
|
+
cwd,
|
|
348
|
+
env,
|
|
349
|
+
ownedManagedSession,
|
|
350
|
+
restoreState: managedSessionRestoreState,
|
|
351
|
+
stdin,
|
|
352
|
+
};
|
|
353
|
+
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, undefined, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
|
|
354
|
+
if (planningPolicyError) {
|
|
355
|
+
return {
|
|
356
|
+
aborted: false,
|
|
357
|
+
agentBrowserStarted: false,
|
|
358
|
+
exitCode: 1,
|
|
359
|
+
spawnError: new Error(planningPolicyError),
|
|
360
|
+
stderr: "",
|
|
361
|
+
stdout: "",
|
|
362
|
+
timedOut: false,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
366
|
+
const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
367
|
+
const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
|
|
368
|
+
const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
|
|
369
|
+
if (managedSessionRestoreConfigEnv === undefined) {
|
|
370
|
+
return {
|
|
371
|
+
aborted: false,
|
|
372
|
+
agentBrowserStarted: false,
|
|
373
|
+
exitCode: 1,
|
|
374
|
+
spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
|
|
375
|
+
stderr: "",
|
|
376
|
+
stdout: "",
|
|
377
|
+
timedOut: false,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
189
380
|
const processOverrides = {
|
|
190
381
|
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
382
|
+
...managedSessionRestoreEnv,
|
|
191
383
|
...env,
|
|
384
|
+
...managedSessionRestoreConfigEnv,
|
|
385
|
+
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
386
|
+
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
387
|
+
...getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions),
|
|
388
|
+
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
192
389
|
};
|
|
193
390
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
194
391
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
392
|
+
if (ownedManagedSessionClose)
|
|
393
|
+
effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
|
|
195
394
|
const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
|
|
196
|
-
if (requestedSocketDir
|
|
395
|
+
if (requestedSocketDir !== undefined) {
|
|
396
|
+
const socketDirIsSecure = requestedSocketDir.length > 0 && await ensureAgentBrowserSocketDir(requestedSocketDir);
|
|
397
|
+
if (signal?.aborted) {
|
|
398
|
+
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
399
|
+
}
|
|
400
|
+
if (!socketDirIsSecure) {
|
|
401
|
+
return {
|
|
402
|
+
aborted: false,
|
|
403
|
+
agentBrowserStarted: false,
|
|
404
|
+
exitCode: 1,
|
|
405
|
+
spawnError: new Error("Agent-browser socket storage must be an absolute, non-symlink directory owned by the current user with mode 0700."),
|
|
406
|
+
stderr: "",
|
|
407
|
+
stdout: "",
|
|
408
|
+
timedOut: false,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
197
411
|
effectiveEnv = { ...effectiveEnv, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
|
|
198
412
|
}
|
|
413
|
+
if (signal?.aborted) {
|
|
414
|
+
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
415
|
+
}
|
|
199
416
|
return await new Promise((resolve) => {
|
|
200
417
|
let aborted = false;
|
|
418
|
+
let agentBrowserStarted = false;
|
|
201
419
|
let settled = false;
|
|
202
420
|
let spawnError;
|
|
203
421
|
let stderr = "";
|
|
@@ -268,6 +486,15 @@ export async function runAgentBrowserProcess(options) {
|
|
|
268
486
|
if (stdoutSpillHandle) {
|
|
269
487
|
await stdoutSpillHandle.close().catch(() => undefined);
|
|
270
488
|
}
|
|
489
|
+
const windowsMissingBinary = processPlatform === "win32" && exitCode !== 0 && isWindowsAgentBrowserCommandMissing(stderr);
|
|
490
|
+
if (processPlatform === "win32" && !windowsMissingBinary && !spawnError)
|
|
491
|
+
agentBrowserStarted = true;
|
|
492
|
+
if (windowsMissingBinary && !spawnError) {
|
|
493
|
+
spawnError = Object.assign(new Error("spawn agent-browser ENOENT"), { code: "ENOENT" });
|
|
494
|
+
}
|
|
495
|
+
else if (processPlatform === "win32" && shouldCommitManagedRestoreAfterWindowsProcess({ exitCode, spawnError, stderr })) {
|
|
496
|
+
commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
|
|
497
|
+
}
|
|
271
498
|
if (!spawnError && stdoutSpillError) {
|
|
272
499
|
spawnError = stdoutSpillError;
|
|
273
500
|
}
|
|
@@ -275,6 +502,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
275
502
|
destroySpawnedChildStreams(child);
|
|
276
503
|
resolve({
|
|
277
504
|
aborted,
|
|
505
|
+
agentBrowserStarted,
|
|
278
506
|
exitCode,
|
|
279
507
|
spawnError,
|
|
280
508
|
stderr,
|
|
@@ -285,12 +513,24 @@ export async function runAgentBrowserProcess(options) {
|
|
|
285
513
|
});
|
|
286
514
|
});
|
|
287
515
|
};
|
|
288
|
-
const
|
|
516
|
+
const childEnv = buildAgentBrowserProcessEnv(processEnv, effectiveEnv);
|
|
517
|
+
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, childEnv, allowManagedSessionTarget, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection, managedSessionRestoreConfigEnv.AGENT_BROWSER_CONFIG !== undefined);
|
|
518
|
+
if (spawnPolicyError) {
|
|
519
|
+
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args));
|
|
289
523
|
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
290
524
|
cwd,
|
|
291
|
-
env:
|
|
525
|
+
env: childEnv,
|
|
292
526
|
stdio: ["pipe", "pipe", "pipe"],
|
|
293
527
|
});
|
|
528
|
+
if (processPlatform !== "win32") {
|
|
529
|
+
child.once("spawn", () => {
|
|
530
|
+
agentBrowserStarted = true;
|
|
531
|
+
commitManagedSessionRestoreSuppression(managedSessionRestoreOptions);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
294
534
|
const terminateChild = (reason) => {
|
|
295
535
|
if (settled)
|
|
296
536
|
return;
|
|
@@ -356,13 +596,10 @@ export async function runAgentBrowserProcess(options) {
|
|
|
356
596
|
timeoutTimer.unref?.();
|
|
357
597
|
}
|
|
358
598
|
if (signal) {
|
|
359
|
-
|
|
599
|
+
abortListener = () => terminateChild("abort");
|
|
600
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
601
|
+
if (signal.aborted)
|
|
360
602
|
terminateChild("abort");
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
abortListener = () => terminateChild("abort");
|
|
364
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
365
|
-
}
|
|
366
603
|
}
|
|
367
604
|
writeChildStdin();
|
|
368
605
|
});
|
|
@@ -73,16 +73,18 @@ export function formatSessionArtifactRetentionSummary(manifest) {
|
|
|
73
73
|
parts.push(`${missingCount} missing`);
|
|
74
74
|
return `Session artifacts: ${parts.join(", ")} (${manifest.entries.length}/${manifest.maxEntries} recent).`;
|
|
75
75
|
}
|
|
76
|
+
export function getSessionArtifactManifestEntryKey(entry) {
|
|
77
|
+
return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
|
|
78
|
+
}
|
|
76
79
|
export function mergeSessionArtifactManifest(options) {
|
|
77
80
|
const nowMs = options.nowMs ?? Date.now();
|
|
78
81
|
const maxEntries = getSessionArtifactManifestMaxEntries();
|
|
79
|
-
const getEntryKey = (entry) => entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
|
|
80
82
|
const byPath = new Map();
|
|
81
83
|
for (const entry of options.base?.entries ?? []) {
|
|
82
|
-
byPath.set(
|
|
84
|
+
byPath.set(getSessionArtifactManifestEntryKey(entry), entry);
|
|
83
85
|
}
|
|
84
86
|
for (const entry of options.entries ?? []) {
|
|
85
|
-
const key =
|
|
87
|
+
const key = getSessionArtifactManifestEntryKey(entry);
|
|
86
88
|
const existing = byPath.get(key);
|
|
87
89
|
byPath.set(key, {
|
|
88
90
|
...existing,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Responsibilities: Normalize scalar fields, stringify model-facing values, and apply sensitive-text redaction.
|
|
4
4
|
* Scope: Leaf helpers only; command-family formatting lives in sibling modules.
|
|
5
5
|
*/
|
|
6
|
+
import { containsManagedSessionRestoreKey } from "../../managed-session-capabilities.js";
|
|
6
7
|
import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
|
|
7
8
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
8
9
|
export function stringifyModelFacing(value) {
|
|
@@ -27,7 +28,7 @@ export function redactModelFacingText(text) {
|
|
|
27
28
|
return redactSensitiveText(text);
|
|
28
29
|
}
|
|
29
30
|
export function redactModelFacingTextIfSensitive(text) {
|
|
30
|
-
return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
|
|
31
|
+
return containsManagedSessionRestoreKey(text) || /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
|
|
31
32
|
? redactModelFacingText(text)
|
|
32
33
|
: text;
|
|
33
34
|
}
|
|
@@ -9,6 +9,7 @@ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifa
|
|
|
9
9
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
10
10
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
11
11
|
import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
|
|
12
|
+
import { filterCallerOwnedSessionListItems, filterCallerOwnedStateListItems, filterManagedSessionListRows, filterManagedStateListRows, } from "./managed-list-filter.js";
|
|
12
13
|
const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
|
|
13
14
|
const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
|
|
14
15
|
const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
@@ -114,7 +115,7 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
114
115
|
if (commandInfo.command === "session") {
|
|
115
116
|
const sessions = getArrayField(data, "sessions");
|
|
116
117
|
if (sessions)
|
|
117
|
-
return `Sessions: ${sessions.length}`;
|
|
118
|
+
return `Sessions: ${filterCallerOwnedSessionListItems(sessions).length}`;
|
|
118
119
|
const session = getStringField(data, "session");
|
|
119
120
|
if (session)
|
|
120
121
|
return `Session: ${session}`;
|
|
@@ -172,11 +173,13 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
172
173
|
}
|
|
173
174
|
if (commandInfo.command === "state") {
|
|
174
175
|
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
175
|
-
if (states)
|
|
176
|
-
|
|
176
|
+
if (states) {
|
|
177
|
+
const visibleStates = commandInfo.subcommand === "list" ? filterCallerOwnedStateListItems(states) : states;
|
|
178
|
+
return `States: ${visibleStates.length}`;
|
|
179
|
+
}
|
|
177
180
|
if (commandInfo.subcommand === "load")
|
|
178
181
|
return undefined;
|
|
179
|
-
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
182
|
+
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "filename") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
180
183
|
if (stateName)
|
|
181
184
|
return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
|
|
182
185
|
}
|
|
@@ -261,9 +264,10 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
261
264
|
function formatSessionText(data) {
|
|
262
265
|
const sessions = getArrayField(data, "sessions");
|
|
263
266
|
if (sessions) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
const visibleSessions = filterCallerOwnedSessionListItems(sessions);
|
|
268
|
+
if (visibleSessions.length === 0)
|
|
269
|
+
return sessions.length === 0 ? "No active sessions." : "No caller-owned active sessions.";
|
|
270
|
+
return visibleSessions
|
|
267
271
|
.map((item, index) => {
|
|
268
272
|
if (!isRecord(item))
|
|
269
273
|
return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
@@ -912,12 +916,25 @@ function formatFrameText(data) {
|
|
|
912
916
|
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
|
|
913
917
|
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
914
918
|
}
|
|
915
|
-
function formatStateText(data) {
|
|
919
|
+
function formatStateText(data, subcommand) {
|
|
920
|
+
if (subcommand === "show") {
|
|
921
|
+
const filename = getStringField(data, "filename") ?? getStringField(data, "name") ?? "saved state";
|
|
922
|
+
const summary = getStringField(data, "summary");
|
|
923
|
+
const lines = [`Saved state: ${redactModelFacingText(filename)}`];
|
|
924
|
+
if (summary)
|
|
925
|
+
lines.push(`Summary: ${redactModelFacingText(summary)}`);
|
|
926
|
+
if (typeof data.encrypted === "boolean")
|
|
927
|
+
lines.push(`Encrypted: ${data.encrypted ? "yes" : "no"}`);
|
|
928
|
+
if (typeof data.size === "number")
|
|
929
|
+
lines.push(`Size: ${data.size} bytes`);
|
|
930
|
+
return lines.join("\n");
|
|
931
|
+
}
|
|
916
932
|
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
917
933
|
if (states) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
934
|
+
const visibleStates = filterCallerOwnedStateListItems(states);
|
|
935
|
+
if (visibleStates.length === 0)
|
|
936
|
+
return "No caller-owned saved states.";
|
|
937
|
+
return visibleStates
|
|
921
938
|
.map((item, index) => {
|
|
922
939
|
if (!isRecord(item))
|
|
923
940
|
return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
|
|
@@ -960,6 +977,12 @@ export function redactPresentationData(commandInfo, data) {
|
|
|
960
977
|
return redactStatefulValues(data, new Set(["value"]));
|
|
961
978
|
if (commandInfo.command === "storage")
|
|
962
979
|
return redactStorageData(data);
|
|
980
|
+
if (commandInfo.command === "session" && commandInfo.subcommand === "list")
|
|
981
|
+
return redactStructuredPresentationValue(filterManagedSessionListRows(data));
|
|
982
|
+
if (commandInfo.command === "state" && commandInfo.subcommand === "list")
|
|
983
|
+
return redactStructuredPresentationValue(filterManagedStateListRows(data));
|
|
984
|
+
if (commandInfo.command === "state" && commandInfo.subcommand === "show")
|
|
985
|
+
return redactStatefulValues(data, new Set(["value"]));
|
|
963
986
|
return redactStructuredPresentationValue(data);
|
|
964
987
|
}
|
|
965
988
|
export function formatDiagnosticText(commandInfo, data) {
|
|
@@ -986,7 +1009,7 @@ export function formatDiagnosticText(commandInfo, data) {
|
|
|
986
1009
|
if (commandInfo.command === "frame")
|
|
987
1010
|
return formatFrameText(data);
|
|
988
1011
|
if (commandInfo.command === "state")
|
|
989
|
-
return formatStateText(data);
|
|
1012
|
+
return formatStateText(data, commandInfo.subcommand);
|
|
990
1013
|
if (commandInfo.command === "network" && commandInfo.subcommand === "requests")
|
|
991
1014
|
return formatNetworkRequestsText(data, commandInfo);
|
|
992
1015
|
if (commandInfo.command === "network" && commandInfo.subcommand === "request")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Remove wrapper-owned managed session and state capabilities from caller-visible list results.
|
|
3
|
+
* Responsibilities: Recognize managed list rows recursively and preserve the surrounding upstream result shape.
|
|
4
|
+
* Scope: Presentation filtering only; command authorization lives in managed-session-state-policy.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { containsManagedSessionRestoreKey, isWrapperManagedSessionName } from "../../managed-session-capabilities.js";
|
|
7
|
+
import { isRecord } from "../../parsing.js";
|
|
8
|
+
function isWrapperManagedSessionListItem(item) {
|
|
9
|
+
if (typeof item === "string")
|
|
10
|
+
return isWrapperManagedSessionName(item);
|
|
11
|
+
if (!isRecord(item))
|
|
12
|
+
return false;
|
|
13
|
+
return ["name", "session", "id"].some((key) => isWrapperManagedSessionName(typeof item[key] === "string" ? item[key] : undefined));
|
|
14
|
+
}
|
|
15
|
+
function containsManagedStateCapability(value) {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
return containsManagedSessionRestoreKey(value);
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.some(containsManagedStateCapability);
|
|
20
|
+
return isRecord(value) && Object.values(value).some(containsManagedStateCapability);
|
|
21
|
+
}
|
|
22
|
+
export function filterCallerOwnedSessionListItems(items) {
|
|
23
|
+
return items.filter((item) => !isWrapperManagedSessionListItem(item));
|
|
24
|
+
}
|
|
25
|
+
export function filterCallerOwnedStateListItems(items) {
|
|
26
|
+
return items.filter((item) => !containsManagedStateCapability(item));
|
|
27
|
+
}
|
|
28
|
+
export function filterManagedSessionListRows(data) {
|
|
29
|
+
if (!isRecord(data) || !Array.isArray(data.sessions))
|
|
30
|
+
return data;
|
|
31
|
+
return { ...data, sessions: filterCallerOwnedSessionListItems(data.sessions) };
|
|
32
|
+
}
|
|
33
|
+
export function filterManagedStateListRows(data) {
|
|
34
|
+
if (!isRecord(data))
|
|
35
|
+
return data;
|
|
36
|
+
return Object.fromEntries(Object.entries(data).map(([key, value]) => [
|
|
37
|
+
key,
|
|
38
|
+
(key === "states" || key === "files") && Array.isArray(value)
|
|
39
|
+
? filterCallerOwnedStateListItems(value)
|
|
40
|
+
: value,
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { buildNextToolAction, withOptionalSessionArgs } from "./next-actions.js";
|
|
9
9
|
export const AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS = {
|
|
10
10
|
aboutBlankListTabs: "list-tabs-for-about-blank-recovery",
|
|
11
|
+
connectedSessionGetUrl: "verify-connected-session-url",
|
|
11
12
|
connectedSessionListTabs: "list-connected-session-tabs",
|
|
12
13
|
genericTabDriftListTabs: "list-tabs-for-recovery",
|
|
13
14
|
noActivePageListTabs: "list-tabs-after-no-active-page",
|
|
@@ -60,6 +61,12 @@ export function buildRecoveryNextActions(recovery) {
|
|
|
60
61
|
const sessionArgs = (args) => withOptionalSessionArgs(recovery.sessionName, args);
|
|
61
62
|
if (recovery.kind === "connected-session") {
|
|
62
63
|
return [
|
|
64
|
+
buildNextToolAction({
|
|
65
|
+
args: sessionArgs(["get", "url"]),
|
|
66
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionGetUrl,
|
|
67
|
+
reason: "Verify the attached page URL before any page-content inspection.",
|
|
68
|
+
safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target is a local file page or the URL cannot be verified.",
|
|
69
|
+
}),
|
|
63
70
|
buildNextToolAction({
|
|
64
71
|
args: sessionArgs(["tab", "list"]),
|
|
65
72
|
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionListTabs,
|