pi-agent-browser-native 0.6.16 → 0.7.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +59 -165
  3. package/dist/extensions/agent-browser/index.js +634 -501
  4. package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
  6. package/dist/extensions/agent-browser/lib/execution-cwd.js +66 -0
  7. package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
  8. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
  10. package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
  11. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +41 -24
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
  21. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
  22. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
  23. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
  24. package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
  25. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
  26. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
  27. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
  28. package/dist/extensions/agent-browser/lib/playbook.js +46 -25
  29. package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
  30. package/dist/extensions/agent-browser/lib/process.js +16 -13
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
  32. package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
  33. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
  34. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
  35. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
  36. package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
  37. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
  38. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
  39. package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
  40. package/dist/extensions/agent-browser/lib/runtime.js +18 -6
  41. package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
  42. package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
  43. package/dist/extensions/agent-browser/script-worker.js +4 -1
  44. package/docs/ARCHITECTURE.md +47 -61
  45. package/docs/COMMAND_REFERENCE.md +78 -137
  46. package/docs/ELECTRON.md +51 -49
  47. package/docs/RELEASE.md +31 -15
  48. package/docs/REQUIREMENTS.md +18 -14
  49. package/docs/SUPPORT_MATRIX.md +33 -15
  50. package/docs/TOOL_CONTRACT.md +218 -401
  51. package/package.json +4 -4
  52. package/scripts/doctor.mjs +2 -2
@@ -1,149 +1,153 @@
1
1
  import { JsonSchema } from "../json-schema.js";
2
2
  import { StringEnum as localStringEnum } from "../string-enum-schema.js";
3
- import { AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES } from "./script.js";
4
- import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS, } from "../electron/discovery.js";
5
- import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_JOB_STEP_ACTIONS, AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
6
- // Keep descriptions terse: Pi sends this schema every turn; workflows belong in prompt guidance and docs.
3
+ import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS } from "../electron/discovery.js";
4
+ import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
5
+ export const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
6
+ export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
7
+ function outputProperties(Type) {
8
+ return {
9
+ outputPath: Type.Optional(Type.String({ description: "Result-data path, separate from browser artifact paths.", minLength: 1 })),
10
+ timeoutMs: Type.Optional(Type.Integer({ description: "Timeout in ms; exceed explicit waits.", minimum: 1 })),
11
+ };
12
+ }
13
+ function sessionModeProperty(Type, StringEnum) {
14
+ return Type.Optional(StringEnum(["auto", "fresh"], {
15
+ description: "Native configured sessions win; auto reuses the browser, fresh intentionally launches a separate managed browser.",
16
+ default: DEFAULT_SESSION_MODE,
17
+ }));
18
+ }
19
+ // Keep the always-on schemas small; specialized guidance belongs to each advanced tool.
7
20
  export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
