pi-agent-browser-native 0.2.77 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/README.md +69 -35
- package/dist/extensions/agent-browser/index.js +911 -121
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
- package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
- package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
- package/dist/extensions/agent-browser/lib/config.js +0 -6
- package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
- package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
- 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 +148 -34
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/parsing.js +0 -7
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
- package/dist/extensions/agent-browser/lib/playbook.js +20 -24
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
- package/dist/extensions/agent-browser/lib/process.js +131 -48
- package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
- package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
- package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
- package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
- package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
- package/dist/extensions/agent-browser/lib/results/network.js +0 -7
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
- package/dist/extensions/agent-browser/lib/results/text.js +0 -7
- package/dist/extensions/agent-browser/lib/runtime.js +119 -52
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/temp.js +0 -7
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +0 -5
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +50 -31
- package/docs/COMMAND_REFERENCE.md +97 -41
- package/docs/ELECTRON.md +3 -19
- package/docs/RELEASE.md +8 -20
- package/docs/REQUIREMENTS.md +8 -6
- package/docs/SUPPORT_MATRIX.md +32 -25
- package/docs/TOOL_CONTRACT.md +107 -64
- package/docs/platform-smoke.md +1 -1
- package/package.json +7 -6
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +8 -7
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
- package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
- package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
- package/dist/extensions/agent-browser/lib/results.js +0 -14
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Resolve and secure the filesystem identity used by automatic managed-session restore.
|
|
3
|
-
* Responsibilities: Bind restore keys to one Git checkout generation, validate trusted HOME ancestry, and prepare owner-only upstream state directories.
|
|
4
|
-
* Scope: Filesystem policy only; argv/session ownership and snapshot retention live in sibling modules.
|
|
5
|
-
*/
|
|
6
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
2
|
import { linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
3
|
import { homedir } from "node:os";
|
|
@@ -10,6 +5,7 @@ import { dirname, isAbsolute, join, parse, resolve, win32 } from "node:path";
|
|
|
10
5
|
import { canonicalizeAgentBrowserNamespace } from "./argv-grammar.js";
|
|
11
6
|
export { isManagedSessionRestoreKey } from "./managed-session-capabilities.js";
|
|
12
7
|
const MANAGED_SESSION_NAME_PREFIX = "piab-r2-";
|
|
8
|
+
const MANAGED_SESSION_FRESH_SUFFIX_PATTERN = /-fresh-[a-f\d]{10}$/i;
|
|
13
9
|
const MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH = 32;
|
|
14
10
|
const PROJECT_GENERATION_MARKER_NAME = "pi-agent-browser-project-generation-v1.json";
|
|
15
11
|
const PROJECT_GENERATION_MARKER_MAX_BYTES = 1_024;
|
|
@@ -20,7 +16,7 @@ function isAbsoluteHome(path, platform) {
|
|
|
20
16
|
function currentUid() {
|
|
21
17
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
22
18
|
}
|
|
23
|
-
function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
19
|
+
function isTrustedPosixDirectory(path, requireCurrentOwner, platform = process.platform) {
|
|
24
20
|
const uid = currentUid();
|
|
25
21
|
if (uid === undefined)
|
|
26
22
|
return false;
|
|
@@ -37,11 +33,13 @@ function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
|
37
33
|
}
|
|
38
34
|
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
39
35
|
return false;
|
|
40
|
-
|
|
36
|
+
const androidSystemAncestor = platform === "android" && (cursor === "/data" || cursor === "/data/data") && entry.uid === 1000 && (entry.mode & 0o002) === 0;
|
|
37
|
+
const androidAppDirectory = platform === "android" && entry.uid === uid && entry.gid === uid && (entry.mode & 0o002) === 0;
|
|
38
|
+
if (!androidSystemAncestor && entry.uid !== 0 && entry.uid !== uid)
|
|
41
39
|
return false;
|
|
42
40
|
const writableByOthers = (entry.mode & 0o022) !== 0;
|
|
43
41
|
const rootOwnedStickyDirectory = entry.uid === 0 && (entry.mode & 0o1000) !== 0;
|
|
44
|
-
if (writableByOthers && !rootOwnedStickyDirectory)
|
|
42
|
+
if (writableByOthers && !rootOwnedStickyDirectory && !androidSystemAncestor && !androidAppDirectory)
|
|
45
43
|
return false;
|
|
46
44
|
}
|
|
47
45
|
try {
|
|
@@ -61,7 +59,7 @@ export function resolveManagedSessionRestoreHome(parentEnv, platform = process.p
|
|
|
61
59
|
return candidate;
|
|
62
60
|
try {
|
|
63
61
|
const canonical = realpathSync(candidate);
|
|
64
|
-
return isTrustedPosixDirectory(canonical, true) ? canonical : undefined;
|
|
62
|
+
return isTrustedPosixDirectory(canonical, true, platform) ? canonical : undefined;
|
|
65
63
|
}
|
|
66
64
|
catch {
|
|
67
65
|
return undefined;
|
|
@@ -143,12 +141,15 @@ function readProjectGenerationMarker(path, platform) {
|
|
|
143
141
|
return undefined;
|
|
144
142
|
}
|
|
145
143
|
}
|
|
146
|
-
function getDirectoryFilesystemIdentity(path) {
|
|
144
|
+
function getDirectoryFilesystemIdentity(path, platform) {
|
|
147
145
|
try {
|
|
148
146
|
const entry = statSync(path, { bigint: true });
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
if (!entry.isDirectory() || entry.dev <= 0n || entry.ino <= 0n)
|
|
148
|
+
return undefined;
|
|
149
|
+
// ponytail: Android reports mutable ctime as birthtime; use statx birthtime/inode generation when Node exposes either reliably.
|
|
150
|
+
return platform === "android"
|
|
151
|
+
? `${entry.dev}:${entry.ino}`
|
|
152
|
+
: entry.birthtimeNs > 0n ? `${entry.dev}:${entry.ino}:${entry.birthtimeNs}` : undefined;
|
|
152
153
|
}
|
|
153
154
|
catch {
|
|
154
155
|
return undefined;
|
|
@@ -162,13 +163,13 @@ function resolveManagedSessionRestoreProjectCheckout(cwd, platform) {
|
|
|
162
163
|
catch {
|
|
163
164
|
return undefined;
|
|
164
165
|
}
|
|
165
|
-
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false))
|
|
166
|
+
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false, platform))
|
|
166
167
|
return undefined;
|
|
167
168
|
const checkout = resolveGitCheckout(canonicalCwd, platform);
|
|
168
169
|
if (!checkout)
|
|
169
170
|
return undefined;
|
|
170
|
-
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true)
|
|
171
|
-
|| !isTrustedPosixDirectory(checkout.gitDirectory, true)))
|
|
171
|
+
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true, platform)
|
|
172
|
+
|| !isTrustedPosixDirectory(checkout.gitDirectory, true, platform)))
|
|
172
173
|
return undefined;
|
|
173
174
|
return { canonicalCwd, ...checkout };
|
|
174
175
|
}
|
|
@@ -180,8 +181,8 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
180
181
|
if (!checkout)
|
|
181
182
|
return undefined;
|
|
182
183
|
const { canonicalCwd } = checkout;
|
|
183
|
-
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory);
|
|
184
|
-
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory);
|
|
184
|
+
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory, platform);
|
|
185
|
+
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory, platform);
|
|
185
186
|
if (!gitFilesystemIdentity || !worktreeFilesystemIdentity)
|
|
186
187
|
return undefined;
|
|
187
188
|
const markerPath = join(checkout.gitDirectory, PROJECT_GENERATION_MARKER_NAME);
|
|
@@ -197,22 +198,35 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
197
198
|
projectGenerationCache.delete(canonicalCwd);
|
|
198
199
|
try {
|
|
199
200
|
if (!marker) {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
const content = JSON.stringify({ id: randomUUID(), version: 1 });
|
|
202
|
+
if (platform === "android") {
|
|
203
|
+
// ponytail: Android denies hard links in app storage; use renameat2(RENAME_NOREPLACE) if Node exposes it.
|
|
203
204
|
try {
|
|
204
|
-
|
|
205
|
+
writeFileSync(markerPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
205
206
|
}
|
|
206
207
|
catch (error) {
|
|
207
208
|
if (error.code !== "EEXIST")
|
|
208
209
|
return undefined;
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
|
-
|
|
212
|
+
else {
|
|
213
|
+
const candidatePath = `${markerPath}.candidate-${process.pid}-${randomUUID()}`;
|
|
212
214
|
try {
|
|
213
|
-
|
|
215
|
+
writeFileSync(candidatePath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
216
|
+
try {
|
|
217
|
+
linkSync(candidatePath, markerPath);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code !== "EEXIST")
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
try {
|
|
226
|
+
unlinkSync(candidatePath);
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
214
229
|
}
|
|
215
|
-
catch { }
|
|
216
230
|
}
|
|
217
231
|
marker = readProjectGenerationMarker(markerPath, platform);
|
|
218
232
|
}
|
|
@@ -236,16 +250,23 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
236
250
|
export function hasManagedSessionRestoreProjectIdentity(cwd) {
|
|
237
251
|
return resolveProjectGenerationIdentity(cwd) !== undefined;
|
|
238
252
|
}
|
|
239
|
-
/**
|
|
240
|
-
export function
|
|
253
|
+
/** Keep fresh rotations from one Pi transcript in one private upstream restore pool. */
|
|
254
|
+
export function getManagedSessionRestoreScope(sessionName) {
|
|
255
|
+
return sessionName.replace(MANAGED_SESSION_FRESH_SUFFIX_PATTERN, "");
|
|
256
|
+
}
|
|
257
|
+
/** Stable for one Pi transcript and checkout generation; isolated from other concurrent transcripts. */
|
|
258
|
+
export function createManagedSessionRestoreKey(cwd, restoreScope = "", platform = process.platform) {
|
|
241
259
|
let canonicalCwd = resolve(cwd);
|
|
242
260
|
try {
|
|
243
261
|
canonicalCwd = realpathSync(canonicalCwd);
|
|
244
262
|
}
|
|
245
263
|
catch { }
|
|
246
|
-
const identity = resolveProjectGenerationIdentity(canonicalCwd);
|
|
264
|
+
const identity = resolveProjectGenerationIdentity(canonicalCwd, platform);
|
|
247
265
|
const material = identity ?? `unavailable:${canonicalCwd}`;
|
|
248
|
-
const digest = createHash("sha256")
|
|
266
|
+
const digest = createHash("sha256")
|
|
267
|
+
.update(`restore-v3:${material}:scope:${restoreScope}`)
|
|
268
|
+
.digest("hex")
|
|
269
|
+
.slice(0, MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH);
|
|
249
270
|
return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
|
|
250
271
|
}
|
|
251
272
|
function hasValidEncryptionKey(parentEnv) {
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Keep wrapper-side navigation policy parsing and evaluation small and explicit.
|
|
3
|
-
* Responsibilities: Parse allowed-domain argv values and detect final-page host escapes.
|
|
4
|
-
* Scope: Wrapper diagnostics only; upstream remains responsible for browser-time enforcement.
|
|
5
|
-
*/
|
|
6
1
|
function normalizeDomainEntry(value) {
|
|
7
2
|
let candidate = value.trim().toLowerCase();
|
|
8
3
|
if (!candidate)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
|
|
1
2
|
// Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
|
|
2
3
|
export function parseBatchCommandArgument(command) {
|
|
3
4
|
const tokens = [];
|
|
@@ -36,20 +37,20 @@ export function parseBatchCommandArgument(command) {
|
|
|
36
37
|
function validateUserBatchStep(step, index) {
|
|
37
38
|
if (!Array.isArray(step)) {
|
|
38
39
|
return {
|
|
39
|
-
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens
|
|
40
|
+
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
|
|
40
41
|
ok: false,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
if (step.length === 0) {
|
|
44
45
|
return {
|
|
45
|
-
error: `agent_browser batch stdin step ${index} must not be empty
|
|
46
|
+
error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
|
|
46
47
|
ok: false,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
50
51
|
if (invalidTokenIndex !== -1) {
|
|
51
52
|
return {
|
|
52
|
-
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string
|
|
53
|
+
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.${BATCH_STDIN_EXAMPLE}`,
|
|
53
54
|
ok: false,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -62,13 +63,13 @@ export function parseBatchStdinJsonArray(stdin) {
|
|
|
62
63
|
try {
|
|
63
64
|
const parsed = JSON.parse(stdin);
|
|
64
65
|
if (!Array.isArray(parsed)) {
|
|
65
|
-
return { error:
|
|
66
|
+
return { error: `agent_browser batch stdin must be a JSON array of command steps.${BATCH_STDIN_EXAMPLE}` };
|
|
66
67
|
}
|
|
67
68
|
return { steps: parsed };
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
71
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
-
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
72
|
+
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}.${BATCH_STDIN_EXAMPLE}` };
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
export function parseUserBatchStdin(stdin) {
|
|
@@ -86,6 +87,26 @@ export function parseUserBatchStdin(stdin) {
|
|
|
86
87
|
}
|
|
87
88
|
return { steps };
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The batch steps upstream will actually execute: run_batch uses raw batch
|
|
92
|
+
* arguments exclusively when any exist and reads stdin only otherwise.
|
|
93
|
+
* Upstream filters only the exact `--bail` token, so an equals form such as
|
|
94
|
+
* `--bail=true` stays a raw command (an unknown-command row) and keeps stdin
|
|
95
|
+
* ignored.
|
|
96
|
+
*/
|
|
97
|
+
export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
98
|
+
if (commandTokens[0] !== "batch")
|
|
99
|
+
return [];
|
|
100
|
+
const argumentSteps = commandTokens.slice(1).flatMap((command) => {
|
|
101
|
+
if (command === "--bail")
|
|
102
|
+
return [];
|
|
103
|
+
const step = parseBatchCommandArgument(command).step;
|
|
104
|
+
return step ? [step] : [];
|
|
105
|
+
});
|
|
106
|
+
if (argumentSteps.length > 0)
|
|
107
|
+
return argumentSteps;
|
|
108
|
+
return parseUserBatchStdin(stdin).steps ?? [];
|
|
109
|
+
}
|
|
89
110
|
export function parseValidBatchStepEntries(stdin) {
|
|
90
111
|
const parsed = parseBatchStdinJsonArray(stdin);
|
|
91
112
|
if (parsed.error || parsed.steps === undefined)
|
|
@@ -1,46 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
|
+
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
3
5
|
const SCREENSHOT_BOOLEAN_FLAGS = new Set(["--annotate", "--full", "-f"]);
|
|
4
6
|
const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
|
|
5
|
-
const SCREENSHOT_IMAGE_EXTENSIONS =
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
8
|
+
function isSingleScreenshotPathToken(token) {
|
|
9
|
+
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
10
|
+
if (token.startsWith("#") || token.startsWith("@") || (token.startsWith(".") && !explicitlyRelative && !token.includes("/")))
|
|
11
|
+
return false;
|
|
12
|
+
return explicitlyRelative || token.includes("/") || SCREENSHOT_IMAGE_EXTENSIONS.some((extension) => token.endsWith(extension));
|
|
13
|
+
}
|
|
14
|
+
function getScreenshotPositionalIndices(commandTokens) {
|
|
15
|
+
if (commandTokens[0] !== "screenshot")
|
|
16
|
+
return [];
|
|
17
|
+
const positionalIndices = [];
|
|
18
|
+
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
19
|
+
const token = commandTokens[index];
|
|
20
|
+
if (SCREENSHOT_VALUE_FLAGS.has(token)) {
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (SCREENSHOT_BOOLEAN_FLAGS.has(token))
|
|
25
|
+
continue;
|
|
26
|
+
positionalIndices.push(index);
|
|
27
|
+
}
|
|
28
|
+
return positionalIndices;
|
|
9
29
|
}
|
|
10
30
|
export function getScreenshotPathTokenIndex(commandTokens) {
|
|
11
|
-
|
|
31
|
+
const positionalIndices = getScreenshotPositionalIndices(commandTokens);
|
|
32
|
+
if (positionalIndices.length === 0)
|
|
12
33
|
return undefined;
|
|
34
|
+
const candidateIndex = positionalIndices.length >= 2 ? positionalIndices[1] : positionalIndices[0];
|
|
35
|
+
const candidate = commandTokens[candidateIndex];
|
|
36
|
+
if (positionalIndices.length >= 2 || isSingleScreenshotPathToken(candidate)) {
|
|
37
|
+
return candidateIndex;
|
|
13
38
|
}
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
42
|
+
function getDiffScreenshotOutputPath(commandTokens) {
|
|
43
|
+
let outputPath;
|
|
44
|
+
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
16
45
|
const token = commandTokens[index];
|
|
17
|
-
if (token
|
|
18
|
-
|
|
19
|
-
|
|
46
|
+
if (!DIFF_SCREENSHOT_VALUE_FLAGS.has(token))
|
|
47
|
+
continue;
|
|
48
|
+
const value = commandTokens[index + 1];
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (token === "-o" || token === "--output")
|
|
52
|
+
outputPath = value;
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return outputPath;
|
|
56
|
+
}
|
|
57
|
+
function foldArtifactPath(path, platform) {
|
|
58
|
+
return foldAgentBrowserFilesystemIdentity(path, platform);
|
|
59
|
+
}
|
|
60
|
+
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
61
|
+
let cursor = absolutePath;
|
|
62
|
+
const suffix = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const canonicalPath = join(realpathSync.native(cursor), ...suffix);
|
|
66
|
+
try {
|
|
67
|
+
const stats = statSync(canonicalPath, { bigint: true });
|
|
68
|
+
if (stats.ino > 0n)
|
|
69
|
+
return `inode:${stats.dev}:${stats.ino}`;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// The destination does not exist yet; canonical ancestry still catches aliases.
|
|
20
73
|
}
|
|
21
|
-
|
|
74
|
+
return foldArtifactPath(canonicalPath, platform);
|
|
22
75
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
catch {
|
|
77
|
+
let symlinkTarget;
|
|
78
|
+
try {
|
|
79
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
80
|
+
symlinkTarget = resolve(dirname(cursor), readlinkSync(cursor));
|
|
28
81
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
catch { }
|
|
83
|
+
if (symlinkTarget) {
|
|
84
|
+
if (seenSymlinks.has(cursor))
|
|
85
|
+
throw new Error(`Artifact destination contains a symlink loop: ${absolutePath}`);
|
|
86
|
+
if (seenSymlinks.size >= 32)
|
|
87
|
+
throw new Error(`Artifact destination has too many symlink hops: ${absolutePath}`);
|
|
88
|
+
seenSymlinks.add(cursor);
|
|
89
|
+
return canonicalizeArtifactPath(join(symlinkTarget, ...suffix), platform, seenSymlinks);
|
|
33
90
|
}
|
|
91
|
+
const parent = dirname(cursor);
|
|
92
|
+
if (parent === cursor)
|
|
93
|
+
return foldArtifactPath(absolutePath, platform);
|
|
94
|
+
suffix.unshift(basename(cursor));
|
|
95
|
+
cursor = parent;
|
|
34
96
|
}
|
|
35
|
-
positionalIndices.push(index);
|
|
36
97
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
}
|
|
99
|
+
export function canonicalizeExplicitArtifactDestination(cwd, destination, platform = process.platform) {
|
|
100
|
+
return canonicalizeArtifactPath(resolve(cwd, destination), platform, new Set());
|
|
101
|
+
}
|
|
102
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
103
|
+
const command = commandTokens[0];
|
|
104
|
+
const subcommand = commandTokens[1];
|
|
105
|
+
if (command === "screenshot") {
|
|
106
|
+
const index = getScreenshotPathTokenIndex(commandTokens);
|
|
107
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
44
108
|
}
|
|
109
|
+
if (command === "download")
|
|
110
|
+
return commandTokens[2];
|
|
111
|
+
if (command === "pdf")
|
|
112
|
+
return commandTokens[1];
|
|
113
|
+
if (command === "wait")
|
|
114
|
+
return parseWaitCommandTokens(commandTokens).downloadPath;
|
|
115
|
+
if (command === "state" && subcommand === "save")
|
|
116
|
+
return commandTokens[2];
|
|
117
|
+
if (command === "diff" && subcommand === "screenshot")
|
|
118
|
+
return getDiffScreenshotOutputPath(commandTokens);
|
|
119
|
+
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
120
|
+
return commandTokens[3];
|
|
121
|
+
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
122
|
+
return commandTokens[2];
|
|
123
|
+
if (command === "record" && (subcommand === "start" || subcommand === "restart"))
|
|
124
|
+
return commandTokens[2];
|
|
45
125
|
return undefined;
|
|
46
126
|
}
|
|
@@ -245,6 +245,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
245
245
|
const result = getEvalResultRecord(data);
|
|
246
246
|
if (!result)
|
|
247
247
|
return undefined;
|
|
248
|
+
options.probe.cleaned = true;
|
|
248
249
|
const status = typeof result.status === "string" ? result.status : undefined;
|
|
249
250
|
if (status !== "no-native-event-observed")
|
|
250
251
|
return undefined;
|
|
@@ -264,7 +265,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
267
|
export async function cleanupClickDispatchProbe(options) {
|
|
267
|
-
if (!options.probe || !options.sessionName)
|
|
268
|
+
if (!options.probe || options.probe.cleaned || !options.sessionName)
|
|
268
269
|
return;
|
|
269
270
|
await runSessionCommandData({
|
|
270
271
|
args: ["eval", "--stdin"],
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
4
|
-
import { boundElectronProbeString } from "../../electron/
|
|
4
|
+
import { boundElectronProbeString } from "../../electron/cdp.js";
|
|
5
5
|
import { executableExistsOnPath } from "../../executable-path.js";
|
|
6
6
|
import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
|
|
7
7
|
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
8
8
|
import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
9
9
|
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
|
|
10
|
-
import { extractRefSnapshotFromData, normalizeComparableUrl } from "../../session-page-state.js";
|
|
11
|
-
import { redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
10
|
+
import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
|
|
11
|
+
import { extractUpstreamCommandTokens, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
12
12
|
import { isRecord } from "../../parsing.js";
|
|
13
13
|
import { getManagedSessionStateAccessValidationError, isFileUrl } from "../../managed-session-state-policy.js";
|
|
14
14
|
import { extractBatchResultCommand, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, runSessionCommandData, } from "./session-state.js";
|
|
@@ -22,8 +22,14 @@ export async function collectNavigationSummary(options) {
|
|
|
22
22
|
const url = extractStringResultField(await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "url");
|
|
23
23
|
if (!url || !/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
24
24
|
return undefined;
|
|
25
|
-
if (isFileUrl(url))
|
|
25
|
+
if (isFileUrl(url) || isAboutBlankUrl(url))
|
|
26
26
|
return { url };
|
|
27
|
+
// Reuse the title already observed for this exact URL instead of spending a second probe. Titles can
|
|
28
|
+
// change without a URL change on SPAs, but this summary is only a "last observed" page label; the URL
|
|
29
|
+
// stays live-probed on every call.
|
|
30
|
+
if (options.priorTarget?.title && normalizeComparableUrl(options.priorTarget.url) === normalizeComparableUrl(url)) {
|
|
31
|
+
return { title: options.priorTarget.title, url };
|
|
32
|
+
}
|
|
27
33
|
const title = extractStringResultField(await runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "title");
|
|
28
34
|
return { title, url };
|
|
29
35
|
}
|
|
@@ -390,14 +396,14 @@ function isBroadGetTextSelector(selector) {
|
|
|
390
396
|
return normalized === "body" || normalized === "html" || normalized === ":root" || normalized === "*" || normalized === "main" || normalized === "div" || normalized === "section" || normalized === "article" || /^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
|
|
391
397
|
}
|
|
392
398
|
function getElectronTextScopeContext(options) {
|
|
393
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
399
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
394
400
|
if (!record)
|
|
395
401
|
return undefined;
|
|
396
402
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
397
403
|
return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
398
404
|
}
|
|
399
405
|
export function getSourceLookupElectronContext(options) {
|
|
400
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
406
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
401
407
|
if (!record)
|
|
402
408
|
return undefined;
|
|
403
409
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
@@ -469,9 +475,11 @@ export function formatEvalResultWarningText(warning) {
|
|
|
469
475
|
return warning ? `Eval result warning: ${warning.reason} ${warning.suggestion}` : undefined;
|
|
470
476
|
}
|
|
471
477
|
export async function getArtifactCleanupGuidance(options) {
|
|
472
|
-
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest
|
|
478
|
+
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest)
|
|
473
479
|
return undefined;
|
|
474
480
|
const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
|
|
481
|
+
if (explicitEntries.length === 0)
|
|
482
|
+
return undefined;
|
|
475
483
|
const explicitArtifactPaths = [];
|
|
476
484
|
const seenPaths = new Set();
|
|
477
485
|
for (const entry of explicitEntries) {
|
|
@@ -490,16 +498,15 @@ export async function getArtifactCleanupGuidance(options) {
|
|
|
490
498
|
seenPaths.add(displayPath);
|
|
491
499
|
explicitArtifactPaths.push(displayPath);
|
|
492
500
|
}
|
|
501
|
+
if (explicitArtifactPaths.length === 0)
|
|
502
|
+
return undefined;
|
|
493
503
|
return { explicitArtifactPaths, note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean existing paths with host file tools when no longer needed.", owner: "host-file-tools", summary: formatSessionArtifactRetentionSummary(options.manifest) };
|
|
494
504
|
}
|
|
495
505
|
export function formatArtifactCleanupGuidanceText(guidance) {
|
|
496
|
-
if (!guidance)
|
|
506
|
+
if (!guidance || guidance.explicitArtifactPaths.length === 0)
|
|
497
507
|
return undefined;
|
|
498
508
|
const explicitCount = guidance.explicitArtifactPaths.length;
|
|
499
|
-
|
|
500
|
-
? "No existing explicit artifact paths were found in the recent manifest."
|
|
501
|
-
: `${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths.`;
|
|
502
|
-
return `Artifact lifecycle: ${explicitSummary} Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
509
|
+
return `Artifact lifecycle: ${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths. Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
503
510
|
}
|
|
504
511
|
async function collectManagedSessionCommandData(options) {
|
|
505
512
|
try {
|
|
@@ -718,24 +725,18 @@ function getLastPositionalToken(args, startIndex = 1) {
|
|
|
718
725
|
return undefined;
|
|
719
726
|
}
|
|
720
727
|
function getTimeoutStepArtifactPath(args) {
|
|
721
|
-
const
|
|
728
|
+
const commandArgs = extractUpstreamCommandTokens(args);
|
|
729
|
+
const [command] = commandArgs;
|
|
722
730
|
if (command === "screenshot") {
|
|
723
|
-
const index = getScreenshotPathTokenIndex(
|
|
724
|
-
return index === undefined ? undefined :
|
|
731
|
+
const index = getScreenshotPathTokenIndex(commandArgs);
|
|
732
|
+
return index === undefined ? undefined : commandArgs[index];
|
|
725
733
|
}
|
|
726
734
|
if (command === "pdf")
|
|
727
|
-
return getLastPositionalToken(
|
|
735
|
+
return getLastPositionalToken(commandArgs);
|
|
728
736
|
if (command === "download")
|
|
729
|
-
return getLastPositionalToken(
|
|
730
|
-
if (command === "wait")
|
|
731
|
-
|
|
732
|
-
if (inlineDownload)
|
|
733
|
-
return inlineDownload.slice("--download=".length) || undefined;
|
|
734
|
-
const downloadIndex = args.indexOf("--download");
|
|
735
|
-
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
736
|
-
if (downloadPath && !downloadPath.startsWith("-"))
|
|
737
|
-
return downloadPath;
|
|
738
|
-
}
|
|
737
|
+
return getLastPositionalToken(commandArgs, 2);
|
|
738
|
+
if (command === "wait")
|
|
739
|
+
return parseWaitCommandTokens(commandArgs).downloadPath;
|
|
739
740
|
return undefined;
|
|
740
741
|
}
|
|
741
742
|
async function statTimeoutArtifactPath(absolutePath) {
|