@ai-agent-forge/agent-forge 0.88.4 → 0.88.6
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 +30 -0
- package/bundled-plugins/agent-loop-policy/agent-forge.json +1 -1
- package/bundled-plugins/agent-loop-policy/plugin.json +1 -1
- package/bundled-plugins/alignment/agent-forge.json +19 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts +12 -0
- package/bundled-plugins/alignment/dist/baseline.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/baseline.js +68 -0
- package/bundled-plugins/alignment/dist/baseline.js.map +1 -0
- package/bundled-plugins/alignment/dist/engine.d.ts +102 -0
- package/bundled-plugins/alignment/dist/engine.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/engine.js +654 -0
- package/bundled-plugins/alignment/dist/engine.js.map +1 -0
- package/bundled-plugins/alignment/dist/entry.d.ts +14 -0
- package/bundled-plugins/alignment/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/entry.js +23 -0
- package/bundled-plugins/alignment/dist/entry.js.map +1 -0
- package/bundled-plugins/alignment/dist/export.d.ts +15 -0
- package/bundled-plugins/alignment/dist/export.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/export.js +122 -0
- package/bundled-plugins/alignment/dist/export.js.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts +33 -0
- package/bundled-plugins/alignment/dist/http-server.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/http-server.js +119 -0
- package/bundled-plugins/alignment/dist/http-server.js.map +1 -0
- package/bundled-plugins/alignment/dist/index.d.ts +24 -0
- package/bundled-plugins/alignment/dist/index.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/index.js +719 -0
- package/bundled-plugins/alignment/dist/index.js.map +1 -0
- package/bundled-plugins/alignment/dist/page.d.ts +15 -0
- package/bundled-plugins/alignment/dist/page.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/page.js +832 -0
- package/bundled-plugins/alignment/dist/page.js.map +1 -0
- package/bundled-plugins/alignment/dist/render.d.ts +40 -0
- package/bundled-plugins/alignment/dist/render.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/render.js +194 -0
- package/bundled-plugins/alignment/dist/render.js.map +1 -0
- package/bundled-plugins/alignment/dist/types.d.ts +217 -0
- package/bundled-plugins/alignment/dist/types.d.ts.map +1 -0
- package/bundled-plugins/alignment/dist/types.js +30 -0
- package/bundled-plugins/alignment/dist/types.js.map +1 -0
- package/bundled-plugins/alignment/plugin.json +12 -0
- package/bundled-plugins/browser-playwright/MANUAL.md +60 -0
- package/bundled-plugins/browser-playwright/agent-forge.json +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts +25 -0
- package/bundled-plugins/browser-playwright/dist/capability.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/capability.js +612 -0
- package/bundled-plugins/browser-playwright/dist/capability.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts +13 -0
- package/bundled-plugins/browser-playwright/dist/entry.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/entry.js +14 -0
- package/bundled-plugins/browser-playwright/dist/entry.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts +7 -0
- package/bundled-plugins/browser-playwright/dist/index.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/index.js +5 -0
- package/bundled-plugins/browser-playwright/dist/index.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts +69 -0
- package/bundled-plugins/browser-playwright/dist/operations.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/operations.js +149 -0
- package/bundled-plugins/browser-playwright/dist/operations.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts +275 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js +953 -0
- package/bundled-plugins/browser-playwright/dist/playwright-driver.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts +88 -0
- package/bundled-plugins/browser-playwright/dist/setup.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/setup.js +252 -0
- package/bundled-plugins/browser-playwright/dist/setup.js.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts +270 -0
- package/bundled-plugins/browser-playwright/dist/types.d.ts.map +1 -0
- package/bundled-plugins/browser-playwright/dist/types.js +2 -0
- package/bundled-plugins/browser-playwright/dist/types.js.map +1 -0
- package/bundled-plugins/browser-playwright/plugin.json +20 -0
- package/bundled-plugins/coding-tools/agent-forge.json +1 -1
- package/bundled-plugins/coding-tools/dist/bash.d.ts +2 -7
- package/bundled-plugins/coding-tools/dist/bash.d.ts.map +1 -1
- package/bundled-plugins/coding-tools/dist/bash.js +2 -34
- package/bundled-plugins/coding-tools/dist/bash.js.map +1 -1
- package/bundled-plugins/coding-tools/plugin.json +1 -1
- package/bundled-plugins/compaction-policy/agent-forge.json +1 -1
- package/bundled-plugins/compaction-policy/plugin.json +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.d.ts.map +1 -1
- package/bundled-plugins/mcp-http/dist/mcp-http.js +84 -31
- package/bundled-plugins/mcp-http/dist/mcp-http.js.map +1 -1
- package/bundled-plugins/overflow-policy/agent-forge.json +1 -1
- package/bundled-plugins/overflow-policy/plugin.json +1 -1
- package/bundled-plugins/provider-failover/agent-forge.json +1 -1
- package/bundled-plugins/provider-failover/plugin.json +1 -1
- package/bundled-plugins/retry-policy/agent-forge.json +1 -1
- package/bundled-plugins/retry-policy/plugin.json +1 -1
- package/dist/bundle/.build-manifest.json +60 -58
- package/dist/bundle/chunks/{anthropic-messages-3FJHRAJL.js → anthropic-messages-NL4Y6WPB.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-GCL47IDS.js → azure-openai-responses-CPXC42HO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/chunk-5LRQFZFE.js +2 -0
- package/dist/bundle/chunks/{chunk-HTEVUY7F.js → chunk-7HSJPQKN.js} +1 -1
- package/dist/bundle/chunks/{chunk-AC2FIS4G.js → chunk-EDO7Q6XR.js} +1 -1
- package/dist/bundle/chunks/{chunk-755VNWIF.js → chunk-ISLHFRER.js} +6 -6
- package/dist/bundle/chunks/{chunk-HV64ASMT.js → chunk-K3S6GTL7.js} +1 -1
- package/dist/bundle/chunks/{chunk-HEL6TAWE.js → chunk-POLDAUYA.js} +37 -36
- package/dist/bundle/chunks/{chunk-V3ACW7VD.js → chunk-WJSKB3FC.js} +1 -1
- package/dist/bundle/chunks/{chunk-32YDZI4J.js → chunk-ZS5JEI76.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-6BMXWA44.js → google-generative-ai-UMZ53EFA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-6H7NLRTK.js → google-vertex-ZNR5NS6H.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{mistral-conversations-IXUHSFSJ.js → mistral-conversations-IT66WUNN.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-YHUAEVCM.js → openai-codex-responses-FOVCVG6I.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-ULQCFC72.js → openai-completions-FLQ7CBOL.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-NLLAJDPS.js → openai-responses-ABGYHS2T.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-EJTI2EJ3.js → pi-messages-6K5W6LUN.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-TQWEGNNZ.js → virtual-modules-2QGILFV7.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/client.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/client/remote-session.d.ts.map +1 -1
- package/dist/client/remote-session.js +65 -6
- package/dist/client/remote-session.js.map +1 -1
- package/dist/core/agent-session-store.d.ts.map +1 -1
- package/dist/core/agent-session-store.js +20 -3
- package/dist/core/agent-session-store.js.map +1 -1
- package/dist/core/extensions/agent-composition.d.ts.map +1 -1
- package/dist/core/extensions/agent-composition.js +20 -1
- package/dist/core/extensions/agent-composition.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/core-errors.js +1 -0
- package/dist/core/i18n/locales/zh-cn/core-errors.js.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.d.ts.map +1 -1
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js +10 -3
- package/dist/core/i18n/locales/zh-cn/interactive-mode.js.map +1 -1
- package/dist/core/keybindings.d.ts +35 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +34 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +62 -15
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +11 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +2 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/capability-mount.d.ts.map +1 -1
- package/dist/core/tools/capability-mount.js +4 -21
- package/dist/core/tools/capability-mount.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +2 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +25 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/changelog-pager.d.ts +53 -0
- package/dist/modes/interactive/components/changelog-pager.d.ts.map +1 -0
- package/dist/modes/interactive/components/changelog-pager.js +239 -0
- package/dist/modes/interactive/components/changelog-pager.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.d.ts +41 -11
- package/dist/modes/interactive/components/tool-run-group.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-run-group.js +126 -14
- package/dist/modes/interactive/components/tool-run-group.js.map +1 -1
- package/dist/modes/interactive/interactive-commands.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-commands.js +23 -13
- package/dist/modes/interactive/interactive-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-events.d.ts +14 -4
- package/dist/modes/interactive/interactive-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-events.js +94 -12
- package/dist/modes/interactive/interactive-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +6 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts +2 -1
- package/dist/plugins/ecosystem/node-local-package-source.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-local-package-source.js +110 -4
- package/dist/plugins/ecosystem/node-local-package-source.js.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-package-install-host.js +106 -39
- package/dist/plugins/ecosystem/node-package-install-host.js.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.d.ts.map +1 -1
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js +59 -2
- package/dist/plugins/ecosystem/node-plugin-bootstrap.js.map +1 -1
- package/dist/suites/builtin-suites.d.ts.map +1 -1
- package/dist/suites/builtin-suites.js +2 -2
- package/dist/suites/builtin-suites.js.map +1 -1
- package/dist/utils/changelog.d.ts +27 -0
- package/dist/utils/changelog.d.ts.map +1 -1
- package/dist/utils/changelog.js +110 -0
- package/dist/utils/changelog.js.map +1 -1
- package/docs/keybindings.md +14 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/extensions/with-deps/plugin.json +1 -1
- package/package.json +1 -1
- package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright-backed driver for the browser capability.
|
|
3
|
+
*
|
|
4
|
+
* Playwright is resolved lazily at connect time through
|
|
5
|
+
* `createRequire(<browser-deps>/package.json)` — the deliberate optional-heavy
|
|
6
|
+
* dependency load point; this package never imports Playwright types, so the
|
|
7
|
+
* surface used here is declared structurally below. A missing module throws
|
|
8
|
+
* the structured guidance message (exact install commands plus the
|
|
9
|
+
* `browser_setup` hint), which the capability layer surfaces verbatim.
|
|
10
|
+
*
|
|
11
|
+
* Bounded results: aria snapshots are capped at 16KiB, DOM captures and all
|
|
12
|
+
* JSON payloads (console/network entries, performance metrics, evaluate
|
|
13
|
+
* results, storage/tab/viewport state) at 64KiB, screenshots at 4MiB of PNG
|
|
14
|
+
* bytes. Styles capture is declared `not_supported_in_this_build` (parameter
|
|
15
|
+
* stays accepted for contract compatibility). Console and network evidence
|
|
16
|
+
* come from 200-entry ring buffers attached to every opened page.
|
|
17
|
+
*
|
|
18
|
+
* Built-in policies (no dedicated tools): `dialog` events are auto-accepted
|
|
19
|
+
* and logged into the console ring as a `[dialog]` entry; `download` events
|
|
20
|
+
* are saved into `<agentDir>/browser-deps/downloads/` and the most recent
|
|
21
|
+
* download filename is attached to snapshot results.
|
|
22
|
+
*/
|
|
23
|
+
import { mkdirSync } from "node:fs";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { buildPluginResultV1, } from "@agent-forge/plugin-sdk";
|
|
27
|
+
import { BROWSER_DEFAULT_EVALUATE_TIMEOUT_MS_V1, clampDragStepsV1, clampViewportDimensionV1, clampWaitMsV1, filterConsoleEntriesV1, filterNetworkEntriesV1, parseScrollDirectiveV1, parseUploadPathsV1, scrollWheelDeltaV1, serializeJsonPayloadV1, shapePerformanceV1, waitMissingInput, } from "./operations.js";
|
|
28
|
+
import { BROWSER_BROWSERS_DIR_NAME_V1, boundedPreviewV1, browserDepsDirFor, installGuidanceMessage, resolvePlaywrightModule, setupBrowserDepsPlaywrightV1, } from "./setup.js";
|
|
29
|
+
export const BROWSER_MAX_SNAPSHOT_CHARS_V1 = 16 * 1024;
|
|
30
|
+
export const BROWSER_MAX_DOM_CHARS_V1 = 64 * 1024;
|
|
31
|
+
export const BROWSER_MAX_EVIDENCE_CHARS_V1 = 64 * 1024;
|
|
32
|
+
export const BROWSER_MAX_SCREENSHOT_BYTES_V1 = 4 * 1024 * 1024;
|
|
33
|
+
export const BROWSER_RING_BUFFER_SIZE_V1 = 200;
|
|
34
|
+
export const BROWSER_DOWNLOADS_DIR_NAME_V1 = "downloads";
|
|
35
|
+
const BROWSER_CONDITION_LABEL_CHARS_V1 = 256;
|
|
36
|
+
const placeholder = (text) => (text.trim() === "" ? "(empty)" : text);
|
|
37
|
+
const sanitizeUrlV1 = (value) => {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(value);
|
|
40
|
+
return `${parsed.origin}${parsed.pathname.slice(0, 512)}`;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return value.slice(0, 512);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const conditionLabel = (condition, index) => {
|
|
47
|
+
const raw = condition.kind === "url"
|
|
48
|
+
? `url ${condition.equals !== undefined ? `equals ${JSON.stringify(condition.equals)}` : ""}${condition.contains !== undefined ? ` contains ${JSON.stringify(condition.contains)}` : ""}`
|
|
49
|
+
: condition.kind === "element"
|
|
50
|
+
? `element ${condition.locator.kind}=${JSON.stringify(condition.locator.value)} state ${condition.state}`
|
|
51
|
+
: condition.kind === "text"
|
|
52
|
+
? `text contains ${JSON.stringify(condition.contains)}`
|
|
53
|
+
: condition.kind === "console"
|
|
54
|
+
? `console contains ${JSON.stringify(condition.contains)}`
|
|
55
|
+
: `network containsUrl ${JSON.stringify(condition.containsUrl)}`;
|
|
56
|
+
return `${index + 1}: ${raw.trim()}`.slice(0, BROWSER_CONDITION_LABEL_CHARS_V1);
|
|
57
|
+
};
|
|
58
|
+
const locatorLabel = (locator) => `${locator.kind}=${locator.value.slice(0, 64)}${locator.name === undefined ? "" : ` name=${locator.name.slice(0, 64)}`}`;
|
|
59
|
+
/**
|
|
60
|
+
* In-page performance collection evaluated through `page.evaluate`. Must stay a
|
|
61
|
+
* closure-free function object: real Playwright stringifies it and runs it in
|
|
62
|
+
* the page. Passing an equivalent `() => {...}` *string* is silently wrong —
|
|
63
|
+
* real Playwright evaluates the string, serializes the resulting function
|
|
64
|
+
* object, and yields `undefined` on the Node side.
|
|
65
|
+
*/
|
|
66
|
+
const collectPerformanceInPage = () => {
|
|
67
|
+
const entries = (type) => performance.getEntriesByType(type);
|
|
68
|
+
const nav = entries("navigation")[0] ?? null;
|
|
69
|
+
const paints = entries("paint").map((entry) => ({ name: String(entry.name), startTimeMs: entry.startTime }));
|
|
70
|
+
const resources = entries("resource").map((entry) => ({
|
|
71
|
+
url: String(entry.name),
|
|
72
|
+
durationMs: entry.duration,
|
|
73
|
+
transferBytes: entry.transferSize,
|
|
74
|
+
}));
|
|
75
|
+
return {
|
|
76
|
+
navigation: nav === null
|
|
77
|
+
? null
|
|
78
|
+
: {
|
|
79
|
+
ttfbMs: nav.responseStart - nav.startTime,
|
|
80
|
+
domContentLoadedMs: nav.domContentLoadedEventEnd - nav.startTime,
|
|
81
|
+
loadMs: nav.loadEventEnd - nav.startTime,
|
|
82
|
+
},
|
|
83
|
+
paints,
|
|
84
|
+
resources,
|
|
85
|
+
domNodes: document.getElementsByTagName("*").length,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
+
/**
|
|
90
|
+
* `page.evaluate` with a wall-clock escape: Playwright itself cannot cancel a
|
|
91
|
+
* hung script, so the race rejects after `timeoutMs` and leaves the page-side
|
|
92
|
+
* evaluation to finish on its own.
|
|
93
|
+
*/
|
|
94
|
+
async function evaluateWithTimeout(page, expression, arg, timeoutMs) {
|
|
95
|
+
let timer;
|
|
96
|
+
const timeout = new Promise((_, reject) => {
|
|
97
|
+
timer = setTimeout(() => reject(new Error(`browser_evaluate timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([page.evaluate(expression, arg), timeout]);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
if (timer !== undefined)
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const isTimeoutError = (error) => error instanceof Error && error.name === "TimeoutError";
|
|
109
|
+
export class PlaywrightBrowserDriver {
|
|
110
|
+
depsDir;
|
|
111
|
+
headless;
|
|
112
|
+
resolveModule;
|
|
113
|
+
loadPlaywright;
|
|
114
|
+
ensureDir;
|
|
115
|
+
browser;
|
|
116
|
+
context;
|
|
117
|
+
page;
|
|
118
|
+
pages = [];
|
|
119
|
+
pageIndex = 0;
|
|
120
|
+
consoleRing = [];
|
|
121
|
+
networkRing = [];
|
|
122
|
+
lastDownload;
|
|
123
|
+
constructor(options = {}) {
|
|
124
|
+
this.depsDir =
|
|
125
|
+
options.browserDepsDir ?? (options.agentDir === undefined ? "" : browserDepsDirFor(options.agentDir));
|
|
126
|
+
this.headless = options.headless ?? true;
|
|
127
|
+
this.resolveModule = options.resolvePlaywright ?? resolvePlaywrightModule;
|
|
128
|
+
this.loadPlaywright = options.loadPlaywright;
|
|
129
|
+
this.ensureDir = options.ensureDir ?? ((dir) => mkdirSync(dir, { recursive: true }));
|
|
130
|
+
}
|
|
131
|
+
/** Resolves Playwright, launches Chromium, and opens the context. Idempotent. */
|
|
132
|
+
async connect() {
|
|
133
|
+
if (this.browser !== undefined)
|
|
134
|
+
return;
|
|
135
|
+
const playwright = this.loadModule();
|
|
136
|
+
// Point the Playwright registry at the same isolated directory the setup
|
|
137
|
+
// flow installs browsers into; never override an explicit user setting.
|
|
138
|
+
// Without this, `browser_setup` binaries land in <deps>/browsers while
|
|
139
|
+
// launch silently looks in the shared OS cache and fails on a fresh host.
|
|
140
|
+
if (this.depsDir !== "") {
|
|
141
|
+
process.env.PLAYWRIGHT_BROWSERS_PATH ??= join(this.depsDir, BROWSER_BROWSERS_DIR_NAME_V1);
|
|
142
|
+
}
|
|
143
|
+
let browser;
|
|
144
|
+
try {
|
|
145
|
+
browser = await playwright.chromium.launch({ headless: this.headless });
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : "";
|
|
149
|
+
// The module resolves but the browser binaries were never installed.
|
|
150
|
+
if (/Executable doesn't exist|playwright install/i.test(message)) {
|
|
151
|
+
throw new Error(installGuidanceMessage(this.depsDir), {
|
|
152
|
+
cause: error instanceof Error ? error : undefined,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
this.browser = browser;
|
|
158
|
+
this.context = await browser.newContext();
|
|
159
|
+
}
|
|
160
|
+
/** Idempotent best-effort teardown; never throws. */
|
|
161
|
+
close() {
|
|
162
|
+
const browser = this.browser;
|
|
163
|
+
this.browser = undefined;
|
|
164
|
+
this.context = undefined;
|
|
165
|
+
this.page = undefined;
|
|
166
|
+
this.pages = [];
|
|
167
|
+
this.pageIndex = 0;
|
|
168
|
+
this.consoleRing = [];
|
|
169
|
+
this.networkRing = [];
|
|
170
|
+
this.lastDownload = undefined;
|
|
171
|
+
if (browser === undefined)
|
|
172
|
+
return;
|
|
173
|
+
void browser.close().catch(() => undefined);
|
|
174
|
+
}
|
|
175
|
+
async navigate(input, options = {}) {
|
|
176
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
177
|
+
if (options.signal?.aborted)
|
|
178
|
+
return cancelled(operationId);
|
|
179
|
+
await this.connect();
|
|
180
|
+
const page = this.page ?? (await this.openPage());
|
|
181
|
+
await page.goto(input.url, { waitUntil: input.waitUntil ?? "load" });
|
|
182
|
+
return buildPluginResultV1({
|
|
183
|
+
version: 1,
|
|
184
|
+
operationId,
|
|
185
|
+
status: "succeeded",
|
|
186
|
+
summary: `Navigated to ${sanitizeUrlV1(page.url())}`,
|
|
187
|
+
artifacts: [],
|
|
188
|
+
evidence: [],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async snapshot(input = {}, options = {}) {
|
|
192
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
193
|
+
if (options.signal?.aborted)
|
|
194
|
+
return cancelled(operationId);
|
|
195
|
+
const page = this.page;
|
|
196
|
+
if (page === undefined)
|
|
197
|
+
return sessionRequired(operationId);
|
|
198
|
+
const evidence = evidenceBuilder(operationId);
|
|
199
|
+
const parts = [];
|
|
200
|
+
if (input.includeStyles === true)
|
|
201
|
+
parts.push("styles: not_supported_in_this_build");
|
|
202
|
+
if (input.includeA11y !== false) {
|
|
203
|
+
evidence.add("structure", placeholder(boundedPreviewV1(await page.locator("body").ariaSnapshot(), BROWSER_MAX_SNAPSHOT_CHARS_V1)));
|
|
204
|
+
parts.push("a11y");
|
|
205
|
+
}
|
|
206
|
+
if (input.includeDom === true) {
|
|
207
|
+
evidence.add("dom", placeholder(boundedPreviewV1(await page.content(), BROWSER_MAX_DOM_CHARS_V1)));
|
|
208
|
+
parts.push("dom");
|
|
209
|
+
}
|
|
210
|
+
if (input.includeScreenshot === true) {
|
|
211
|
+
const png = await page.screenshot({ type: "png" });
|
|
212
|
+
if (png.byteLength > BROWSER_MAX_SCREENSHOT_BYTES_V1) {
|
|
213
|
+
return failed(operationId, "screenshot_too_large", "Screenshot exceeds the 4MiB evidence budget");
|
|
214
|
+
}
|
|
215
|
+
evidence.add("screenshot", `data:image/png;base64,${png.toString("base64")}`);
|
|
216
|
+
parts.push("screenshot");
|
|
217
|
+
}
|
|
218
|
+
if (this.lastDownload !== undefined) {
|
|
219
|
+
const filename = this.lastDownload.slice(0, 256);
|
|
220
|
+
evidence.add("log", `download: ${filename}`);
|
|
221
|
+
parts.push(`download: ${filename}`);
|
|
222
|
+
}
|
|
223
|
+
return buildPluginResultV1({
|
|
224
|
+
version: 1,
|
|
225
|
+
operationId,
|
|
226
|
+
status: "succeeded",
|
|
227
|
+
summary: `Captured browser snapshot (${parts.length === 0 ? "none" : parts.join(", ")})`,
|
|
228
|
+
artifacts: [],
|
|
229
|
+
evidence: evidence.refs,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async locatorAction(input, options = {}) {
|
|
233
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
234
|
+
if (options.signal?.aborted)
|
|
235
|
+
return cancelled(operationId);
|
|
236
|
+
const page = this.page;
|
|
237
|
+
if (page === undefined)
|
|
238
|
+
return sessionRequired(operationId);
|
|
239
|
+
const missing = actionMissingInput(input);
|
|
240
|
+
if (missing !== undefined) {
|
|
241
|
+
return failed(operationId, "browser_action_invalid", `The ${input.action} action requires ${missing}`);
|
|
242
|
+
}
|
|
243
|
+
const locator = resolveLocator(page, input.locator);
|
|
244
|
+
switch (input.action) {
|
|
245
|
+
case "click":
|
|
246
|
+
await locator.click();
|
|
247
|
+
break;
|
|
248
|
+
case "dblclick":
|
|
249
|
+
await locator.dblclick();
|
|
250
|
+
break;
|
|
251
|
+
case "rightclick":
|
|
252
|
+
await locator.click({ button: "right" });
|
|
253
|
+
break;
|
|
254
|
+
case "fill":
|
|
255
|
+
await locator.fill(input.value ?? "");
|
|
256
|
+
break;
|
|
257
|
+
case "type":
|
|
258
|
+
await locator.type(input.value ?? "", input.delay === undefined ? {} : { delay: input.delay });
|
|
259
|
+
break;
|
|
260
|
+
case "press":
|
|
261
|
+
await locator.press(input.key ?? "");
|
|
262
|
+
break;
|
|
263
|
+
case "hover":
|
|
264
|
+
await locator.hover();
|
|
265
|
+
break;
|
|
266
|
+
case "select":
|
|
267
|
+
await locator.selectOption(input.value ?? "");
|
|
268
|
+
break;
|
|
269
|
+
case "check":
|
|
270
|
+
await locator.check();
|
|
271
|
+
break;
|
|
272
|
+
case "uncheck":
|
|
273
|
+
await locator.uncheck();
|
|
274
|
+
break;
|
|
275
|
+
case "clear":
|
|
276
|
+
await locator.clear();
|
|
277
|
+
break;
|
|
278
|
+
case "upload": {
|
|
279
|
+
let paths;
|
|
280
|
+
try {
|
|
281
|
+
paths = parseUploadPathsV1(input.value ?? "");
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
return failed(operationId, "browser_action_invalid", error instanceof Error ? error.message : "Invalid upload value");
|
|
285
|
+
}
|
|
286
|
+
await locator.setInputFiles([...paths]);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case "scroll": {
|
|
290
|
+
let directive;
|
|
291
|
+
try {
|
|
292
|
+
directive = parseScrollDirectiveV1(input.value ?? "");
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return failed(operationId, "browser_action_invalid", error instanceof Error ? error.message : "Invalid scroll value");
|
|
296
|
+
}
|
|
297
|
+
await locator.scrollIntoViewIfNeeded();
|
|
298
|
+
// Wheel scrolls whatever scrollable container is under the cursor,
|
|
299
|
+
// so park the cursor over the target element first; otherwise the
|
|
300
|
+
// delta lands on whatever the mouse happened to hover before.
|
|
301
|
+
const scrollBox = await locator.boundingBox();
|
|
302
|
+
if (scrollBox !== null) {
|
|
303
|
+
await page.mouse.move(scrollBox.x + scrollBox.width / 2, scrollBox.y + scrollBox.height / 2);
|
|
304
|
+
}
|
|
305
|
+
const delta = scrollWheelDeltaV1(directive);
|
|
306
|
+
await page.mouse.wheel(delta.deltaX, delta.deltaY);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const evidence = evidenceBuilder(operationId);
|
|
311
|
+
evidence.add("structure", placeholder(boundedPreviewV1(await page.locator("body").ariaSnapshot(), BROWSER_MAX_SNAPSHOT_CHARS_V1)));
|
|
312
|
+
return buildPluginResultV1({
|
|
313
|
+
version: 1,
|
|
314
|
+
operationId,
|
|
315
|
+
status: "succeeded",
|
|
316
|
+
summary: `Performed ${input.action}`,
|
|
317
|
+
artifacts: [],
|
|
318
|
+
evidence: evidence.refs,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async assertPostcondition(input, options = {}) {
|
|
322
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
323
|
+
if (options.signal?.aborted)
|
|
324
|
+
return cancelled(operationId);
|
|
325
|
+
const page = this.page;
|
|
326
|
+
if (page === undefined)
|
|
327
|
+
return sessionRequired(operationId);
|
|
328
|
+
const conditions = Array.isArray(input) ? input : [input];
|
|
329
|
+
const checks = [];
|
|
330
|
+
const failedChecks = [];
|
|
331
|
+
for (const [index, condition] of conditions.entries()) {
|
|
332
|
+
const label = conditionLabel(condition, index);
|
|
333
|
+
const passed = await this.checkCondition(page, condition);
|
|
334
|
+
checks.push(`${label}:${passed ? "passed" : "failed"}`);
|
|
335
|
+
if (!passed)
|
|
336
|
+
failedChecks.push(label);
|
|
337
|
+
}
|
|
338
|
+
if (failedChecks.length === 0) {
|
|
339
|
+
return buildPluginResultV1({
|
|
340
|
+
version: 1,
|
|
341
|
+
operationId,
|
|
342
|
+
status: "succeeded",
|
|
343
|
+
summary: `All ${checks.length} postcondition(s) passed`,
|
|
344
|
+
artifacts: [],
|
|
345
|
+
evidence: [],
|
|
346
|
+
verification: { version: 1, status: "passed", checks },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return buildPluginResultV1({
|
|
350
|
+
version: 1,
|
|
351
|
+
operationId,
|
|
352
|
+
status: "failed",
|
|
353
|
+
summary: "One or more postconditions failed",
|
|
354
|
+
artifacts: [],
|
|
355
|
+
evidence: [],
|
|
356
|
+
verification: { version: 1, status: "failed", checks },
|
|
357
|
+
error: {
|
|
358
|
+
version: 1,
|
|
359
|
+
code: "postcondition_failed",
|
|
360
|
+
message: boundedPreviewV1(`Failed postconditions: ${failedChecks.join("; ")}`, 2048),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async collectEvidence(kind, options = {}) {
|
|
365
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
366
|
+
if (options.signal?.aborted)
|
|
367
|
+
return cancelled(operationId);
|
|
368
|
+
const page = this.page;
|
|
369
|
+
if (page === undefined)
|
|
370
|
+
return sessionRequired(operationId);
|
|
371
|
+
const evidence = evidenceBuilder(operationId);
|
|
372
|
+
if (kind === "console") {
|
|
373
|
+
const text = this.consoleRing.map((entry) => `[${entry.type}] ${entry.text}`).join("\n");
|
|
374
|
+
evidence.add("log", placeholder(boundedPreviewV1(text, BROWSER_MAX_EVIDENCE_CHARS_V1)));
|
|
375
|
+
}
|
|
376
|
+
else if (kind === "network") {
|
|
377
|
+
const text = this.networkRing.map((entry) => `${entry.status} ${entry.url}`).join("\n");
|
|
378
|
+
evidence.add("network", placeholder(boundedPreviewV1(text, BROWSER_MAX_EVIDENCE_CHARS_V1)));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
evidence.add("structure", placeholder(boundedPreviewV1(await page.locator("body").ariaSnapshot(), BROWSER_MAX_SNAPSHOT_CHARS_V1)));
|
|
382
|
+
}
|
|
383
|
+
return buildPluginResultV1({
|
|
384
|
+
version: 1,
|
|
385
|
+
operationId,
|
|
386
|
+
status: "succeeded",
|
|
387
|
+
summary: `Collected ${kind} evidence`,
|
|
388
|
+
artifacts: [],
|
|
389
|
+
evidence: evidence.refs,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async evaluate(input, options = {}) {
|
|
393
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
394
|
+
if (options.signal?.aborted)
|
|
395
|
+
return cancelled(operationId);
|
|
396
|
+
const page = this.page;
|
|
397
|
+
if (page === undefined)
|
|
398
|
+
return sessionRequired(operationId);
|
|
399
|
+
const started = Date.now();
|
|
400
|
+
const value = await evaluateWithTimeout(page, input.expression, input.arg, BROWSER_DEFAULT_EVALUATE_TIMEOUT_MS_V1);
|
|
401
|
+
const evidence = evidenceBuilder(operationId);
|
|
402
|
+
evidence.add("log", placeholder(boundedPreviewV1(serializeJsonPayloadV1(value), BROWSER_MAX_EVIDENCE_CHARS_V1)));
|
|
403
|
+
return buildPluginResultV1({
|
|
404
|
+
version: 1,
|
|
405
|
+
operationId,
|
|
406
|
+
status: "succeeded",
|
|
407
|
+
summary: `Evaluated expression in ${Date.now() - started}ms`,
|
|
408
|
+
artifacts: [],
|
|
409
|
+
evidence: evidence.refs,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async consoleEntries(input, options = {}) {
|
|
413
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
414
|
+
if (options.signal?.aborted)
|
|
415
|
+
return cancelled(operationId);
|
|
416
|
+
const page = this.page;
|
|
417
|
+
if (page === undefined)
|
|
418
|
+
return sessionRequired(operationId);
|
|
419
|
+
let result;
|
|
420
|
+
try {
|
|
421
|
+
result = filterConsoleEntriesV1(this.consoleRing, input);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
return failed(operationId, "browser_console_invalid", error instanceof Error ? error.message : "Invalid console filter");
|
|
425
|
+
}
|
|
426
|
+
return payloadResult(operationId, `Read console: ${result.total} entry(ies), ${result.returned.length} returned`, "log", result);
|
|
427
|
+
}
|
|
428
|
+
async networkEntries(input, options = {}) {
|
|
429
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
430
|
+
if (options.signal?.aborted)
|
|
431
|
+
return cancelled(operationId);
|
|
432
|
+
const page = this.page;
|
|
433
|
+
if (page === undefined)
|
|
434
|
+
return sessionRequired(operationId);
|
|
435
|
+
const result = filterNetworkEntriesV1(this.networkRing, input);
|
|
436
|
+
return payloadResult(operationId, `Read network: ${result.total} entry(ies), ${result.returned.length} returned`, "network", result);
|
|
437
|
+
}
|
|
438
|
+
async performance(options = {}) {
|
|
439
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
440
|
+
if (options.signal?.aborted)
|
|
441
|
+
return cancelled(operationId);
|
|
442
|
+
const page = this.page;
|
|
443
|
+
if (page === undefined)
|
|
444
|
+
return sessionRequired(operationId);
|
|
445
|
+
const raw = await page.evaluate(collectPerformanceInPage);
|
|
446
|
+
const payload = shapePerformanceV1(raw);
|
|
447
|
+
return payloadResult(operationId, `Collected performance metrics (${payload.resources.count} resource(s), ${payload.domNodes} DOM nodes)`, "trace", payload);
|
|
448
|
+
}
|
|
449
|
+
async wait(input, options = {}) {
|
|
450
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
451
|
+
if (options.signal?.aborted)
|
|
452
|
+
return cancelled(operationId);
|
|
453
|
+
const page = this.page;
|
|
454
|
+
if (page === undefined)
|
|
455
|
+
return sessionRequired(operationId);
|
|
456
|
+
const missing = waitMissingInput(input);
|
|
457
|
+
if (missing !== undefined) {
|
|
458
|
+
return failed(operationId, "browser_wait_invalid", `The ${input.until} wait requires ${missing}`);
|
|
459
|
+
}
|
|
460
|
+
const effectiveMs = clampWaitMsV1(input.ms);
|
|
461
|
+
const started = Date.now();
|
|
462
|
+
let label;
|
|
463
|
+
try {
|
|
464
|
+
switch (input.until) {
|
|
465
|
+
case "selector": {
|
|
466
|
+
const locator = resolveLocator(page, input.locator);
|
|
467
|
+
await locator.waitFor({ state: "visible", timeout: effectiveMs });
|
|
468
|
+
label = `selector ${locatorLabel(input.locator)}`;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "navigation":
|
|
472
|
+
await page.waitForLoadState("load", { timeout: effectiveMs });
|
|
473
|
+
label = "navigation";
|
|
474
|
+
break;
|
|
475
|
+
case "networkidle":
|
|
476
|
+
await page.waitForLoadState("networkidle", { timeout: effectiveMs });
|
|
477
|
+
label = "networkidle";
|
|
478
|
+
break;
|
|
479
|
+
case "time":
|
|
480
|
+
await sleep(effectiveMs);
|
|
481
|
+
label = "time";
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (!isTimeoutError(error))
|
|
487
|
+
throw error;
|
|
488
|
+
return failed(operationId, "browser_wait_timeout", `Wait for ${input.until} timed out after ${effectiveMs}ms: ${boundedPreviewV1(error.message, 2048)}`);
|
|
489
|
+
}
|
|
490
|
+
const waitedMs = Date.now() - started;
|
|
491
|
+
const payload = {
|
|
492
|
+
until: input.until,
|
|
493
|
+
...(input.ms === undefined ? {} : { requestedMs: input.ms }),
|
|
494
|
+
effectiveMs,
|
|
495
|
+
waitedMs,
|
|
496
|
+
};
|
|
497
|
+
return payloadResult(operationId, `Waited ${waitedMs}ms for ${label}`, "log", payload);
|
|
498
|
+
}
|
|
499
|
+
async drag(input, options = {}) {
|
|
500
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
501
|
+
if (options.signal?.aborted)
|
|
502
|
+
return cancelled(operationId);
|
|
503
|
+
const page = this.page;
|
|
504
|
+
if (page === undefined)
|
|
505
|
+
return sessionRequired(operationId);
|
|
506
|
+
const steps = clampDragStepsV1(input.steps);
|
|
507
|
+
const from = resolveLocator(page, input.from);
|
|
508
|
+
const to = resolveLocator(page, input.to);
|
|
509
|
+
await from.hover();
|
|
510
|
+
await page.mouse.down();
|
|
511
|
+
// Deliberately no `to.hover()` while the button is down: pages that call
|
|
512
|
+
// setPointerCapture (the standard drag idiom) retarget pointer events to
|
|
513
|
+
// the from-element, so Playwright's hover hit-target check would retry
|
|
514
|
+
// until timeout. Move by geometry instead.
|
|
515
|
+
const box = await to.boundingBox();
|
|
516
|
+
if (box !== null) {
|
|
517
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps });
|
|
518
|
+
}
|
|
519
|
+
await page.mouse.up();
|
|
520
|
+
const payload = {
|
|
521
|
+
steps,
|
|
522
|
+
toCenter: box === null ? null : { x: box.x + box.width / 2, y: box.y + box.height / 2 },
|
|
523
|
+
};
|
|
524
|
+
return payloadResult(operationId, `Dragged from ${locatorLabel(input.from)} to ${locatorLabel(input.to)}`, "log", payload);
|
|
525
|
+
}
|
|
526
|
+
async mouse(input, options = {}) {
|
|
527
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
528
|
+
if (options.signal?.aborted)
|
|
529
|
+
return cancelled(operationId);
|
|
530
|
+
const page = this.page;
|
|
531
|
+
if (page === undefined)
|
|
532
|
+
return sessionRequired(operationId);
|
|
533
|
+
const buttonOptions = input.button === undefined ? {} : { button: input.button };
|
|
534
|
+
let summary;
|
|
535
|
+
switch (input.action) {
|
|
536
|
+
case "move":
|
|
537
|
+
await page.mouse.move(input.x, input.y);
|
|
538
|
+
summary = `Mouse moved to (${input.x}, ${input.y})`;
|
|
539
|
+
break;
|
|
540
|
+
case "click":
|
|
541
|
+
await page.mouse.click(input.x, input.y, buttonOptions);
|
|
542
|
+
summary = `Mouse clicked (${input.button ?? "left"}) at (${input.x}, ${input.y})`;
|
|
543
|
+
break;
|
|
544
|
+
case "dblclick":
|
|
545
|
+
await page.mouse.dblclick(input.x, input.y, buttonOptions);
|
|
546
|
+
summary = `Mouse double-clicked (${input.button ?? "left"}) at (${input.x}, ${input.y})`;
|
|
547
|
+
break;
|
|
548
|
+
case "rightclick":
|
|
549
|
+
await page.mouse.click(input.x, input.y, { ...buttonOptions, button: "right" });
|
|
550
|
+
summary = `Mouse right-clicked at (${input.x}, ${input.y})`;
|
|
551
|
+
break;
|
|
552
|
+
case "wheel": {
|
|
553
|
+
const deltaX = input.deltaX ?? 0;
|
|
554
|
+
const deltaY = input.deltaY ?? 0;
|
|
555
|
+
await page.mouse.wheel(deltaX, deltaY);
|
|
556
|
+
summary = `Mouse wheel (${deltaX}, ${deltaY})`;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return payloadResult(operationId, summary, "log", input);
|
|
561
|
+
}
|
|
562
|
+
async pressKey(input, options = {}) {
|
|
563
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
564
|
+
if (options.signal?.aborted)
|
|
565
|
+
return cancelled(operationId);
|
|
566
|
+
const page = this.page;
|
|
567
|
+
if (page === undefined)
|
|
568
|
+
return sessionRequired(operationId);
|
|
569
|
+
await page.keyboard.press(input.key, input.delay === undefined ? {} : { delay: input.delay });
|
|
570
|
+
return payloadResult(operationId, `Pressed key ${input.key}`, "log", input);
|
|
571
|
+
}
|
|
572
|
+
async tabs(input, options = {}) {
|
|
573
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
574
|
+
if (options.signal?.aborted)
|
|
575
|
+
return cancelled(operationId);
|
|
576
|
+
if (input.action === "new") {
|
|
577
|
+
await this.connect();
|
|
578
|
+
const page = await this.openPage();
|
|
579
|
+
if (input.url !== undefined)
|
|
580
|
+
await page.goto(input.url, { waitUntil: "load" });
|
|
581
|
+
return payloadResult(operationId, `Opened tab ${this.pageIndex}`, "log", {
|
|
582
|
+
index: this.pageIndex,
|
|
583
|
+
title: await page.title(),
|
|
584
|
+
url: page.url(),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
const page = this.page;
|
|
588
|
+
if (page === undefined)
|
|
589
|
+
return sessionRequired(operationId);
|
|
590
|
+
if (input.action === "list") {
|
|
591
|
+
const tabs = [];
|
|
592
|
+
for (const [index, entry] of this.pages.entries()) {
|
|
593
|
+
tabs.push({ index, title: await entry.title(), url: entry.url() });
|
|
594
|
+
}
|
|
595
|
+
return payloadResult(operationId, `Listed ${tabs.length} tab(s), current ${this.pageIndex}`, "log", {
|
|
596
|
+
currentIndex: this.pageIndex,
|
|
597
|
+
tabs,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (input.action === "switch") {
|
|
601
|
+
if (input.index === undefined ||
|
|
602
|
+
!Number.isInteger(input.index) ||
|
|
603
|
+
input.index < 0 ||
|
|
604
|
+
input.index >= this.pages.length) {
|
|
605
|
+
return failed(operationId, "browser_tabs_invalid", `switch requires an index between 0 and ${this.pages.length - 1}`);
|
|
606
|
+
}
|
|
607
|
+
this.pageIndex = input.index;
|
|
608
|
+
this.page = this.pages[this.pageIndex];
|
|
609
|
+
return payloadResult(operationId, `Switched to tab ${this.pageIndex}`, "log", {
|
|
610
|
+
index: this.pageIndex,
|
|
611
|
+
title: await this.page.title(),
|
|
612
|
+
url: this.page.url(),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
// close
|
|
616
|
+
if (input.index === undefined ||
|
|
617
|
+
!Number.isInteger(input.index) ||
|
|
618
|
+
input.index < 0 ||
|
|
619
|
+
input.index >= this.pages.length) {
|
|
620
|
+
return failed(operationId, "browser_tabs_invalid", `close requires an index between 0 and ${this.pages.length - 1}`);
|
|
621
|
+
}
|
|
622
|
+
const closing = this.pages[input.index];
|
|
623
|
+
await closing.close();
|
|
624
|
+
this.pages.splice(input.index, 1);
|
|
625
|
+
if (this.pageIndex >= input.index)
|
|
626
|
+
this.pageIndex = Math.max(0, this.pageIndex - 1);
|
|
627
|
+
this.page = this.pages[this.pageIndex];
|
|
628
|
+
return payloadResult(operationId, `Closed tab ${input.index}; ${this.pages.length} remaining`, "log", {
|
|
629
|
+
closedIndex: input.index,
|
|
630
|
+
remaining: this.pages.length,
|
|
631
|
+
...(this.page === undefined ? {} : { currentIndex: this.pageIndex }),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
async storage(input, options = {}) {
|
|
635
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
636
|
+
if (options.signal?.aborted)
|
|
637
|
+
return cancelled(operationId);
|
|
638
|
+
switch (input.action) {
|
|
639
|
+
case "get-cookies": {
|
|
640
|
+
const context = this.context;
|
|
641
|
+
if (context === undefined)
|
|
642
|
+
return sessionRequired(operationId);
|
|
643
|
+
const cookies = await context.cookies();
|
|
644
|
+
return payloadResult(operationId, `Read ${cookies.length} cookie(s)`, "log", { cookies });
|
|
645
|
+
}
|
|
646
|
+
case "set-cookie": {
|
|
647
|
+
const context = this.context;
|
|
648
|
+
if (context === undefined)
|
|
649
|
+
return sessionRequired(operationId);
|
|
650
|
+
if (input.name === undefined || input.value === undefined || input.domain === undefined) {
|
|
651
|
+
return failed(operationId, "browser_storage_invalid", "set-cookie requires name, value, and domain");
|
|
652
|
+
}
|
|
653
|
+
await context.addCookies([
|
|
654
|
+
{
|
|
655
|
+
name: input.name,
|
|
656
|
+
value: input.value,
|
|
657
|
+
domain: input.domain,
|
|
658
|
+
...(input.path === undefined ? {} : { path: input.path }),
|
|
659
|
+
...(input.expires === undefined ? {} : { expires: input.expires }),
|
|
660
|
+
},
|
|
661
|
+
]);
|
|
662
|
+
return payloadResult(operationId, `Set cookie ${input.name}`, "log", {
|
|
663
|
+
name: input.name,
|
|
664
|
+
domain: input.domain,
|
|
665
|
+
...(input.path === undefined ? {} : { path: input.path }),
|
|
666
|
+
...(input.expires === undefined ? {} : { expires: input.expires }),
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
case "clear-cookies": {
|
|
670
|
+
const context = this.context;
|
|
671
|
+
if (context === undefined)
|
|
672
|
+
return sessionRequired(operationId);
|
|
673
|
+
await context.clearCookies();
|
|
674
|
+
return payloadResult(operationId, "Cleared cookies", "log", { cleared: "cookies" });
|
|
675
|
+
}
|
|
676
|
+
case "get-local": {
|
|
677
|
+
const page = this.page;
|
|
678
|
+
if (page === undefined)
|
|
679
|
+
return sessionRequired(operationId);
|
|
680
|
+
if (input.key === undefined) {
|
|
681
|
+
return failed(operationId, "browser_storage_invalid", "get-local requires a key");
|
|
682
|
+
}
|
|
683
|
+
const value = await page.evaluate((key) => window.localStorage.getItem(key), input.key);
|
|
684
|
+
return payloadResult(operationId, `Read localStorage["${input.key}"]`, "log", { key: input.key, value });
|
|
685
|
+
}
|
|
686
|
+
case "set-local": {
|
|
687
|
+
const page = this.page;
|
|
688
|
+
if (page === undefined)
|
|
689
|
+
return sessionRequired(operationId);
|
|
690
|
+
if (input.key === undefined || input.value === undefined) {
|
|
691
|
+
return failed(operationId, "browser_storage_invalid", "set-local requires key and value");
|
|
692
|
+
}
|
|
693
|
+
await page.evaluate(({ key, value }) => {
|
|
694
|
+
window.localStorage.setItem(key, value);
|
|
695
|
+
}, { key: input.key, value: input.value });
|
|
696
|
+
return payloadResult(operationId, `Set localStorage["${input.key}"]`, "log", { key: input.key });
|
|
697
|
+
}
|
|
698
|
+
case "clear-local": {
|
|
699
|
+
const page = this.page;
|
|
700
|
+
if (page === undefined)
|
|
701
|
+
return sessionRequired(operationId);
|
|
702
|
+
await page.evaluate(() => {
|
|
703
|
+
window.localStorage.clear();
|
|
704
|
+
});
|
|
705
|
+
return payloadResult(operationId, "Cleared localStorage", "log", { cleared: "local" });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async viewport(input, options = {}) {
|
|
710
|
+
const operationId = options.operationId ?? "browser-tool-call";
|
|
711
|
+
if (options.signal?.aborted)
|
|
712
|
+
return cancelled(operationId);
|
|
713
|
+
const page = this.page;
|
|
714
|
+
if (page === undefined)
|
|
715
|
+
return sessionRequired(operationId);
|
|
716
|
+
if (input.action === "get") {
|
|
717
|
+
const viewport = page.viewportSize();
|
|
718
|
+
return payloadResult(operationId, "Read viewport size", "log", {
|
|
719
|
+
width: viewport?.width ?? null,
|
|
720
|
+
height: viewport?.height ?? null,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (input.width === undefined || input.height === undefined) {
|
|
724
|
+
return failed(operationId, "browser_viewport_invalid", "set requires width and height");
|
|
725
|
+
}
|
|
726
|
+
const width = clampViewportDimensionV1(input.width);
|
|
727
|
+
const height = clampViewportDimensionV1(input.height);
|
|
728
|
+
await page.setViewportSize({ width, height });
|
|
729
|
+
const clamped = width !== input.width || height !== input.height;
|
|
730
|
+
return payloadResult(operationId, `Viewport set to ${width}x${height}${clamped ? " (clamped)" : ""}`, "log", {
|
|
731
|
+
width,
|
|
732
|
+
height,
|
|
733
|
+
...(clamped ? { requestedWidth: input.width, requestedHeight: input.height } : {}),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
loadModule() {
|
|
737
|
+
if (this.loadPlaywright !== undefined)
|
|
738
|
+
return this.loadPlaywright();
|
|
739
|
+
if (this.depsDir === "")
|
|
740
|
+
throw new Error(installGuidanceMessage());
|
|
741
|
+
if (this.resolveModule(this.depsDir) === undefined)
|
|
742
|
+
throw new Error(installGuidanceMessage(this.depsDir));
|
|
743
|
+
try {
|
|
744
|
+
const require = createRequire(join(this.depsDir, "package.json"));
|
|
745
|
+
return require("playwright");
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
throw new Error(installGuidanceMessage(this.depsDir), {
|
|
749
|
+
cause: error instanceof Error ? error : undefined,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
downloadsDir() {
|
|
754
|
+
return this.depsDir === "" ? undefined : join(this.depsDir, BROWSER_DOWNLOADS_DIR_NAME_V1);
|
|
755
|
+
}
|
|
756
|
+
async openPage() {
|
|
757
|
+
const context = this.context;
|
|
758
|
+
if (context === undefined)
|
|
759
|
+
throw new Error("Browser context is not initialized; connect first");
|
|
760
|
+
const page = await context.newPage();
|
|
761
|
+
page.on("console", (message) => {
|
|
762
|
+
const location = message.location();
|
|
763
|
+
this.pushRing(this.consoleRing, {
|
|
764
|
+
type: message.type(),
|
|
765
|
+
text: message.text(),
|
|
766
|
+
timestamp: Date.now(),
|
|
767
|
+
...(location.url === "" ? {} : { location }),
|
|
768
|
+
}, BROWSER_RING_BUFFER_SIZE_V1);
|
|
769
|
+
});
|
|
770
|
+
page.on("response", (response) => {
|
|
771
|
+
this.pushRing(this.networkRing, networkEntryOf(response), BROWSER_RING_BUFFER_SIZE_V1);
|
|
772
|
+
});
|
|
773
|
+
// Built-in dialog policy: auto-accept and keep a bounded trace entry so
|
|
774
|
+
// assert/evidence tools can observe that a dialog appeared.
|
|
775
|
+
page.on("dialog", (dialog) => {
|
|
776
|
+
void dialog.accept().catch(() => undefined);
|
|
777
|
+
this.pushRing(this.consoleRing, { type: "dialog", text: `[dialog] ${dialog.type()}: ${dialog.message()}`, timestamp: Date.now() }, BROWSER_RING_BUFFER_SIZE_V1);
|
|
778
|
+
});
|
|
779
|
+
// Built-in download policy: persist into <agentDir>/browser-deps/downloads.
|
|
780
|
+
page.on("download", (download) => {
|
|
781
|
+
const filename = download.suggestedFilename();
|
|
782
|
+
this.lastDownload = filename;
|
|
783
|
+
const dir = this.downloadsDir();
|
|
784
|
+
if (dir === undefined)
|
|
785
|
+
return;
|
|
786
|
+
try {
|
|
787
|
+
this.ensureDir(dir);
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
this.pushRing(this.consoleRing, {
|
|
791
|
+
type: "download",
|
|
792
|
+
text: `[download] could not create ${dir}: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
793
|
+
timestamp: Date.now(),
|
|
794
|
+
}, BROWSER_RING_BUFFER_SIZE_V1);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
void download.saveAs(join(dir, filename)).catch((error) => {
|
|
798
|
+
this.pushRing(this.consoleRing, {
|
|
799
|
+
type: "download",
|
|
800
|
+
text: `[download] could not save ${filename}: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
801
|
+
timestamp: Date.now(),
|
|
802
|
+
}, BROWSER_RING_BUFFER_SIZE_V1);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
this.pages.push(page);
|
|
806
|
+
this.pageIndex = this.pages.length - 1;
|
|
807
|
+
this.page = page;
|
|
808
|
+
return page;
|
|
809
|
+
}
|
|
810
|
+
pushRing(ring, entry, maximum) {
|
|
811
|
+
ring.push(entry);
|
|
812
|
+
if (ring.length > maximum)
|
|
813
|
+
ring.splice(0, ring.length - maximum);
|
|
814
|
+
}
|
|
815
|
+
async checkCondition(page, condition) {
|
|
816
|
+
if (condition.kind === "url") {
|
|
817
|
+
const url = page.url();
|
|
818
|
+
if (condition.equals !== undefined && url !== condition.equals)
|
|
819
|
+
return false;
|
|
820
|
+
if (condition.contains !== undefined && !url.includes(condition.contains))
|
|
821
|
+
return false;
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
if (condition.kind === "element") {
|
|
825
|
+
const locator = resolveLocator(page, condition.locator);
|
|
826
|
+
if (condition.state === "visible")
|
|
827
|
+
return locator.isVisible();
|
|
828
|
+
if (condition.state === "hidden")
|
|
829
|
+
return locator.isHidden();
|
|
830
|
+
const value = await locator.inputValue();
|
|
831
|
+
return condition.value === undefined || value === condition.value;
|
|
832
|
+
}
|
|
833
|
+
if (condition.kind === "text") {
|
|
834
|
+
const text = condition.locator === undefined
|
|
835
|
+
? ((await page.locator("body").textContent()) ?? "")
|
|
836
|
+
: ((await resolveLocator(page, condition.locator).textContent()) ?? "");
|
|
837
|
+
return text.includes(condition.contains);
|
|
838
|
+
}
|
|
839
|
+
if (condition.kind === "console") {
|
|
840
|
+
return this.consoleRing.some((entry) => entry.text.includes(condition.contains) &&
|
|
841
|
+
(condition.level === undefined || entry.type === condition.level));
|
|
842
|
+
}
|
|
843
|
+
return this.networkRing.some((entry) => entry.url.includes(condition.containsUrl) &&
|
|
844
|
+
(condition.status === undefined || entry.status === condition.status));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/** Shapes a ring-buffer response event into a bounded network entry. */
|
|
848
|
+
function networkEntryOf(response) {
|
|
849
|
+
const request = response.request();
|
|
850
|
+
let durationMs;
|
|
851
|
+
const timing = request.timing();
|
|
852
|
+
if (timing.responseEnd > 0 && timing.responseEnd >= timing.startTime) {
|
|
853
|
+
durationMs = Math.round(timing.responseEnd - timing.startTime);
|
|
854
|
+
}
|
|
855
|
+
const contentLength = response.headers()["content-length"];
|
|
856
|
+
const sizeBytes = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10);
|
|
857
|
+
return {
|
|
858
|
+
method: request.method(),
|
|
859
|
+
url: response.url(),
|
|
860
|
+
status: response.status(),
|
|
861
|
+
timestamp: Date.now(),
|
|
862
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
863
|
+
...(sizeBytes === undefined || Number.isNaN(sizeBytes) ? {} : { sizeBytes }),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/** Builds a succeeded result carrying one bounded JSON payload as evidence. */
|
|
867
|
+
function payloadResult(operationId, summary, kind, payload) {
|
|
868
|
+
const evidence = evidenceBuilder(operationId);
|
|
869
|
+
evidence.add(kind, placeholder(boundedPreviewV1(serializeJsonPayloadV1(payload), BROWSER_MAX_EVIDENCE_CHARS_V1)));
|
|
870
|
+
return buildPluginResultV1({
|
|
871
|
+
version: 1,
|
|
872
|
+
operationId,
|
|
873
|
+
status: "succeeded",
|
|
874
|
+
summary,
|
|
875
|
+
artifacts: [],
|
|
876
|
+
evidence: evidence.refs,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Production runtime: one driver per session plus the `browser_setup`
|
|
881
|
+
* self-install face. Hosts and tests may inject `spawnSync` and
|
|
882
|
+
* `resolvePlaywright` through the setup options.
|
|
883
|
+
*/
|
|
884
|
+
export class PlaywrightBrowserRuntimeV1 {
|
|
885
|
+
options;
|
|
886
|
+
driver;
|
|
887
|
+
constructor(options = {}) {
|
|
888
|
+
this.options = options;
|
|
889
|
+
}
|
|
890
|
+
async connect() {
|
|
891
|
+
if (this.driver === undefined)
|
|
892
|
+
this.driver = new PlaywrightBrowserDriver(this.options);
|
|
893
|
+
await this.driver.connect();
|
|
894
|
+
return this.driver;
|
|
895
|
+
}
|
|
896
|
+
async setup() {
|
|
897
|
+
return setupBrowserDepsPlaywrightV1(this.options);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const actionMissingInput = (input) => {
|
|
901
|
+
if ((input.action === "fill" ||
|
|
902
|
+
input.action === "type" ||
|
|
903
|
+
input.action === "select" ||
|
|
904
|
+
input.action === "upload" ||
|
|
905
|
+
input.action === "scroll") &&
|
|
906
|
+
input.value === undefined)
|
|
907
|
+
return "a value";
|
|
908
|
+
if (input.action === "press" && input.key === undefined)
|
|
909
|
+
return "a key";
|
|
910
|
+
return undefined;
|
|
911
|
+
};
|
|
912
|
+
const resolveLocator = (page, locator) => {
|
|
913
|
+
if (locator.kind === "id")
|
|
914
|
+
return page.locator(`#${locator.value}`);
|
|
915
|
+
if (locator.kind === "role")
|
|
916
|
+
return page.getByRole(locator.value, locator.name === undefined ? {} : { name: locator.name });
|
|
917
|
+
if (locator.kind === "text")
|
|
918
|
+
return page.getByText(locator.value);
|
|
919
|
+
if (locator.kind === "test-id")
|
|
920
|
+
return page.getByTestId(locator.value);
|
|
921
|
+
return page.locator(locator.value);
|
|
922
|
+
};
|
|
923
|
+
const cancelled = (operationId) => failed(operationId, "cancelled", "Browser operation was cancelled");
|
|
924
|
+
const sessionRequired = (operationId) => failed(operationId, "browser_session_required", "No active browser session. Call browser_navigate first.");
|
|
925
|
+
function failed(operationId, code, message) {
|
|
926
|
+
return buildPluginResultV1({
|
|
927
|
+
version: 1,
|
|
928
|
+
operationId,
|
|
929
|
+
status: code === "cancelled" ? "cancelled" : "failed",
|
|
930
|
+
summary: message,
|
|
931
|
+
artifacts: [],
|
|
932
|
+
evidence: [],
|
|
933
|
+
error: { version: 1, code, message },
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function evidenceBuilder(operationId) {
|
|
937
|
+
const refs = [];
|
|
938
|
+
let index = 1;
|
|
939
|
+
return {
|
|
940
|
+
refs,
|
|
941
|
+
add(kind, summary) {
|
|
942
|
+
refs.push({
|
|
943
|
+
version: 1,
|
|
944
|
+
evidenceId: `${operationId}-evidence-${index}`,
|
|
945
|
+
kind,
|
|
946
|
+
operationId,
|
|
947
|
+
summary,
|
|
948
|
+
});
|
|
949
|
+
index += 1;
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//# sourceMappingURL=playwright-driver.js.map
|