8
- return Type.Object({
9
- script: Type.Optional(Type.String({
10
- description: "One-shot JavaScript orchestration with async browser({ args, stdin?, timeoutMs? }) and emit(value); isolated session and no host access.",
11
- maxLength: AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES,
12
- })),
13
- args: Type.Optional(Type.Array(Type.String(), {
14
- description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes. Input: type <selector> <text>, or keyboard type <text> at current focus. Wait duration: wait <ms> (no --time). Artifacts: screenshot [selector] [path] [--full/-f]; record start <path> [url]; record restart <path> [url]; record stop. Paths are positional (no --path); use --full, not --full-page.",
15
- minItems: 1,
16
- })),
17
- semanticAction: Type.Optional(Type.Object({
18
- action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
19
- locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator for check/click/fill; select supports role or label." })),
20
- value: Type.Optional(Type.String({ description: "Locator value or one select option; for select by label, this is the label text." })),
21
- values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
22
- selector: Type.Optional(Type.String({ description: "Direct selector or @ref." })),
23
- text: Type.Optional(Type.String({ description: "Fill text." })),
24
- role: Type.Optional(Type.String({ description: "Role for locator=role; select needs combobox or listbox." })),
25
- name: Type.Optional(Type.String({ description: "Accessible name." })),
26
- session: Type.Optional(Type.String({ description: "Upstream session name." })),
27
- }, {
28
- additionalProperties: false,
29
- // Pi normalizes optional nulls through properties, not union branches.
30
- anyOf: [
31
- Type.Object({
32
- action: StringEnum(["select"]),
33
- locator: Type.Optional(StringEnum(["role", "label"])),
34
- }, { not: { required: ["text"] } }),
35
- Type.Object({
36
- action: StringEnum(["check", "click", "fill"]),
37
- }, { not: { required: ["values"] } }),
38
- ],
39
- description: "Stable locator or direct-selector action. values only with action=select.",
40
- })),
41
- qa: Type.Optional(Type.Union([
42
- Type.Object({
43
- attached: Type.Literal(true),
44
- expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
45
- expectedSelector: Type.Optional(Type.String()),
46
- screenshotPath: Type.Optional(Type.String()),
47
- checkConsole: Type.Optional(Type.Boolean()),
48
- checkErrors: Type.Optional(Type.Boolean()),
49
- checkNetwork: Type.Optional(Type.Boolean()),
50
- loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES)),
51
- }, { additionalProperties: false }),
52
- Type.Object({
53
- url: Type.String(),
54
- attached: Type.Optional(Type.Literal(false)),
55
- expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
56
- expectedSelector: Type.Optional(Type.String()),
57
- screenshotPath: Type.Optional(Type.String()),
58
- checkConsole: Type.Optional(Type.Boolean()),
59
- checkErrors: Type.Optional(Type.Boolean()),
60
- checkNetwork: Type.Optional(Type.Boolean()),
61
- loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES)),
62
- }, { additionalProperties: false }),
63
- ], { description: "QA a URL or current session (attached=true). Default readiness: domcontentloaded; use networkidle only without long-lived requests. URL diagnostics default on, attached diagnostics off." })),
64
- sourceLookup: Type.Optional(Type.Object({
65
- selector: Type.Optional(Type.String({ description: "Visible selector or @ref." })),
66
- reactFiberId: Type.Optional(Type.String({ description: "React fiber id; requires --enable react-devtools." })),
67
- componentName: Type.Optional(Type.String({ description: "Component for local source search." })),
68
- includeDomHints: Type.Optional(Type.Boolean({ description: "Inspect DOM source hints; default true." })),
69
- maxWorkspaceFiles: Type.Optional(Type.Number({ description: "Source scan cap; default 2000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
70
- }, { additionalProperties: false, description: "EXPERIMENTAL UI-to-source candidates; not guaranteed mappings." })),
71
- networkSourceLookup: Type.Optional(Type.Object({
72
- filter: Type.Optional(Type.String({ description: "Network request filter." })),
73
- namespace: Type.Optional(Type.String()),
74
- requestId: Type.Optional(Type.String({ description: "Request id to inspect." })),
75
- session: Type.Optional(Type.String()),
76
- url: Type.Optional(Type.String({ description: "Failed URL or fragment." })),
77
- maxWorkspaceFiles: Type.Optional(Type.Number({ description: "Source scan cap; default 2000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
78
- }, { additionalProperties: false, description: "EXPERIMENTAL failed-request-to-source candidates; not proof." })),
79
- electron: Type.Optional(Type.Union([
80
- Type.Object({
81
- action: StringEnum(["list"]),
82
- query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
83
- maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, values over ${ELECTRON_DISCOVERY_MAX_RESULTS} are clamped.`, minimum: 1 })),
84
- }, { additionalProperties: false }),
85
- Type.Object({
86
- action: StringEnum(["launch"]),
87
- appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
88
- appName: Type.Optional(Type.String({ description: "Name from electron.list.", minLength: 1 })),
89
- bundleId: Type.Optional(Type.String({ description: "Bundle id from electron.list.", minLength: 1 })),
90
- executablePath: Type.Optional(Type.String({ description: "Executable path.", minLength: 1 })),
91
- appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
92
- handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
93
- targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
94
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
95
- allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
96
- deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
97
- }, { additionalProperties: false }),
98
- Type.Object({
99
- action: StringEnum(["status", "cleanup"]),
100
- launchId: Type.String({ description: "Tracked launch id.", minLength: 1 }),
101
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
102
- }, { additionalProperties: false }),
103
- Type.Object({
104
- action: StringEnum(["status", "cleanup"]),
105
- all: Type.Literal(true),
106
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
107
- }, { additionalProperties: false }),
108
- Type.Object({
109
- action: StringEnum(["status", "cleanup"]),
110
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
111
- }, { additionalProperties: false }),
112
- Type.Object({
113
- action: StringEnum(["probe"]),
114
- launchId: Type.Optional(Type.String({ description: "Tracked launch id.", minLength: 1 })),
115
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
116
- }, { additionalProperties: false }),
117
- ], { description: "Electron discovery, isolated-profile launch, status, probe, or cleanup. Launch defaults: handoff=snapshot, targetType=page; deny wins over allow; lifecycle/debug appArgs rejected." })),
118
- job: Type.Optional(Type.Object({
119
- failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure; default true." })),
120
- steps: Type.Array(Type.Object({
121
- action: StringEnum(AGENT_BROWSER_JOB_STEP_ACTIONS),
122
- url: Type.Optional(Type.String({ description: "Open URL, or assertUrl exact URL/glob that distinguishes the expected state from the starting page." })),
123
- loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES, { description: "Readiness wait after open." })),
124
- selector: Type.Optional(Type.String({ description: "Selector or @ref." })),
125
- locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator when selector is omitted." })),
126
- role: Type.Optional(Type.String({ description: "Role locator." })),
127
- name: Type.Optional(Type.String({ description: "Accessible name." })),
128
- text: Type.Optional(Type.String({ description: "Fill/type text; assertText uses only text, not selector/locator fields." })),
129
- value: Type.Optional(Type.String({ description: "Select option or locator value." })),
130
- values: Type.Optional(Type.Array(Type.String(), { description: "Select options.", minItems: 1 })),
131
- path: Type.Optional(Type.String({ description: "Download or screenshot path." })),
132
- delayMs: Type.Optional(Type.Integer({ description: `Per-character type delay; text is capped at ${AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS} characters.`, minimum: 1 })),
133
- press: Type.Optional(Type.String({ description: "Key to press after typing." })),
134
- milliseconds: Type.Optional(Type.Number({ description: "Wait duration in milliseconds." })),
135
- }, { additionalProperties: false }), { minItems: 1 }),
136
- }, { additionalProperties: false, description: "Constrained multi-step batch. Clicks can stale later refs; split and re-snapshot before using them." })),
137
- stdin: Type.Optional(Type.String({ description: "For batch, a JSON array of token arrays, e.g. [[\"get\",\"title\"]]. Raw text only for eval --stdin or auth save --password-stdin; unavailable with structured modes and electron." })),
138
- outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result-data path; keep it distinct from screenshot, download, recording, and other browser artifact destinations.", minLength: 1 })),
139
- timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. electron.list has no configurable timeout; other Electron actions use electron.timeoutMs.", minimum: 1 })),
140
- sessionMode: Type.Optional(StringEnum(["auto", "fresh"], {
141
- description: "Native configured sessions win; otherwise auto reuses the managed session and fresh starts a new managed browser for launch-only flags.",
142
- default: DEFAULT_SESSION_MODE,
143
- })),
21
+ return Type.Unsafe(Type.Object({
22
+ args: Type.Array(Type.String(), { description: "Native agent-browser argv without the binary or shell syntax. Fixed sequences: batch --bail with JSON-array stdin.", minItems: 1 }),
23
+ stdin: Type.Optional(Type.String({ description: "Native batch JSON, eval --stdin source, or auth save --password-stdin text." })),
24
+ ...outputProperties(Type),
25
+ sessionMode: sessionModeProperty(Type, StringEnum),
26
+ }, { additionalProperties: false }));
27
+ }
28
+ export function createAgentBrowserCodeParamsSchema(Type = JsonSchema) {
29
+ return Type.Unsafe(Type.Object({
30
+ code: Type.String({ description: "JavaScript with await browser({args, stdin?, timeoutMs?}), emit(JSON), and emitImage(image handle). Fresh JS context; persistent browser.", minLength: 1, maxLength: 65_536 }),
31
+ session: Type.Optional(Type.String({ description: "Explicit native session; omitted uses the ordinary default browser.", minLength: 1 })),
32
+ namespace: Type.Optional(Type.String({ description: "Native namespace; empty string selects the default namespace." })),
33
+ ...outputProperties(Type),
34
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: AGENT_BROWSER_CODE_MAX_TIMEOUT_MS })),
35
+ }, { additionalProperties: false }));
36
+ }
37
+ export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
38
+ return Type.Unsafe(Type.Object({
39
+ action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
40
+ locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Select supports role or label." })),
41
+ value: Type.Optional(Type.String({ description: "Locator value or select option; label text for select by label." })),
42
+ values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
43
+ selector: Type.Optional(Type.String({ description: "Direct selector or current @ref." })),
44
+ text: Type.Optional(Type.String({ description: "Fill text." })),
45
+ role: Type.Optional(Type.String({ description: "Role locator; select needs combobox or listbox." })),
46
+ name: Type.Optional(Type.String({ description: "Accessible name." })),
47
+ session: Type.Optional(Type.String({ description: "Upstream session name." })),
48
+ ...outputProperties(Type),
144
49
  }, {
145
50
  additionalProperties: false,
146
- description: "Choose one input mode: script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron.",
147
- });
51
+ // Top-level properties let Pi normalize optional nulls before validating branches.
52
+ anyOf: [
53
+ Type.Object({ action: StringEnum(["select"]), locator: Type.Optional(StringEnum(["role", "label"])) }, { not: { required: ["text"] } }),
54
+ Type.Object({ action: StringEnum(["check", "click", "fill"]) }, { not: { required: ["values"] } }),
55
+ ],
56
+ }));
57
+ }
58
+ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
59
+ return Type.Unsafe(Type.Object({
60
+ url: Type.Optional(Type.String()),
61
+ attached: Type.Optional(Type.Boolean()),
62
+ expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
63
+ expectedSelector: Type.Optional(Type.String()),
64
+ screenshotPath: Type.Optional(Type.String()),
65
+ checkConsole: Type.Optional(Type.Boolean()),
66
+ checkErrors: Type.Optional(Type.Boolean()),
67
+ checkNetwork: Type.Optional(Type.Boolean()),
68
+ loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES)),
69
+ ...outputProperties(Type),
70
+ sessionMode: sessionModeProperty(Type, StringEnum),
71
+ }, {
72
+ additionalProperties: false,
73
+ anyOf: [
74
+ Type.Object({ attached: Type.Literal(true), sessionMode: Type.Optional(StringEnum(["auto"])) }, { not: { required: ["url"] } }),
75
+ Type.Object({ url: Type.String(), attached: Type.Optional(Type.Literal(false)) }),
76
+ ],
77
+ }));
78
+ }
79
+ export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
80
+ const common = { outputPath: outputProperties(Type).outputPath };
81
+ const timeoutMs = Type.Optional(Type.Integer({ minimum: 1 }));
82
+ return Type.Unsafe(Type.Union([
83
+ Type.Object({
84
+ action: StringEnum(["list"]),
85
+ query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
86
+ maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, clamped to ${ELECTRON_DISCOVERY_MAX_RESULTS}.`, minimum: 1 })),
87
+ ...common,
88
+ }, { additionalProperties: false }),
89
+ Type.Object({
90
+ action: StringEnum(["launch"]),
91
+ appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
92
+ appName: Type.Optional(Type.String({ description: "Name from agent_browser_electron list.", minLength: 1 })),
93
+ bundleId: Type.Optional(Type.String({ minLength: 1 })),
94
+ executablePath: Type.Optional(Type.String({ minLength: 1 })),
95
+ appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
96
+ handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
97
+ targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
98
+ timeoutMs,
99
+ allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
100
+ deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
101
+ ...common,
102
+ }, { additionalProperties: false }),
103
+ Type.Object({
104
+ action: StringEnum(["status", "cleanup"]),
105
+ launchId: Type.Optional(Type.String({ minLength: 1 })),
106
+ all: Type.Optional(Type.Literal(true)),
107
+ timeoutMs,
108
+ ...common,
109
+ }, { additionalProperties: false, not: { required: ["launchId", "all"] } }),
110
+ Type.Object({
111
+ action: StringEnum(["probe"]),
112
+ launchId: Type.Optional(Type.String({ minLength: 1 })),
113
+ timeoutMs,
114
+ ...common,
115
+ }, { additionalProperties: false }),
116
+ ]));
117
+ }
118
+ export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
119
+ return Type.Unsafe(Type.Object({
120
+ selector: Type.Optional(Type.String({ description: "Visible selector or @ref." })),
121
+ reactFiberId: Type.Optional(Type.String({ description: "React fiber id; requires --enable react-devtools." })),
122
+ componentName: Type.Optional(Type.String({ description: "Component for local source search." })),
123
+ includeDomHints: Type.Optional(Type.Boolean({ description: "Inspect DOM hints; default true." })),
124
+ maxWorkspaceFiles: Type.Optional(Type.Integer({ description: "Source scan cap; default 2000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
125
+ ...outputProperties(Type),
126
+ sessionMode: sessionModeProperty(Type, StringEnum),
127
+ }, { additionalProperties: false }));
128
+ }
129
+ export function createAgentBrowserNetworkSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
130
+ return Type.Unsafe(Type.Object({
131
+ filter: Type.Optional(Type.String({ description: "Network request filter." })),
132
+ namespace: Type.Optional(Type.String()),
133
+ requestId: Type.Optional(Type.String({ description: "Request id to inspect." })),
134
+ session: Type.Optional(Type.String()),
135
+ url: Type.Optional(Type.String({ description: "Failed URL or fragment." })),
136
+ maxWorkspaceFiles: Type.Optional(Type.Integer({ description: "Source scan cap; default 2000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
137
+ ...outputProperties(Type),
138
+ sessionMode: sessionModeProperty(Type, StringEnum),
139
+ }, { additionalProperties: false }));
140
+ }
141
+ export function createAgentBrowserToolsParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
142
+ return Type.Unsafe(Type.Object({
143
+ enable: Type.Optional(Type.Array(StringEnum(AGENT_BROWSER_ADVANCED_TOOLS), { description: "Add capabilities to active tools; omit to list inventory." })),
144
+ }, { additionalProperties: false }));
148
145
  }
149
146
  export const AGENT_BROWSER_PARAMS = createAgentBrowserParamsSchema();
147
+ export const AGENT_BROWSER_CODE_PARAMS = createAgentBrowserCodeParamsSchema();
148
+ export const AGENT_BROWSER_ACTION_PARAMS = createAgentBrowserActionParamsSchema();
149
+ export const AGENT_BROWSER_QA_PARAMS = createAgentBrowserQaParamsSchema();
150
+ export const AGENT_BROWSER_ELECTRON_PARAMS = createAgentBrowserElectronParamsSchema();
151
+ export const AGENT_BROWSER_SOURCE_PARAMS = createAgentBrowserSourceParamsSchema();
152
+ export const AGENT_BROWSER_NETWORK_SOURCE_PARAMS = createAgentBrowserNetworkSourceParamsSchema();
153
+ export const AGENT_BROWSER_TOOLS_PARAMS = createAgentBrowserToolsParamsSchema();
@@ -1,15 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import { existsSync } from "node:fs";
4
3
  import { dirname, join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
- import { parseArgvDescriptor } from "../argv-descriptor.js";
7
- import { getFlagName } from "../argv-grammar.js";
8
- import { isBrowserIndependentRead, needsManagedSession } from "../command-policy.js";
9
- import { isCloseCommand } from "../command-taxonomy.js";
10
- import { LAUNCH_SCOPED_FLAGS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS } from "../launch-scoped-flags.js";
11
5
  import { isRecord } from "../parsing.js";
12
- import { validateToolArgs } from "../runtime.js";
6
+ import { extractExplicitNamespace, extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
7
+ import { getUpstreamEffectiveBatchSteps } from "../orchestration/batch-stdin.js";
8
+ import { extractUpstreamCommandTokens } from "../argv-descriptor.js";
9
+ import { isCloseAllCommand } from "../command-taxonomy.js";
10
+ import { redactSensitiveText, validateToolArgs } from "../runtime.js";
13
11
  export const AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES = 64 * 1_024;
14
12
  export const AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS = 120_000;
15
13
  export const AGENT_BROWSER_SCRIPT_NAMESPACE = "";
@@ -18,7 +16,6 @@ export const AGENT_BROWSER_SCRIPT_MAX_CALLS = 25;
18
16
  export const AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES = 64 * 1_024;
19
17
  export const AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES = 1 * 1_024 * 1_024;
20
18
  export const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
21
- export const AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES = 512 * 1_024;
22
19
  function findPackageRoot(startDir) {
23
20
  let currentDir = startDir;
24
21
  for (;;) {
@@ -37,25 +34,13 @@ function resolveScriptWorkerPath() {
37
34
  return workerPath;
38
35
  }
39
36
  const SCRIPT_SESSION_NAME_PATTERN = /^piab-script-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
40
- const SCRIPT_ALLOWED_LAUNCH_FLAG = "--allowed-domains";
41
- const SCRIPT_FORBIDDEN_COMMANDS = new Set(["attach", "auth", "batch", "connect", "script", "session", "state"]);
42
- const SCRIPT_FORBIDDEN_FLAGS = new Set([
43
- ...LAUNCH_SCOPED_FLAGS.filter((flag) => flag !== SCRIPT_ALLOWED_LAUNCH_FLAG),
44
- ...MANAGED_RESTORE_INCOMPATIBLE_FLAGS.filter((flag) => flag !== SCRIPT_ALLOWED_LAUNCH_FLAG),
45
- "--namespace",
46
- "--session",
47
- "--config",
48
- ]);
49
- export function compileAgentBrowserScript(input) {
37
+ export function validateAgentBrowserScriptSource(input) {
50
38
  if (typeof input !== "string")
51
39
  return { error: "script must be a string." };
52
40
  const bytes = Buffer.byteLength(input, "utf8");
53
41
  return bytes > AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES
54
42
  ? { error: `script must be ${AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES} bytes or less.` }
55
- : { compiled: { code: input } };
56
- }
57
- export function createAgentBrowserScriptSessionName() {
58
- return `piab-script-${randomUUID()}`;
43
+ : {};
59
44
  }
60
45
  export function createAgentBrowserScriptCloseArgs(sessionName) {
61
46
  return ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, "close"];
@@ -63,25 +48,6 @@ export function createAgentBrowserScriptCloseArgs(sessionName) {
63
48
  export function isAgentBrowserScriptSessionName(value) {
64
49
  return typeof value === "string" && SCRIPT_SESSION_NAME_PATTERN.test(value);
65
50
  }
66
- function getScriptCallPolicyError(args) {
67
- const descriptor = parseArgvDescriptor(args);
68
- const command = descriptor.commandInfo.command;
69
- if (!command)
70
- return "script browser call args must contain an agent-browser command.";
71
- if (isCloseCommand(command))
72
- return "script browser calls cannot close, quit, or exit their isolated session.";
73
- if (SCRIPT_FORBIDDEN_COMMANDS.has(command))
74
- return `script browser calls cannot use ${command}.`;
75
- if (!needsManagedSession(descriptor) && !isBrowserIndependentRead(descriptor.upstreamCommandTokens))
76
- return `script browser calls cannot use sessionless/local command ${command}.`;
77
- for (const token of args) {
78
- const flag = getFlagName(token);
79
- if (SCRIPT_FORBIDDEN_FLAGS.has(flag)) {
80
- return `script browser calls cannot use ${flag}; the parent owns the isolated session identity and launch policy.`;
81
- }
82
- }
83
- return undefined;
84
- }
85
51
  export function validateAgentBrowserScriptBrowserParams(input) {
86
52
  if (!isRecord(input))
87
53
  return { error: "script browser(params) requires an object." };
@@ -102,53 +68,51 @@ export function validateAgentBrowserScriptBrowserParams(input) {
102
68
  stdin: input.stdin,
103
69
  timeoutMs: input.timeoutMs,
104
70
  };
105
- const policyError = getScriptCallPolicyError(params.args);
106
- if (policyError)
107
- return { error: policyError, policyBlocked: true };
108
71
  const validationError = validateToolArgs(params.args);
109
72
  return validationError ? { error: validationError } : { params };
110
73
  }
111
- function buildRejectedCallEnvelope(error, policyBlocked) {
74
+ export function bindBrowserCodeCall(params, identity) {
75
+ const explicitSession = extractExplicitSessionName(params.args);
76
+ const namespaceFlags = scanUpstreamGlobalFlagOccurrences(params.args, "--namespace");
77
+ const explicitNamespace = extractExplicitNamespace(params.args);
78
+ if ((explicitSession !== undefined && explicitSession !== identity.sessionName)
79
+ || (namespaceFlags.length > 0 && (explicitNamespace || undefined) !== (identity.namespace || undefined))) {
80
+ throw new Error("A code call uses one browser identity. Set session/namespace on agent_browser_code to choose another browser.");
81
+ }
82
+ const tokens = extractUpstreamCommandTokens(params.args);
83
+ if (isCloseAllCommand(tokens) || getUpstreamEffectiveBatchSteps(tokens, params.stdin).some(isCloseAllCommand)) {
84
+ throw new Error("Run namespace-wide close --all directly with agent_browser, outside a session-scoped code call.");
85
+ }
86
+ return { ...params, args: [
87
+ ...(namespaceFlags.length === 0 ? ["--namespace", identity.namespace ?? ""] : []),
88
+ ...(explicitSession === undefined ? ["--session", identity.sessionName] : []),
89
+ ...params.args,
90
+ ] };
91
+ }
92
+ function buildRejectedCallEnvelope(error) {
112
93
  return {
113
94
  data: null,
114
- details: { failureCategory: policyBlocked ? "policy-blocked" : "validation-error", resultCategory: "failure" },
115
95
  error,
116
- failureCategory: policyBlocked ? "policy-blocked" : "validation-error",
117
- ok: false,
96
+ failureCategory: "validation-error",
97
+ success: false,
118
98
  resultCategory: "failure",
119
99
  summary: error,
120
- text: error,
121
100
  };
122
101
  }
123
102
  function normalizeBrowserEnvelope(value) {
124
- if (!isRecord(value)
125
- || typeof value.ok !== "boolean"
126
- || typeof value.text !== "string"
127
- || typeof value.summary !== "string"
128
- || (value.resultCategory !== "success" && value.resultCategory !== "failure")) {
129
- return buildRejectedCallEnvelope("The ordinary agent_browser executor returned an invalid script envelope.", false);
103
+ if (!isRecord(value) || typeof value.success !== "boolean" || (value.resultCategory !== "success" && value.resultCategory !== "failure")) {
104
+ return buildRejectedCallEnvelope("The browser executor returned an invalid code observation.");
130
105
  }
131
- return {
132
- data: value.data ?? null,
133
- details: isRecord(value.details) ? value.details : undefined,
134
- error: typeof value.error === "string" ? value.error : undefined,
135
- failureCategory: value.failureCategory,
136
- nextActions: Array.isArray(value.nextActions) ? value.nextActions : undefined,
137
- ok: value.ok,
138
- resultCategory: value.resultCategory,
139
- successCategory: value.successCategory,
140
- summary: value.summary,
141
- text: value.text,
142
- };
106
+ return value;
143
107
  }
144
108
  function buildStepSummary(index, envelope) {
145
109
  return {
146
110
  failureCategory: envelope.failureCategory,
147
111
  index,
148
- ok: envelope.ok,
112
+ ok: envelope.success,
149
113
  resultCategory: envelope.resultCategory,
150
114
  successCategory: envelope.successCategory,
151
- summary: envelope.summary,
115
+ summary: envelope.summary ?? (typeof envelope.error === "string" ? envelope.error : "Browser call completed."),
152
116
  };
153
117
  }
154
118
  function buildFailedRun(options) {
@@ -168,7 +132,7 @@ function isScriptChildMessage(value) {
168
132
  return true;
169
133
  if (value.type === "call")
170
134
  return typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id > 0;
171
- if (value.type === "emit")
135
+ if (value.type === "emit" || value.type === "image")
172
136
  return true;
173
137
  return value.type === "complete";
174
138
  }
@@ -206,7 +170,7 @@ function serializeFinalOutput(value) {
206
170
  return JSON.stringify(value);
207
171
  }
208
172
  export async function runAgentBrowserScript(options) {
209
- const compiled = compileAgentBrowserScript(options.code);
173
+ const compiled = validateAgentBrowserScriptSource(options.code);
210
174
  if (compiled.error) {
211
175
  return buildFailedRun({ callCount: 0, emitCount: 0, error: compiled.error, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
212
176
  }
@@ -241,12 +205,12 @@ export async function runAgentBrowserScript(options) {
241
205
  let cumulativeBytes = 0;
242
206
  let callCount = 0;
243
207
  let rejectedCallCount = 0;
244
- let leaseStarted = false;
245
208
  let ready = false;
246
209
  let stopping = false;
247
210
  let activeCallController;
248
211
  const emissions = [];
249
212
  const steps = [];
213
+ const failures = [];
250
214
  const messages = [];
251
215
  let draining = false;
252
216
  let drainPromise = Promise.resolve();
@@ -275,10 +239,11 @@ export async function runAgentBrowserScript(options) {
275
239
  if (timeout)
276
240
  clearTimeout(timeout);
277
241
  options.signal?.removeEventListener("abort", abortListener);
278
- activeCallController?.abort();
242
+ activeCallController?.abort(result.timedOut ? new DOMException("Browser code deadline exceeded.", "TimeoutError") : options.signal?.reason);
279
243
  killTimer = terminateChild(child);
244
+ // The caller holds the browser lease until dispatch actually settles.
280
245
  if (waitForDrain)
281
- await settleWithin(drainPromise, 5_000);
246
+ await drainPromise.catch(() => undefined);
282
247
  await settleWithin(childExit, 1_000);
283
248
  clearTimeout(killTimer);
284
249
  resolveResult(result);
@@ -287,13 +252,16 @@ export async function runAgentBrowserScript(options) {
287
252
  ...flags,
288
253
  callCount,
289
254
  emitCount: emissions.length,
255
+ ...(emissions.length ? { data: emissions.length === 1 ? emissions[0] : emissions } : {}),
290
256
  error,
291
257
  failureCategory,
258
+ failures,
292
259
  rejectedCallCount,
293
260
  steps,
294
261
  }), waitForDrain);
295
262
  const abortListener = () => {
296
- void fail("Script execution was aborted.", "aborted", { aborted: true }, true);
263
+ const timedOut = options.signal?.reason instanceof Error && options.signal.reason.name === "TimeoutError";
264
+ void fail(timedOut ? "Browser code deadline exceeded." : "Browser code was aborted.", timedOut ? "timeout" : "aborted", timedOut ? { timedOut: true } : { aborted: true }, true);
297
265
  };
298
266
  options.signal?.addEventListener("abort", abortListener, { once: true });
299
267
  timeout = setTimeout(() => {
@@ -333,6 +301,18 @@ export async function runAgentBrowserScript(options) {
333
301
  emissions.push(message.value);
334
302
  continue;
335
303
  }
304
+ if (message.type === "image") {
305
+ try {
306
+ if (!options.emitImage)
307
+ throw new Error("Image emission is unavailable.");
308
+ await options.emitImage(message.value);
309
+ }
310
+ catch (error) {
311
+ await fail(error instanceof Error ? error.message : "Invalid image handle.", "validation-error");
312
+ return;
313
+ }
314
+ continue;
315
+ }
336
316
  if (message.type === "complete") {
337
317
  if (message.error) {
338
318
  await fail(describeScriptError(message.error), "script-error");
@@ -353,7 +333,7 @@ export async function runAgentBrowserScript(options) {
353
333
  await fail(`Final script output exceeds ${AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES} bytes.`, "validation-error");
354
334
  return;
355
335
  }
356
- await finish({ callCount, data, emitCount: emissions.length, ok: true, rejectedCallCount, steps }, false);
336
+ await finish({ callCount, data, emitCount: emissions.length, failures, ok: true, rejectedCallCount, steps }, false);
357
337
  return;
358
338
  }
359
339
  callCount += 1;
@@ -365,38 +345,30 @@ export async function runAgentBrowserScript(options) {
365
345
  let envelope;
366
346
  if (!validated.params) {
367
347
  rejectedCallCount += 1;
368
- envelope = buildRejectedCallEnvelope(validated.error ?? "Invalid script browser call.", validated.policyBlocked === true);
348
+ envelope = buildRejectedCallEnvelope(validated.error ?? "Invalid script browser call.");
369
349
  }
370
350
  else {
371
- if (!leaseStarted) {
372
- try {
373
- options.beforeFirstCall?.();
374
- leaseStarted = true;
375
- }
376
- catch {
377
- await fail("Unable to persist the isolated script session lease.", "upstream-error");
378
- return;
379
- }
380
- }
381
351
  activeCallController = new AbortController();
382
352
  try {
383
353
  envelope = normalizeBrowserEnvelope(await options.dispatch(validated.params, activeCallController.signal));
384
354
  }
385
- catch {
386
- envelope = buildRejectedCallEnvelope("The ordinary agent_browser executor failed while dispatching this call.", false);
355
+ catch (error) {
356
+ envelope = buildRejectedCallEnvelope(redactSensitiveText(error instanceof Error ? error.message : "The browser executor failed while dispatching this call."));
387
357
  }
388
358
  finally {
389
359
  activeCallController = undefined;
390
360
  }
391
- if (stopping)
392
- return;
393
361
  }
394
362
  steps.push(buildStepSummary(callCount - 1, envelope));
363
+ if (!envelope.success)
364
+ failures.push({ ...envelope, index: callCount - 1 });
365
+ if (stopping)
366
+ return;
395
367
  try {
396
368
  await sendParentMessage({ envelope, id: message.id, type: "response" });
397
369
  }
398
370
  catch {
399
- await fail("Unable to return a browser result to the script sandbox.", "upstream-error");
371
+ await fail("Unable to return a browser result within the code IPC limit. Narrow the native extraction or use agent_browser with outputPath; already-dispatched effects are not rolled back.", "upstream-error");
400
372
  return;
401
373
  }
402
374
  }