pi-agent-browser-native 0.2.48 → 0.2.49
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 +17 -0
- package/README.md +16 -6
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/RELEASE.md +22 -11
- package/docs/SUPPORT_MATRIX.md +4 -3
- package/package.json +9 -5
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +8 -7
- package/extensions/agent-browser/index.ts +0 -961
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -211
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -527
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +0 -44
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -280
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -914
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -521
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +0 -158
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +0 -116
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +0 -147
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +0 -183
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +0 -58
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -847
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -559
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +0 -8
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -565
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -267
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -728
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { LAUNCH_SCOPED_FLAG_LABEL } from "./launch-scoped-flags.js";
|
|
2
|
+
/**
|
|
3
|
+
* Purpose: Provide the canonical agent_browser operating playbook shared by runtime prompt metadata and generated documentation fragments.
|
|
4
|
+
* Responsibilities: Define stable guidance bullets, native tool-call examples, and wrapper-behavior notes without importing runtime/browser process code.
|
|
5
|
+
* Scope: Agent-facing documentation and prompt-guidance text only; command execution and wrapper state behavior live in runtime modules.
|
|
6
|
+
* Usage: Imported by the extension entrypoint for promptGuidelines and by the documentation drift-check script for generated Markdown blocks.
|
|
7
|
+
* Invariants/Assumptions: The native pi tool receives args after the agent-browser binary, stdin is only for batch/eval --stdin/auth save --password-stdin, and wrapper behavior documented here must match implemented behavior.
|
|
8
|
+
*/
|
|
9
|
+
export const PROJECT_RULE_PROMPT = "Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
|
|
10
|
+
export const TOOL_PROMPT_GUIDELINES_PREFIX = [
|
|
11
|
+
"Use agent_browser whenever the task requires a real browser or live web content.",
|
|
12
|
+
];
|
|
13
|
+
export function buildInstalledDocsGuideline(paths) {
|
|
14
|
+
return `For deeper agent_browser guidance without bloating context, read installed package docs on demand: ${paths.readmePath} for setup/external dependencies, ${paths.commandReferencePath} for command workflows, and ${paths.toolContractPath} for result/details contracts. Do not load the full command reference unless needed; prefer targeted sections.`;
|
|
15
|
+
}
|
|
16
|
+
export const QUICK_START_GUIDELINES = [
|
|
17
|
+
`Quick start mental model: use exactly one of args (exact agent-browser CLI args after the binary), semanticAction (a thin shorthand compiled to find argv for locator actions, direct selector/ref click/check/fill, or select argv for native dropdowns), job (a constrained short-workflow schema compiled to batch --bail by default; set failFast:false only when later diagnostics should continue after a failed step), qa (a lightweight fail-fast QA preset built on batch --bail with bounded visible expected-text checks, including qa.attached for current sessions), electron (desktop Electron list/launch/status/cleanup/probe), or the experimental sourceLookup / networkSourceLookup helpers (candidates only; each compiled to batch); stdin is only for batch, eval --stdin, auth save --password-stdin, and wrapper-generated batch stdin from job, qa, sourceLookup, or networkSourceLookup, and is rejected with electron; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}) to apply. Use outputPath for durable eval/get/snapshot captures. Do not pass --json in args; the wrapper injects it.`,
|
|
18
|
+
"There is no first-class reusable named browser recipe runtime above top-level job, the qa preset, and raw batch stdin; keep recurring flows in documentation examples or those inputs (closed RQ-0068; see docs/ARCHITECTURE.md#no-reusable-recipe-layer-yet).",
|
|
19
|
+
"Common first calls (first-call recipe): { args: [\"open\", \"<url>\"] } → { args: [\"snapshot\", \"-i\"] } → { args: [\"click\", \"@eN\"] } or { args: [\"fill\", \"@eN\", \"<text>\"] } using @refs and visible labels from that snapshot, then { args: [\"snapshot\", \"-i\"] } after navigation or DOM changes. On https://example.com/ the main link label is Learn more (use exact snapshot text, not guessed link copy).",
|
|
20
|
+
"Locator-first clicks/fills and native select changes without hand-building argv: { semanticAction: { action: \"click\", locator: \"text\", value: \"Close\" } }, { semanticAction: { action: \"fill\", locator: \"label\", value: \"Email\", text: \"user@example.com\" } }, direct current targets such as { semanticAction: { action: \"fill\", selector: \"@e1\", text: \"prompt\" } }, or { semanticAction: { action: \"select\", selector: \"#flavor\", value: \"chocolate\" } }; add semanticAction.session when targeting a named upstream browser session; details.compiledSemanticAction shows the semantic target, while details.effectiveArgs may show a resolved current @ref for active-session role/name click/check/fill actions to avoid hidden duplicate matches; semanticAction does not expose uncheck while upstream find ... uncheck is not runtime-supported, so use raw uncheck with a stable selector or current ref; selector-not-found failures may append bounded click try-*-candidate next actions or, for fill misses with current editable refs, details.richInputRecovery with focus/click actions that do not copy fill text; stale-ref failures can return retry-semantic-action-after-stale-ref for compiled find actions when retry safety is provable.",
|
|
21
|
+
`Common advanced calls: { args: ["batch"], stdin: "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }, { job: { steps: [{ action: "open", url: "https://example.com" }, { action: "assertText", text: "Example Domain" }, { action: "screenshot", path: ".dogfood/example.png" }] } }, { qa: { url: "https://example.com", expectedText: "Example Domain", screenshotPath: ".dogfood/qa-example.png" } } (example.com smoke only; elsewhere match exact visible text from snapshot -i), { electron: { action: "list", query: "code" } }, { electron: { action: "launch", appName: "Visual Studio Code", handoff: "snapshot" } }, { electron: { action: "probe" } }, { qa: { attached: true, expectedText: "Explorer" } }, { args: ["eval", "--stdin"], stdin: "document.title", outputPath: "logs/page-title.json" }, { args: ["auth", "save", "name", "--password-stdin"], stdin: "<password from user-approved secret source>" }, { args: ["--profile", "Default", "open", "https://example.com/account"], sessionMode: "fresh" }, and { args: ["open", "--enable", "react-devtools", "https://example.com"], sessionMode: "fresh" }. For app pages with a native dropdown, job steps can include { action: "select", selector: "#flavor", value: "chocolate" } before the dependent assertion; for locator-friendly pages, job click/fill steps can use semantic locator fields such as { action: "fill", locator: "role", role: "searchbox", name: "Search", text: "agent browser" }; for human-paced input, job type steps can use { action: "type", selector: "#prompt", text: "hello", delayMs: 20, press: "Enter" }; delayed typing is capped at 200 characters per step, and generated per-character rows are compacted in visible batch prose while full rows remain in details.batchSteps.`,
|
|
22
|
+
"Constrained job navigation is explicit only: click (and select/submit flows that may navigate) does not prove the next page loaded; add assertUrl and/or assertText after navigation-prone steps before screenshot or later interactions. Keep jobs short around navigation, click, and rerender boundaries on dynamic React/product apps; avoid a whole checkout in one job. If a long job times out and details.timeoutPartialProgress shows a mutating incomplete step, inspect current page state and continue with a shorter job or single action instead of blindly retrying the mutating step. Example: { job: { steps: [{ action: \"open\", url: \"https://shop.example/checkout\" }, { action: \"fill\", selector: \"#email\", text: \"user@example.com\" }, { action: \"click\", selector: \"#continue\" }, { action: \"assertUrl\", url: \"**/shipping\" }, { action: \"assertText\", text: \"Shipping address\" }, { action: \"screenshot\", path: \".dogfood/shipping.png\" }] } }. Top-level click may add navigationSummary hints, but job never auto-inserts post-click asserts.",
|
|
23
|
+
"High-value command reference: click <selector> --new-tab opens link-like targets in a new tab; select <selector> <value...> changes native dropdown values; scroll <dir> [px] --selector <sel>, wrapper-handled scroll <selector> <dir> [px|percent] targets nested scrollers, and wrapper-handled scroll to end/top targets document scrolling; download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [selector] [path] captures a page or element image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree/inspect/renders/suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation; tap <selector> and swipe <direction> [distance] support iOS/provider touch flows.",
|
|
24
|
+
"For artifact-producing commands, read the visible artifact block and details.artifactVerification before using files: check requested path, absolute path, existence, size bytes, artifact kind, optional mediaType, status, optional limitation, and verified/missing/pending/unverified counts. details.artifacts contains per-file metadata; record start rows are pending/openRecording until record stop writes the target. The wrapper creates parent directories for direct artifact paths and can save simple loopback HTTP(S) anchor downloads directly to the requested path before upstream download fallback. Browser close does not delete explicit saved files; if close reports details.artifactCleanup, use host file tools to remove paths listed in explicitArtifactPaths (when non-empty) after inspection. If close fails with details.promptGuard.reason=requested-artifacts-missing-before-close, save the exact required artifact path before closing. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step; if annotation labels crowd a dense page, use a scoped or non-annotated screenshot plus snapshot refs instead.",
|
|
25
|
+
"When details.nextActions is present, prefer those exact native agent_browser follow-up payloads over prose guidance; they may include args, stdin, sessionMode, networkSourceLookup, safety notes, or artifactPath for saved files.",
|
|
26
|
+
];
|
|
27
|
+
export const WEB_SEARCH_PROMPT_GUIDELINE = "Use agent_browser_web_search for quick live search/URL discovery; it chooses Exa or Brave, preferring Exa unless configured otherwise. Use agent_browser for interaction/DOM/screenshots/auth. Do not run parallel searches: one good query, inspect results, then one follow-up max; on HTTP 429 stop and report provider limits.";
|
|
28
|
+
export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
|
|
29
|
+
"Standard workflow: open the page, snapshot -i, interact using current @refs from that snapshot, and re-snapshot after navigation, scrolling, rerendering, or other major DOM changes because refs are page-scoped; the wrapper fails mutation-prone stale/recycled refs before upstream can silently target a different current-page element. On dense pages, use wrapper-side snapshot -i --search <text> or snapshot -i --filter role=<role> to render matching refs while preserving the full ref map in details.refSnapshot, add snapshot --viewport when scroll position or above/below-fold context matters, and add snapshot --diff when a quick before/after ref-map delta would prevent reading a full spill file.",
|
|
30
|
+
"For ordinary forms from one snapshot, batch multiple fill @refs before the submit/click step to avoid serial tool calls; if a fill may autosubmit, navigate, or rerender later fields, split the flow and refresh refs first.",
|
|
31
|
+
"Snapshot choice: prefer snapshot -i for routine clicks/fills (interactive @refs, main-content-first). Use snapshot --compact when you need a denser same-page tree without full spill; use full snapshot (no -i) only when you need the complete accessibility tree. Re-snapshot after navigation or major DOM changes. When snapshot -i compacts because the tree is oversized, scan visible output for Omitted high-value controls and optional details.data.highValueControlRefIds before opening the spill file: those list bounded searchboxes, textboxes, comboboxes, buttons, tabs, checkboxes, radios, options, and menuitems that did not fit the key/other ref previews.",
|
|
32
|
+
"When a visible text or accessible-name target should survive ref churn, prefer find locators such as role, text, label, placeholder, alt, title, or testid with the intended action instead of guessing a CSS selector.",
|
|
33
|
+
"For desktop or host-controlled rich inputs, if semanticAction fill misses, refresh refs and prefer a current editable @ref from details.richInputRecovery or the latest snapshot; focus or click that ref, then use keyboard inserttext or keyboard type with the intended text. Do not auto-submit with Enter or a submit button unless the user flow explicitly calls for it.",
|
|
34
|
+
"Do not assume Playwright selector dialects such as text=Close or button:has-text('Close') are supported wrapper syntax unless current upstream agent-browser behavior has been verified.",
|
|
35
|
+
"For authenticated or user-specific content explicitly requested by the user, such as feeds, inboxes, account pages, or private dashboards, use a real profile only when the user/config asks for it or profiles have been inspected; do not assume --profile Default exists on every machine. Do not use a real profile for public pages just because they are dashboards. Treat visible page content from real profiles as model-visible transcript data; use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser. If profile/user-data-dir resolution fails, stop retrying opens, run profiles and/or doctor through agent_browser, then report what the user needs to configure.",
|
|
36
|
+
"Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
|
|
37
|
+
`When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.`,
|
|
38
|
+
`If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
|
|
39
|
+
"For React introspection, launch the page with --enable react-devtools before first navigation, then use react tree, react inspect <fiberId>, sourceLookup candidates for local UI source hints, react renders start/stop, or react suspense; sourceLookup is experimental and reports confidence/evidence instead of guaranteed DOM-to-file mappings. For failed fetches and APIs, networkSourceLookup (experimental) correlates failed network requests with initiator metadata and bounded workspace URL literals—candidates only, not definitive blame. Use vitals [url] for Core Web Vitals and hydration timing, and pushstate <url> for client-side SPA navigation.",
|
|
40
|
+
"For first-navigation setup, use open without a URL plus network route --resource-type <csv>, cookies set --curl <file>, or --init-script/--enable before navigate/opening the target page.",
|
|
41
|
+
"For stateful browser context work, prefer purpose-specific page actions before dumping browser data: use auth save --password-stdin with the tool stdin field for credentials, auth list/show/delete/remove for local auth-profile maintenance, auth login when you need the browser to fill a saved profile, state save/load for portable test state, state list/show/rename/clear/clear -a/clean for saved-state lifecycle cleanup, cookies get/set/clear and storage local|session only when the task needs those values, and expect cookie/storage/auth/state summaries to redact credential-like fields while allowing benign primitive storage values when useful for local QA.",
|
|
42
|
+
"For batch chains that touch cookies, storage, auth, or other secret-bearing commands, use details.batchSteps for per-step artifacts, categories, spill paths, and full structured errors; top-level details.data on batch is only a compact redacted step matrix (success, argv-redacted command, redacted result or scrubbed error text) built from the same presentation rules as standalone calls.",
|
|
43
|
+
"For non-core families, pass current upstream commands through the native tool directly: network route/requests/har (including request filters like --type/--method/--status), diff snapshot/screenshot/url with scoped/baseline options, trace/profiler/record, console/errors/highlight/inspect/clipboard, stream enable/disable/status, dashboard start/stop, device list for iOS simulator inventory, and chat. For compact network requests output, prefer details.nextActions for request detail, route-mock diagnostics, actionable failed-request networkSourceLookup, filtering, clearing the aggregate buffer before repro, or HAR capture follow-ups instead of guessing request-id syntax. Artifact-producing commands report details.artifacts and verification state; long-running starts such as stream, dashboard, trace/profiler, and record should be paired with the matching stop/disable command when the task is done; stream enable already-enabled outcomes are treated as idempotent success with status/disable follow-ups.",
|
|
44
|
+
"For Electron desktop apps, prefer top-level electron for wrapper-owned discovery, isolated launch, status, compact probe, and cleanup: list first, treat likely-sensitive annotations as hints rather than enforcement, launch with the default snapshot handoff unless handoff: \"tabs\" is the safer diagnostic starting point, use electron.probe or snapshot -i/qa.attached for current-session state, and always cleanup the returned launchId when done. electron.launch uses an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app. For signed-in local app state, host-launch the normal app with --remote-debugging-port when appropriate, then use raw args connect <port|url>; after connect, inspect tab list, select the stable tab id such as tab t2, then run a condition wait or snapshot -i before using refs. close commands (`close`, `quit`, or `exit`) only close the browser/CDP session; leave manually launched app shutdown, profile cleanup, and explicit artifacts to the host owner.",
|
|
45
|
+
"For provider or specialized app workflows, load version-matched upstream guidance with skills get agentcore|electron|slack|dogfood|vercel-sandbox through the native tool; add --full when you need references/templates, and use skills get --all only for broad skill audits. Provider launches such as -p ios, --provider browserbase/kernel/browseruse/browserless/agentcore, and iOS --device are upstream-owned setup paths; use sessionMode fresh when switching providers and expect external credentials or local Appium/Xcode setup to be required.",
|
|
46
|
+
"For dialogs and frames, use dialog status/accept/dismiss and frame <selector|main> through native args; dialog commands and eval snippets that look like alert/confirm/prompt/dialog triggers are shorter-bounded than normal browser calls, and timed-out dialog-like interactions may add inspect-dialog-after-timeout, dismiss-dialog-after-timeout, or recover-fresh-session-after-dialog-timeout nextActions. When --confirm-actions produces a pending confirmation, use details.nextActions or exact confirm <id> / deny <id> calls instead of inventing ids.",
|
|
47
|
+
"If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. For headed demos, put --headed on the first launch with sessionMode=fresh and verify with screenshot/tab/get-url evidence because tool success cannot prove the OS window is visible to the user. For desktop readiness, prefer real conditions first: wait --text, wait --url, wait --fn, wait --load <state>, wait --download, or qa.attached; for disappearance checks in agent-browser 0.27.2, use wait --fn predicates instead of stale upstream-help examples like wait <selector> --state hidden. Use electron.probe/status for wrapper-owned launch health or target mismatch. Fixed waits are a last resort: use explicit --timeout or top-level timeoutMs for legitimately slow waits, and treat a successful payload like \"waited\":\"timeout\" as elapsed time only—verify completion with an observed condition, fresh snapshot, or screenshot.",
|
|
48
|
+
"For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
|
|
49
|
+
"For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
|
|
50
|
+
"For downloads, prefer download <selector> <path> when an element click should save a file; simple loopback anchor downloads are saved to the requested path when the wrapper can resolve an HTTP(S) href. Do not rely on click alone when you need the downloaded file on disk.",
|
|
51
|
+
"On dashboards with nested scroll containers, verify scroll with a screenshot or fresh snapshot -i; if the viewport did not move, details.data.scrolled may be false/noMovement true and you should prefer scrollintoview <@ref> or target the actual scrollable region with scroll <selector> <dir> [px|percent]. For native selects, use select <selector> <value...> (or semanticAction/job select) instead of clicking option refs; for custom comboboxes, a click/semanticAction may only focus the field, so re-snapshot and fall back to type, press Enter/arrow keys, or visible option refs.",
|
|
52
|
+
"When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
|
|
53
|
+
"When using eval --stdin for extraction, pass the JavaScript through the native tool stdin field, not as an extra args token after --stdin, and return the value you want instead of relying on console.log as the primary result channel. Prefer plain expressions like ({ title: document.title }) or explicitly invoked functions like (() => ({ title: document.title }))(); use outputPath when the eval/get/snapshot data should be saved as a durable local file. If a function-shaped snippet returns {}, details.evalStdinHint may warn that the function was serialized instead of called. On file:// pages, when upstream JSON returns result: null for non-trivial stdin, details.evalResultWarning may append Eval result warning without failing the tool—treat that as inconclusive DOM verification. If get text on a CSS selector surfaces details.selectorTextVisibility or selectorTextVisibilityAll, prefer a visible @ref, a more specific selector, or the inspect-visible-text-candidates nextAction over hidden tab content.",
|
|
54
|
+
"When details.pageChangeSummary is present, use changeType and summary as a compact signal for navigation, DOM mutation, confirmations, or artifacts; when nextActionIds is set, match those ids to entries in details.nextActions (or per-step nextActions inside batch) for concrete follow-up payloads instead of inferring from prose alone. If details.clickDispatch reports a click-dispatch miss, refresh/inspect/retry the real click first; for static local fixtures only, an explicit eval --stdin programmatic .click() can exercise app handlers, but treat it as an untrusted scripted workaround and never use it to bypass stop-before-submit/order/purchase boundaries. If a no-navigation click surfaces details.overlayBlockers, inspect the fresh snapshot evidence before using a close/dismiss candidate nextAction; ordinary page chrome without dialog/alertdialog evidence should not trigger this diagnostic.",
|
|
55
|
+
"When commands save or spill files (screenshots, downloads, PDFs, traces, recordings, HAR, large snapshot spills), use the user's exact requested paths when given and treat paths as provisional until details.artifactVerification shows every row verified: branch on missingCount, pendingCount, unverifiedCount, per-entry state, and optional limitation before downstream file use or PASS/FAIL reporting.",
|
|
56
|
+
"For evidence-only screenshots, QA captures, or other audit artifacts, save to an explicit path and branch on details.artifactVerification plus details.artifacts before reporting PASS/FAIL; do not require vision review of inline image attachments unless the user asked for visual inspection.",
|
|
57
|
+
"Respect explicit user stop boundaries yourself: if the user says to stop before order/post/purchase/submit, do not click that final action. The wrapper does not infer broad business intent from prompt text; details.promptGuard is reserved for concrete artifact-before-close checks.",
|
|
58
|
+
"Successful record stop needs ffmpeg on PATH; the wrapper may warn after record start when ffmpeg is missing.",
|
|
59
|
+
"Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
|
|
60
|
+
];
|
|
61
|
+
export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
|
|
62
|
+
"Prefer agent_browser over bash for opening sites, docs, clicking, filling, screenshots, eval, and batch workflows.",
|
|
63
|
+
"Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when agent_browser can do the job.",
|
|
64
|
+
"Pass exact agent-browser CLI arguments in agent_browser args when you are not using semanticAction, job, or qa, excluding the binary name and --json (agent_browser injects --json automatically).",
|
|
65
|
+
"Use agent_browser stdin only for eval --stdin, batch, auth save --password-stdin, or wrapper-generated job/qa batches instead of shell heredocs or password args; other command/stdin combinations are rejected before launch.",
|
|
66
|
+
`Let the agent_browser extension-managed session handle the common path unless you explicitly need a fresh launch for launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}).`,
|
|
67
|
+
"Use agent_browser sessionMode=fresh when switching from an existing implicit session to a new profile/browser executable/debug/init-script/provider launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
|
|
68
|
+
];
|
|
69
|
+
export const INSPECTION_TOOL_CALL_EXAMPLES = [
|
|
70
|
+
'{ "args": ["--help"] }',
|
|
71
|
+
'{ "args": ["--version"] }',
|
|
72
|
+
];
|
|
73
|
+
export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
|
|
74
|
+
"After launch-scoped open/goto/navigate calls that can restore existing tabs (for example --profile, --session-name, or --state), agent_browser best-effort re-selects the tab whose URL matches the returned page when restored tabs steal focus during launch.",
|
|
75
|
+
"After the wrapper observes tab-drift risk for a session (for example profile restore correction, overlapping stale opens, or resumed session state), later active-tab commands best-effort pin that tab inside the same upstream invocation. Routine same-session commands are not preflighted with tab list just because a target tab is known.",
|
|
76
|
+
"For sessions with observed tab-drift risk, after a successful command on a known target tab, agent_browser also best-effort restores that intended tab if a restored/background tab steals focus after the command completes. Routine same-session commands skip this post-command tab-list probe.",
|
|
77
|
+
"If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
|
|
78
|
+
];
|
|
79
|
+
export function buildSharedBrowserPlaybookGuidelines(options) {
|
|
80
|
+
return [
|
|
81
|
+
SHARED_BROWSER_PLAYBOOK_GUIDELINES[0],
|
|
82
|
+
...(options.includeWebSearch ? [WEB_SEARCH_PROMPT_GUIDELINE] : []),
|
|
83
|
+
...SHARED_BROWSER_PLAYBOOK_GUIDELINES.slice(1),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
/** Tier A: always-on tool promptGuidelines (keep small; Tier B lives in SHARED_BROWSER_PLAYBOOK_GUIDELINES and docs). */
|
|
87
|
+
export const RUNTIME_PROMPT_GUIDELINES = [
|
|
88
|
+
"Use agent_browser with exactly one input mode: args, semanticAction, job, qa, sourceLookup/networkSourceLookup, or electron. stdin only for batch/eval/auth or wrapper batch; electron rejects stdin. Do not pass --json in args; agent_browser injects it.",
|
|
89
|
+
"For agent_browser, the common flow is open, snapshot -i, use current @refs or semanticAction, then re-snapshot after navigation/scroll/rerender/DOM change. Batch same-snapshot forms unless they may submit/navigate/rerender. Keep job flows short around navigation/click/rerender boundaries on dynamic apps. Respect explicit stop boundaries: stop before order/post/purchase/submit.",
|
|
90
|
+
"Use agent_browser top-level sessionMode=fresh for launch-scoped flags; never put --session-mode in args. For signed-in/account-specific content, use requested/configured profiles, never assume --profile Default; on profile failures, run profiles/doctor and tell the user what to configure. Use --executable-path for configured Chromium. Profile content is model-visible.",
|
|
91
|
+
"For agent_browser artifacts, save the exact user path and verify details.artifactVerification/details.artifacts before claiming success. If close is blocked by details.promptGuard, save the required artifact first. record stop needs ffmpeg; close does not delete saved files; waited:timeout is not proof.",
|
|
92
|
+
"When agent_browser details.nextActions is present, prefer exact payloads over prose/guessed selectors. For dense snapshots, check Omitted high-value controls/details.data.highValueControlRefIds. For dashboards, verify scroll with screenshot/snapshot; if nothing moved, target the real scroll region.",
|
|
93
|
+
"For agent_browser extraction, prefer get title/url/text/html/value/attr/count or eval --stdin with plain expression, not console.log. Batch three or more known refs/selectors (e.g. [[\"get\",\"text\",\"@e1\"],[\"get\",\"text\",\"@e2\"]]); selector visibility warnings → visible @refs/nextActions.",
|
|
94
|
+
];
|
|
95
|
+
export function buildBrowserExecutablePathGuideline(executablePath) {
|
|
96
|
+
if (!executablePath)
|
|
97
|
+
return undefined;
|
|
98
|
+
return `agent_browser config sets browser.executablePath to ${JSON.stringify(executablePath)}; for fresh browser launches that should use that Chromium-compatible executable, add --executable-path ${JSON.stringify(executablePath)} with sessionMode:fresh. The upstream profiles command still lists Chrome profiles only; for non-Chrome Chromium login state, ask the user for an explicit profile/user-data directory path or inspect local setup with profiles/doctor before recommending a profile value.`;
|
|
99
|
+
}
|
|
100
|
+
export function buildBrowserDefaultProfileGuideline(profile) {
|
|
101
|
+
if (!profile || profile.policy === "explicit-only")
|
|
102
|
+
return undefined;
|
|
103
|
+
if (profile.policy === "always") {
|
|
104
|
+
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)} with policy always; use --profile ${JSON.stringify(profile.name)} with sessionMode:fresh when a fresh browser launch should use the configured profile, and treat profile content as model-visible user data.`;
|
|
105
|
+
}
|
|
106
|
+
return `agent_browser config sets browser.defaultProfile.name to ${JSON.stringify(profile.name)}; for signed-in/account-specific browser tasks, start with --profile ${JSON.stringify(profile.name)} plus sessionMode:fresh unless the user asks for a different profile.`;
|
|
107
|
+
}
|
|
108
|
+
export function buildToolPromptGuidelines(options) {
|
|
109
|
+
const browserDefaultProfileGuideline = buildBrowserDefaultProfileGuideline(options.browserDefaultProfile);
|
|
110
|
+
const browserExecutablePathGuideline = buildBrowserExecutablePathGuideline(options.browserExecutablePath);
|
|
111
|
+
return [
|
|
112
|
+
...TOOL_PROMPT_GUIDELINES_PREFIX,
|
|
113
|
+
...(options.docs ? [buildInstalledDocsGuideline(options.docs)] : []),
|
|
114
|
+
...RUNTIME_PROMPT_GUIDELINES,
|
|
115
|
+
...(browserExecutablePathGuideline ? [browserExecutablePathGuideline] : []),
|
|
116
|
+
...(browserDefaultProfileGuideline ? [browserDefaultProfileGuideline] : []),
|
|
117
|
+
...(options.includeWebSearch ? [WEB_SEARCH_PROMPT_GUIDELINE] : []),
|
|
118
|
+
TOOL_PROMPT_GUIDELINES_SUFFIX[0],
|
|
119
|
+
TOOL_PROMPT_GUIDELINES_SUFFIX[1],
|
|
120
|
+
];
|
|
121
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
|
|
3
|
+
* Responsibilities: Spawn the agent-browser subprocess, forward a curated environment surface, 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
|
+
* Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
|
|
5
|
+
* Usage: Called by the extension tool after argument validation and session planning are complete.
|
|
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
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { chmod, mkdir } from "node:fs/promises";
|
|
10
|
+
import { env as processEnv, platform as processPlatform } from "node:process";
|
|
11
|
+
import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, getFlagName } from "./argv-grammar.js";
|
|
12
|
+
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
13
|
+
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
14
|
+
const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
15
|
+
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
16
|
+
const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
|
|
17
|
+
const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
|
|
18
|
+
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
19
|
+
const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
|
|
20
|
+
const DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX = "/tmp/piab";
|
|
21
|
+
export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
|
|
22
|
+
const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
23
|
+
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
24
|
+
const EXIT_STDIO_GRACE_MS = 100;
|
|
25
|
+
const httpProxyEnvName = "http_proxy";
|
|
26
|
+
const httpsProxyEnvName = "https_proxy";
|
|
27
|
+
const allProxyEnvName = "all_proxy";
|
|
28
|
+
const noProxyEnvName = "no_proxy";
|
|
29
|
+
const INHERITED_ENV_NAMES = new Set([
|
|
30
|
+
"ALL_PROXY",
|
|
31
|
+
"APPDATA",
|
|
32
|
+
"CI",
|
|
33
|
+
"COLORTERM",
|
|
34
|
+
"COMSPEC",
|
|
35
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
36
|
+
"DISPLAY",
|
|
37
|
+
"FORCE_COLOR",
|
|
38
|
+
"HOME",
|
|
39
|
+
"HOMEDRIVE",
|
|
40
|
+
"HOMEPATH",
|
|
41
|
+
"HTTPS_PROXY",
|
|
42
|
+
"HTTP_PROXY",
|
|
43
|
+
"LANG",
|
|
44
|
+
"LC_ALL",
|
|
45
|
+
"LC_CTYPE",
|
|
46
|
+
"LOCALAPPDATA",
|
|
47
|
+
"LOGNAME",
|
|
48
|
+
"NO_COLOR",
|
|
49
|
+
"NO_PROXY",
|
|
50
|
+
"NODE_EXTRA_CA_CERTS",
|
|
51
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
52
|
+
"OS",
|
|
53
|
+
"PATH",
|
|
54
|
+
"PATHEXT",
|
|
55
|
+
"PWD",
|
|
56
|
+
"SHELL",
|
|
57
|
+
"SSL_CERT_DIR",
|
|
58
|
+
"SSL_CERT_FILE",
|
|
59
|
+
"SYSTEMROOT",
|
|
60
|
+
"TEMP",
|
|
61
|
+
"TERM",
|
|
62
|
+
"TMP",
|
|
63
|
+
"TMPDIR",
|
|
64
|
+
"TZ",
|
|
65
|
+
"USER",
|
|
66
|
+
"USERNAME",
|
|
67
|
+
"USERPROFILE",
|
|
68
|
+
"WAYLAND_DISPLAY",
|
|
69
|
+
"XAUTHORITY",
|
|
70
|
+
"AWS_ACCESS_KEY_ID",
|
|
71
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
72
|
+
"AWS_SESSION_TOKEN",
|
|
73
|
+
"AWS_PROFILE",
|
|
74
|
+
"AWS_REGION",
|
|
75
|
+
"AWS_DEFAULT_REGION",
|
|
76
|
+
httpProxyEnvName,
|
|
77
|
+
httpsProxyEnvName,
|
|
78
|
+
allProxyEnvName,
|
|
79
|
+
noProxyEnvName,
|
|
80
|
+
]);
|
|
81
|
+
const INHERITED_ENV_PREFIXES = [
|
|
82
|
+
"AGENT_BROWSER_",
|
|
83
|
+
"AGENTCORE_",
|
|
84
|
+
"AI_GATEWAY_",
|
|
85
|
+
"BROWSERBASE_",
|
|
86
|
+
"BROWSERLESS_",
|
|
87
|
+
"BROWSER_USE_",
|
|
88
|
+
"KERNEL_",
|
|
89
|
+
"XDG_",
|
|
90
|
+
];
|
|
91
|
+
function appendTail(text, addition, maxChars) {
|
|
92
|
+
const combined = text + addition;
|
|
93
|
+
return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
|
|
94
|
+
}
|
|
95
|
+
function quoteWindowsPowerShellArg(value) {
|
|
96
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
97
|
+
}
|
|
98
|
+
const WINDOWS_LEADING_GLOBAL_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
|
|
99
|
+
/** Exported for unit tests that lock Windows launcher argv ordering. */
|
|
100
|
+
export function reorderWindowsLeadingGlobalArgs(args) {
|
|
101
|
+
const leadingGlobals = [];
|
|
102
|
+
let index = 0;
|
|
103
|
+
while (index < args.length && args[index]?.startsWith("-")) {
|
|
104
|
+
const token = args[index];
|
|
105
|
+
const flagName = getFlagName(token);
|
|
106
|
+
leadingGlobals.push(token);
|
|
107
|
+
index += 1;
|
|
108
|
+
if (WINDOWS_LEADING_GLOBAL_VALUE_FLAGS.has(flagName) && !token.includes("=") && index < args.length) {
|
|
109
|
+
leadingGlobals.push(args[index]);
|
|
110
|
+
index += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(flagName) && ["true", "false"].includes(args[index] ?? "")) {
|
|
114
|
+
leadingGlobals.push(args[index]);
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (leadingGlobals.length === 0 || index >= args.length)
|
|
119
|
+
return args;
|
|
120
|
+
return [args[index], ...leadingGlobals, ...args.slice(index + 1)];
|
|
121
|
+
}
|
|
122
|
+
function buildAgentBrowserSpawnCommand(args) {
|
|
123
|
+
if (processPlatform !== "win32") {
|
|
124
|
+
return { command: "agent-browser", args };
|
|
125
|
+
}
|
|
126
|
+
const commandLine = ["&", "agent-browser", ...reorderWindowsLeadingGlobalArgs(args).map(quoteWindowsPowerShellArg)].join(" ");
|
|
127
|
+
return { command: "powershell.exe", args: ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", commandLine] };
|
|
128
|
+
}
|
|
129
|
+
function terminateSpawnedChild(child, signal) {
|
|
130
|
+
if (processPlatform === "win32" && child.pid) {
|
|
131
|
+
const killer = spawn("taskkill.exe", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
132
|
+
killer.on("error", () => undefined);
|
|
133
|
+
killer.unref();
|
|
134
|
+
}
|
|
135
|
+
child.kill(signal);
|
|
136
|
+
}
|
|
137
|
+
/** Exported for unit tests that lock subprocess exit-code precedence. */
|
|
138
|
+
export function resolveSpawnedChildExitCode(input) {
|
|
139
|
+
// Precedence: observed `close` code when present, then wrapper timeout (124), then
|
|
140
|
+
// post-`exit` fallback when inherited stdio delays `close`, then spawn failure (127).
|
|
141
|
+
if (input.closeCode !== null && input.closeCode !== undefined) {
|
|
142
|
+
return input.closeCode;
|
|
143
|
+
}
|
|
144
|
+
if (input.timedOut) {
|
|
145
|
+
return 124;
|
|
146
|
+
}
|
|
147
|
+
if (input.useExitFallback && input.exitCode !== null && input.exitCode !== undefined) {
|
|
148
|
+
return input.exitCode;
|
|
149
|
+
}
|
|
150
|
+
return input.spawnError ? 127 : 0;
|
|
151
|
+
}
|
|
152
|
+
function watchSpawnedChildCompletion(child, options) {
|
|
153
|
+
let exited = false;
|
|
154
|
+
let exitCode = null;
|
|
155
|
+
let postExitTimer;
|
|
156
|
+
// `completed` suppresses duplicate exit/close callbacks; `settled` in `finish` guards async spill cleanup.
|
|
157
|
+
let completed = false;
|
|
158
|
+
const complete = (closeCode) => {
|
|
159
|
+
if (completed)
|
|
160
|
+
return;
|
|
161
|
+
completed = true;
|
|
162
|
+
if (postExitTimer) {
|
|
163
|
+
clearTimeout(postExitTimer);
|
|
164
|
+
postExitTimer = undefined;
|
|
165
|
+
}
|
|
166
|
+
const context = options.getContext();
|
|
167
|
+
options.onComplete(resolveSpawnedChildExitCode({
|
|
168
|
+
closeCode,
|
|
169
|
+
exitCode,
|
|
170
|
+
useExitFallback: exited,
|
|
171
|
+
timedOut: context.timedOut,
|
|
172
|
+
spawnError: context.spawnError,
|
|
173
|
+
}));
|
|
174
|
+
};
|
|
175
|
+
child.once("exit", (code) => {
|
|
176
|
+
exited = true;
|
|
177
|
+
exitCode = code;
|
|
178
|
+
postExitTimer = setTimeout(() => {
|
|
179
|
+
destroySpawnedChildStreams(child);
|
|
180
|
+
complete(undefined);
|
|
181
|
+
}, options.graceMs);
|
|
182
|
+
postExitTimer.unref?.();
|
|
183
|
+
});
|
|
184
|
+
child.once("close", (code) => {
|
|
185
|
+
complete(code);
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
clear: () => {
|
|
189
|
+
if (postExitTimer) {
|
|
190
|
+
clearTimeout(postExitTimer);
|
|
191
|
+
postExitTimer = undefined;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function destroySpawnedChildStreams(child) {
|
|
197
|
+
child.stdin?.destroy();
|
|
198
|
+
child.stdout?.destroy();
|
|
199
|
+
child.stderr?.destroy();
|
|
200
|
+
}
|
|
201
|
+
function parsePositiveIntegerEnv(value) {
|
|
202
|
+
if (value === undefined || !/^\d+$/.test(value.trim())) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const parsed = Number(value.trim());
|
|
206
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
207
|
+
}
|
|
208
|
+
function clampUpstreamDefaultTimeout(childEnv) {
|
|
209
|
+
const requestedTimeout = parsePositiveIntegerEnv(childEnv[AGENT_BROWSER_DEFAULT_TIMEOUT_ENV]);
|
|
210
|
+
if (requestedTimeout === undefined || requestedTimeout > SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS) {
|
|
211
|
+
childEnv[AGENT_BROWSER_DEFAULT_TIMEOUT_ENV] = String(SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function getAgentBrowserProcessTimeoutMs(env = processEnv) {
|
|
215
|
+
return parsePositiveIntegerEnv(env[PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV]) ?? DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS;
|
|
216
|
+
}
|
|
217
|
+
export function getAgentBrowserSocketDir(platform = processPlatform, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
|
|
218
|
+
if (platform === "win32") {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
return `${DEFAULT_AGENT_BROWSER_SOCKET_DIR_PREFIX}${typeof uid === "number" ? `-${uid}` : ""}`;
|
|
222
|
+
}
|
|
223
|
+
async function ensureAgentBrowserSocketDir(socketDir) {
|
|
224
|
+
try {
|
|
225
|
+
await mkdir(socketDir, { recursive: true, mode: 0o700 });
|
|
226
|
+
await chmod(socketDir, 0o700).catch(() => undefined);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function getChildEnvName(name) {
|
|
234
|
+
if (processPlatform === "win32") {
|
|
235
|
+
const upperName = name.toUpperCase();
|
|
236
|
+
if (INHERITED_ENV_NAMES.has(upperName))
|
|
237
|
+
return upperName;
|
|
238
|
+
return INHERITED_ENV_PREFIXES.some((prefix) => upperName.startsWith(prefix)) ? upperName : undefined;
|
|
239
|
+
}
|
|
240
|
+
if (INHERITED_ENV_NAMES.has(name) || INHERITED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
241
|
+
return name;
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
|
|
246
|
+
const childEnv = {};
|
|
247
|
+
for (const [name, value] of Object.entries(baseEnv)) {
|
|
248
|
+
const childName = getChildEnvName(name);
|
|
249
|
+
if (value !== undefined && childName) {
|
|
250
|
+
childEnv[childName] = value;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!overrides) {
|
|
254
|
+
clampUpstreamDefaultTimeout(childEnv);
|
|
255
|
+
return childEnv;
|
|
256
|
+
}
|
|
257
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
258
|
+
const childName = getChildEnvName(name) ?? name;
|
|
259
|
+
if (value === undefined) {
|
|
260
|
+
delete childEnv[childName];
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
childEnv[childName] = value;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
clampUpstreamDefaultTimeout(childEnv);
|
|
267
|
+
return childEnv;
|
|
268
|
+
}
|
|
269
|
+
export async function runAgentBrowserProcess(options) {
|
|
270
|
+
const { args, cwd, env, signal, stdin } = options;
|
|
271
|
+
const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
|
|
272
|
+
const explicitSocketDir = env?.[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
273
|
+
let effectiveEnv = explicitSocketDir === undefined ? { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : env;
|
|
274
|
+
const requestedSocketDir = explicitSocketDir ?? getAgentBrowserSocketDir();
|
|
275
|
+
if (requestedSocketDir && (await ensureAgentBrowserSocketDir(requestedSocketDir))) {
|
|
276
|
+
effectiveEnv = { ...env, [AGENT_BROWSER_SOCKET_DIR_ENV]: requestedSocketDir };
|
|
277
|
+
}
|
|
278
|
+
return await new Promise((resolve) => {
|
|
279
|
+
let aborted = false;
|
|
280
|
+
let settled = false;
|
|
281
|
+
let spawnError;
|
|
282
|
+
let stderr = "";
|
|
283
|
+
let stdoutBuffers = [];
|
|
284
|
+
let stdoutBufferedBytes = 0;
|
|
285
|
+
let stdoutTail = "";
|
|
286
|
+
let stdoutSpillHandle;
|
|
287
|
+
let stdoutSpillPath;
|
|
288
|
+
let pendingStdoutWrite = Promise.resolve();
|
|
289
|
+
let stdoutSpillError;
|
|
290
|
+
let killTimer;
|
|
291
|
+
let timeoutTimer;
|
|
292
|
+
let abortListener;
|
|
293
|
+
let timedOut = false;
|
|
294
|
+
let completionWatcher;
|
|
295
|
+
const queueStdoutChunk = (buffer) => {
|
|
296
|
+
stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
|
|
297
|
+
if (stdoutSpillError)
|
|
298
|
+
return;
|
|
299
|
+
if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
300
|
+
stdoutBuffers.push(buffer);
|
|
301
|
+
stdoutBufferedBytes += buffer.length;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
pendingStdoutWrite = pendingStdoutWrite
|
|
305
|
+
.then(async () => {
|
|
306
|
+
if (stdoutSpillError)
|
|
307
|
+
return;
|
|
308
|
+
if (!stdoutSpillHandle || !stdoutSpillPath) {
|
|
309
|
+
const tempFile = await openSecureTempFile(PROCESS_STDOUT_SPILL_FILE_PREFIX, ".json");
|
|
310
|
+
stdoutSpillHandle = tempFile.fileHandle;
|
|
311
|
+
stdoutSpillPath = tempFile.path;
|
|
312
|
+
if (stdoutBuffers.length > 0) {
|
|
313
|
+
await writeSecureTempChunk({
|
|
314
|
+
content: Buffer.concat(stdoutBuffers),
|
|
315
|
+
fileHandle: stdoutSpillHandle,
|
|
316
|
+
path: stdoutSpillPath,
|
|
317
|
+
});
|
|
318
|
+
stdoutBuffers = [];
|
|
319
|
+
stdoutBufferedBytes = 0;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
await writeSecureTempChunk({ content: buffer, fileHandle: stdoutSpillHandle, path: stdoutSpillPath });
|
|
323
|
+
})
|
|
324
|
+
.catch((error) => {
|
|
325
|
+
stdoutSpillError = error instanceof Error ? error : new Error(String(error));
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
const removeAbortListener = () => {
|
|
329
|
+
if (!signal || !abortListener)
|
|
330
|
+
return;
|
|
331
|
+
signal.removeEventListener("abort", abortListener);
|
|
332
|
+
abortListener = undefined;
|
|
333
|
+
};
|
|
334
|
+
const finish = (exitCode) => {
|
|
335
|
+
if (settled)
|
|
336
|
+
return;
|
|
337
|
+
settled = true;
|
|
338
|
+
void pendingStdoutWrite.finally(async () => {
|
|
339
|
+
removeAbortListener();
|
|
340
|
+
if (killTimer) {
|
|
341
|
+
clearTimeout(killTimer);
|
|
342
|
+
}
|
|
343
|
+
if (timeoutTimer) {
|
|
344
|
+
clearTimeout(timeoutTimer);
|
|
345
|
+
}
|
|
346
|
+
completionWatcher?.clear();
|
|
347
|
+
if (stdoutSpillHandle) {
|
|
348
|
+
await stdoutSpillHandle.close().catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
if (!spawnError && stdoutSpillError) {
|
|
351
|
+
spawnError = stdoutSpillError;
|
|
352
|
+
}
|
|
353
|
+
// Idempotent teardown: streams may already be destroyed by the post-`exit` fallback.
|
|
354
|
+
destroySpawnedChildStreams(child);
|
|
355
|
+
resolve({
|
|
356
|
+
aborted,
|
|
357
|
+
exitCode,
|
|
358
|
+
spawnError,
|
|
359
|
+
stderr,
|
|
360
|
+
stdout: stdoutSpillPath ? stdoutTail : Buffer.concat(stdoutBuffers).toString("utf8"),
|
|
361
|
+
stdoutSpillPath,
|
|
362
|
+
timedOut,
|
|
363
|
+
timeoutMs: timedOut ? timeoutMs : undefined,
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
const spawnCommand = buildAgentBrowserSpawnCommand(args);
|
|
368
|
+
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
369
|
+
cwd,
|
|
370
|
+
env: buildAgentBrowserProcessEnv(processEnv, effectiveEnv),
|
|
371
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
372
|
+
});
|
|
373
|
+
const terminateChild = (reason) => {
|
|
374
|
+
if (settled)
|
|
375
|
+
return;
|
|
376
|
+
if (reason === "abort") {
|
|
377
|
+
aborted = true;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
timedOut = true;
|
|
381
|
+
}
|
|
382
|
+
terminateSpawnedChild(child, "SIGTERM");
|
|
383
|
+
killTimer = setTimeout(() => {
|
|
384
|
+
terminateSpawnedChild(child, "SIGKILL");
|
|
385
|
+
}, 2_000);
|
|
386
|
+
};
|
|
387
|
+
const recordStdinError = (error) => {
|
|
388
|
+
const stdinError = error instanceof Error ? error : new Error(String(error));
|
|
389
|
+
const errorCode = stdinError.code;
|
|
390
|
+
if (errorCode === "EPIPE" || errorCode === "EOF" || errorCode === "ERR_STREAM_DESTROYED") {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (!spawnError) {
|
|
394
|
+
spawnError = stdinError;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const writeChildStdin = () => {
|
|
398
|
+
if (aborted || signal?.aborted) {
|
|
399
|
+
child.stdin.destroy();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
if (stdin) {
|
|
404
|
+
child.stdin.write(stdin);
|
|
405
|
+
}
|
|
406
|
+
child.stdin.end();
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
recordStdinError(error);
|
|
410
|
+
child.stdin.destroy();
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
child.stdin.on("error", recordStdinError);
|
|
414
|
+
child.once("error", (error) => {
|
|
415
|
+
spawnError = error instanceof Error ? error : new Error(String(error));
|
|
416
|
+
finish(resolveSpawnedChildExitCode({
|
|
417
|
+
useExitFallback: false,
|
|
418
|
+
timedOut,
|
|
419
|
+
spawnError,
|
|
420
|
+
}));
|
|
421
|
+
});
|
|
422
|
+
completionWatcher = watchSpawnedChildCompletion(child, {
|
|
423
|
+
graceMs: EXIT_STDIO_GRACE_MS,
|
|
424
|
+
onComplete: finish,
|
|
425
|
+
getContext: () => ({ timedOut, spawnError }),
|
|
426
|
+
});
|
|
427
|
+
child.stdout.on("data", (chunk) => {
|
|
428
|
+
queueStdoutChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
429
|
+
});
|
|
430
|
+
child.stderr.on("data", (chunk) => {
|
|
431
|
+
stderr = appendTail(stderr, chunk.toString(), MAX_BUFFERED_STDERR_CHARS);
|
|
432
|
+
});
|
|
433
|
+
if (timeoutMs > 0) {
|
|
434
|
+
timeoutTimer = setTimeout(() => terminateChild("timeout"), timeoutMs);
|
|
435
|
+
timeoutTimer.unref?.();
|
|
436
|
+
}
|
|
437
|
+
if (signal) {
|
|
438
|
+
if (signal.aborted) {
|
|
439
|
+
terminateChild("abort");
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
abortListener = () => terminateChild("abort");
|
|
443
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
writeChildStdin();
|
|
447
|
+
});
|
|
448
|
+
}
|