@ai-agent-forge/plugin-browser-playwright 0.88.3

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/MANUAL.md ADDED
@@ -0,0 +1,60 @@
1
+ # browser-playwright 能力手册(AI 用)
2
+
3
+ 内置 Playwright 浏览器操作:导航、DOM/a11y 快照、点击/填写/拖拽/滚动、截图、console/网络/性能观测、存储、多标签。headless Chromium,跨平台。
4
+
5
+ ## 首次使用(最重要)
6
+
7
+ Playwright **不随宿主预装**。第一次调用任何 browser_* 工具会返回结构化安装指引。两条路:
8
+
9
+ 1. **推荐**:直接调 `browser_setup`,自动安装 Playwright 与 Chromium(可能耗时数分钟,Chromium 约 200MB + headless shell 约 100MB)。
10
+ 2. 用户自己执行指引给出的两条命令(npm install + playwright install chromium)。
11
+
12
+ 安装完成后重试即可,无需重启会话。安装进度/失败原因都在指引与错误消息里;npm 慢网时离线 tarball 安装也受支持(指引含精确命令)。
13
+
14
+ ## 工具速查
15
+
16
+ | 工具 | 用途 | 硬约束 |
17
+ | --- | --- | --- |
18
+ | browser_navigate | 打开 URL | waitUntil 默认 load |
19
+ | browser_snapshot | DOM/a11y/截图证据 | 只读 |
20
+ | browser_locator_action | 单元素 13 种动作(click/dblclick/rightclick/fill/type/press/hover/select/check/uncheck/clear/upload/scroll) | scroll 值格式 `down:300` |
21
+ | browser_assert_postcondition | 断言 url/element/text/console/network | 无副作用 |
22
+ | browser_collect_evidence | 证据打包(snapshot/console/network) | 只读 |
23
+ | browser_evaluate | JS 逃生舱 | 见下方"关键坑" |
24
+ | browser_console / browser_network | 环形缓冲读取(可过滤) | 只读 |
25
+ | browser_performance | Navigation Timing/FCP/资源/DOM 数 | 只读 |
26
+ | browser_wait | selector/navigation/networkidle/time | 钳制 30s |
27
+ | browser_drag | hover→down→步进移动→up | pointer capture 页面安全 |
28
+ | browser_mouse | 裸坐标 move/click/dblclick/rightclick/wheel | 坐标是视口像素 |
29
+ | browser_press_key | 页面级键盘(Enter/PageDown/Control+a) | — |
30
+ | browser_tabs | list/new/switch/close | index 从 0 |
31
+ | browser_storage | cookies + localStorage 读写清 | — |
32
+ | browser_viewport | set/get(320..7680) | — |
33
+
34
+ readonly 模式只挂观察类(snapshot/assert/collect/console/network/performance/wait)。
35
+
36
+ ## 正例
37
+
38
+ ```jsonc
39
+ // 定位器语义:kind ∈ css|id|role|text|test-id
40
+ { "locator": { "kind": "css", "value": "#submit" }, "action": "click" }
41
+ { "locator": { "kind": "role", "value": "button", "name": "提交" }, "action": "click" }
42
+ { "locator": { "kind": "css", "value": "#name" }, "action": "fill", "value": "张三" }
43
+ { "locator": { "kind": "css", "value": "#list" }, "action": "scroll", "value": "down:300" }
44
+ { "until": "selector", "locator": { "kind": "css", "value": "#delayed" } }
45
+ { "action": "get-local", "key": "token" }
46
+ ```
47
+
48
+ ## 反例与关键坑(全部真实踩过)
49
+
50
+ 1. **browser_evaluate 只接受纯表达式**:`document.title`、`1 + 1`。多语句必须 IIFE 字符串:`(() => { ...; return x; })()`。**裸箭头字符串 `() => ...` 不会被调用,返回 undefined**——真实 Playwright 的语义,不是 bug。
51
+ 2. **裸坐标点击前先拿实时坐标**:用 evaluate 读 `getBoundingClientRect()` 中心再 `browser_mouse`,别猜固定坐标(布局会变)。
52
+ 3. **容器滚动用 locator_action scroll**,不要用 mouse wheel 打坐标——wheel 落在鼠标悬停处,不一定滚目标容器(scroll 工具已内置先移到元素中心)。
53
+ 4. **文件上传优先 `upload` 动作**(绝对路径);**原生文件选择对话框 Playwright 无法操作**——那是 OS 窗口,需要桌面真实输入(desktop_* 能力,见下)。
54
+ 5. **dialog(alert/confirm)自动接受**并记入 console 环,不会挂死;别为此写等待。
55
+ 6. headless 无桌面形态:需要真实用户手势、反自动化检测场景、Canvas/WebGL 黑盒页面,改用 **desktop_* 桌面通道**(真实鼠标键盘),两通道互补而非二选一。
56
+
57
+ ## 与 desktop 能力协作
58
+
59
+ - Playwright 通道:自带内核、语义级元素操作、稳定可断言——**默认通道**。
60
+ - desktop 通道:真实硬件级输入(CGEvent/mouse_event)、能过 UIA/AX 看到浏览器窗口与页面控件、能操作原生对话框——**Playwright 覆盖不了时的第二通道**。
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @agent-forge/plugin-browser-playwright
2
+
3
+ Built-in Playwright browser capability plugin for [Agent Forge](https://github.com/agent-forge/agent-forge) — the product's single browser channel. It
4
+ registers seventeen tools covering the common Playwright operations prewritten so the model calls them directly; `browser_evaluate` is the declared
5
+ escape hatch for everything the built-in tools do not cover.
6
+
7
+ Playwright is **never bundled**: the package has no Playwright dependency. The driver resolves `playwright` at connect time from
8
+ `<agentDir>/browser-deps` via `createRequire`, and `browser_setup` installs it on first use (`npm install playwright --prefix <dir>`, then
9
+ `npx playwright install chromium`, browsers pinned to `<dir>/browsers` through `PLAYWRIGHT_BROWSERS_PATH`). Every other browser tool, when it finds
10
+ Playwright missing, returns a structured failure whose message carries the two exact install commands plus a pointer to `browser_setup`.
11
+
12
+ ## Tool surface
13
+
14
+ ### Session and setup
15
+
16
+ | Tool | Parameters | Notes |
17
+ | --- | --- | --- |
18
+ | `browser_navigate` | `{url, waitUntil?}` | Opens the session on first use. |
19
+ | `browser_setup` | `{}` | Installs Playwright + Chromium into `<agentDir>/browser-deps`; short-circuits with `alreadyInstalled`. |
20
+
21
+ ### Observation (registered in readonly mode too)
22
+
23
+ | Tool | Parameters | Notes |
24
+ | --- | --- | --- |
25
+ | `browser_snapshot` | `{includeDom?, includeA11y?, includeStyles?, includeScreenshot?}` | a11y evidence is the Playwright aria snapshot (bounded 16KiB), DOM is `page.content()` (bounded 64KiB), screenshots are base64 PNGs with a 4MiB budget. Styles capture is declared `not_supported_in_this_build`; the parameter is accepted for contract compatibility. The summary and evidence also report the most recent downloaded file, if any. |
26
+ | `browser_assert_postcondition` | `{condition}` — one condition or an array of 1..32 (all ANDed) | Over url / element / text / console / network state; console and network come from 200-entry page ring buffers. |
27
+ | `browser_collect_evidence` | `{kind: console\|network\|snapshot}` | Raw bounded evidence dump. |
28
+ | `browser_console` | `{level?, contains?, limit?}` | Reads the console ring buffer. `level` is comma-combined `error/warning/info/debug` (`info` also matches Playwright's `log` type); `limit` defaults to 50, capped at 200. Returns `{total, returned:[{type,text,location?,timestamp}]}`; auto-accepted dialogs appear as `type: "dialog"` entries. |
29
+ | `browser_network` | `{containsUrl?, status?, method?, limit?}` | Reads the network ring buffer. Returns `{total, returned:[{method,url,status,timestamp,durationMs?,sizeBytes?}]}`; duration/size are included only when the request timing and `content-length` header are available. |
30
+ | `browser_performance` | `{}` | Navigation Timing (TTFB / DOMContentLoaded / load), paint (FP/FCP), resource summary (count, total transfer bytes, slowest 5 by duration), and DOM node count. Read-only. |
31
+ | `browser_wait` | `{until: selector\|navigation\|networkidle\|time, locator?, ms?}` | `selector` requires `locator` (waits for visible); `time` requires `ms`. `ms` is clamped to 0..30000 (default 30000 for the non-time kinds). Returns requested/effective/actual wait durations; timeout failures map to `browser_wait_timeout`. |
32
+
33
+ ### Interaction (never registered in readonly mode)
34
+
35
+ | Tool | Parameters | Notes |
36
+ | --- | --- | --- |
37
+ | `browser_locator_action` | `{locator: {kind: css\|id\|role\|text\|test-id, value, name?}, action, value?, key?, delay?}` | Actions: `click`, `dblclick`, `rightclick`, `fill`, `type`, `press`, `hover`, `select`, `check`, `uncheck`, `clear`, `upload` (`value` = one absolute path or a JSON array of paths, applied via `setInputFiles`), `scroll` (`value` = `<direction>:<pixels>`, e.g. `down:500`; scrolls the element into view, then wheels by pixels). Returns a post-action aria-snapshot fragment. |
38
+ | `browser_evaluate` | `{expression, arg?}` | **Escape hatch — only use when no built-in tool covers the operation.** Runs `page.evaluate`; the JSON-serialized result is bounded to 64KiB (`undefined` serializes as `undefined`); 30s wall-clock timeout leaves the page-side script to finish on its own. Dialogs fired during evaluation are auto-accepted. |
39
+ | `browser_drag` | `{from, to, steps?}` | Hover source → mouse down → hover target → stepped `mouse.move` to the target center (Locator.hover has no `steps` option, so the refined movement is a wheel-free mouse move; `steps` defaults to 8, clamped 1..100) → mouse up. Skips the refined move when the target has no bounding box. |
40
+ | `browser_mouse` | `{action: move\|click\|dblclick\|rightclick\|wheel, x, y, deltaX?, deltaY?, button?}` | Raw viewport-coordinate actions without needing an element; `wheel` is the basic page-scroll primitive. |
41
+ | `browser_press_key` | `{key, delay?}` | Page-level `page.keyboard.press` with Playwright key names (`Control+a`, `Enter`, `Meta+c`). |
42
+ | `browser_tabs` | `{action: list\|new\|switch\|close, index?, url?}` | Tab set tracked by the driver; `switch` re-points the current page for all subsequent tools. |
43
+ | `browser_storage` | `{action: get-cookies\|set-cookie\|clear-cookies\|get-local\|set-local\|clear-local, name?, value?, domain?, path?, expires?, key?}` | Cookies go through the context (`set-cookie` requires name/value/domain); localStorage through the page. |
44
+ | `browser_viewport` | `{action: set\|get, width?, height?}` | `set` clamps both dimensions into 320..7680 and reports the requested values when clamping occurred. |
45
+
46
+ All results are `PluginResultV1`; structured payloads (console/network entries, performance metrics, evaluate results, storage and tab state) are
47
+ carried as bounded JSON text (64KiB) in evidence summaries. Readonly host mode registers exactly the seven observation tools (`snapshot`,
48
+ `assert_postcondition`, `collect_evidence`, `console`, `network`, `performance`, `wait`); everything that changes browser or disk state never
49
+ registers there.
50
+
51
+ ## Built-in dialog and download policies
52
+
53
+ No dedicated tools; the driver applies both policies to every opened page:
54
+
55
+ - **Dialogs** are auto-accepted (`dialog.accept()`) and recorded into the console ring buffer as a `[dialog] <type>: <message>` entry, so
56
+ `browser_console` and the console postcondition can observe them.
57
+ - **Downloads** are saved into `<agentDir>/browser-deps/downloads/` (directory created lazily) and the most recent filename is attached to
58
+ `browser_snapshot` results. When no agentDir is known, the filename is still reported but the file is not saved.
59
+
60
+ ## Injection surface
61
+
62
+ `createBrowserCapabilityFactoryV1(runtime)` accepts any `BrowserRuntimeV1 = {connect(): Promise<BrowserDriverV1>; setup?()}` — tests and brokered
63
+ hosts inject fakes. The shipped runtime, `PlaywrightBrowserRuntimeV1`, supports `agentDir`/`browserDepsDir`/`headless`, plus deterministic
64
+ `spawnSync`/`resolvePlaywright`/`loadPlaywright`/`ensureDir` seams for the installer, the lazy module load, and the downloads directory. The default
65
+ export of `dist/entry.js` composes it from the host's `api.host.agentDir`.
66
+
67
+ ## Real-browser test
68
+
69
+ The deterministic suites never download or spawn anything. A real end-to-end session (install -> navigate example.com -> snapshot -> click -> console
70
+ -> performance -> evaluate -> press key -> viewport -> close) runs only with `AGENT_FORGE_BROWSER_REAL_TEST=1` and network access; it uses a throwaway
71
+ agent dir and removes it afterwards.
72
+
73
+ ## Development
74
+
75
+ ```sh
76
+ npm install --ignore-scripts # repo root, creates the workspace symlink
77
+ npm run build # plugins/browser-playwright (tsgo)
78
+ npm test # vitest; the entry fidelity test requires `npm run build` first
79
+ ```
@@ -0,0 +1,25 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.browser-playwright",
4
+ "pluginVersion": "0.88.3",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "capabilityRefs": [
8
+ {
9
+ "id": "browser",
10
+ "version": 1
11
+ }
12
+ ],
13
+ "platform": {
14
+ "os": [
15
+ "windows",
16
+ "macos",
17
+ "linux"
18
+ ],
19
+ "runtime": [
20
+ {
21
+ "name": "node"
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Browser capability factory: registers the seventeen browser tools on a
3
+ * `CapabilityAPI` and shapes every result as `PluginResultV1`.
4
+ *
5
+ * Tool surface: `browser_navigate`, `browser_snapshot`, `browser_locator_action`
6
+ * (click/dblclick/rightclick/fill/type/press/hover/select/check/uncheck/clear/
7
+ * upload/scroll), `browser_assert_postcondition`, `browser_collect_evidence`,
8
+ * the self-installing `browser_setup`, plus the prewritten operation tools
9
+ * `browser_evaluate` (escape hatch), `browser_console`, `browser_network`,
10
+ * `browser_performance`, `browser_wait`, `browser_drag`, `browser_mouse`,
11
+ * `browser_press_key`, `browser_tabs`, `browser_storage`, and
12
+ * `browser_viewport`. The readonly mode registers only the observation-only
13
+ * tools (`snapshot`, `assert_postcondition`, `collect_evidence`, `console`,
14
+ * `network`, `performance`, `wait`); everything that changes browser or disk
15
+ * state never registers there.
16
+ *
17
+ * All browser state lives behind the injected `BrowserRuntimeV1`; the factory
18
+ * itself has no Playwright knowledge. Error messages are sanitized like the
19
+ * pre-split capability: URLs collapse to origin + pathname (<=512) and the
20
+ * total message stays within 2048 chars.
21
+ */
22
+ import { type CapabilityPluginSyncFactory } from "@agent-forge/plugin-sdk";
23
+ import type { BrowserRuntimeV1 } from "./types.ts";
24
+ export declare function createBrowserCapabilityFactoryV1(runtime: BrowserRuntimeV1): CapabilityPluginSyncFactory;
25
+ //# sourceMappingURL=capability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAGN,KAAK,2BAA2B,EAGhC,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAUX,gBAAgB,EAMhB,MAAM,YAAY,CAAC;AAwLpB,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,gBAAgB,GAAG,2BAA2B,CAohBvG","sourcesContent":["/**\n * Browser capability factory: registers the seventeen browser tools on a\n * `CapabilityAPI` and shapes every result as `PluginResultV1`.\n *\n * Tool surface: `browser_navigate`, `browser_snapshot`, `browser_locator_action`\n * (click/dblclick/rightclick/fill/type/press/hover/select/check/uncheck/clear/\n * upload/scroll), `browser_assert_postcondition`, `browser_collect_evidence`,\n * the self-installing `browser_setup`, plus the prewritten operation tools\n * `browser_evaluate` (escape hatch), `browser_console`, `browser_network`,\n * `browser_performance`, `browser_wait`, `browser_drag`, `browser_mouse`,\n * `browser_press_key`, `browser_tabs`, `browser_storage`, and\n * `browser_viewport`. The readonly mode registers only the observation-only\n * tools (`snapshot`, `assert_postcondition`, `collect_evidence`, `console`,\n * `network`, `performance`, `wait`); everything that changes browser or disk\n * state never registers there.\n *\n * All browser state lives behind the injected `BrowserRuntimeV1`; the factory\n * itself has no Playwright knowledge. Error messages are sanitized like the\n * pre-split capability: URLs collapse to origin + pathname (<=512) and the\n * total message stays within 2048 chars.\n */\nimport {\n\tbuildPluginResultV1,\n\ttype CapabilityAPI,\n\ttype CapabilityPluginSyncFactory,\n\ttype DisposableRegistration,\n\ttype PluginResultV1,\n} from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport { boundedPreviewV1 } from \"./setup.ts\";\nimport type {\n\tBrowserDriverV1,\n\tBrowserEvidenceKindV1,\n\tBrowserLocatorActionInputV1,\n\tBrowserLocatorV1,\n\tBrowserMouseButtonV1,\n\tBrowserMouseInputV1,\n\tBrowserNavigateInputV1,\n\tBrowserOperationOptionsV1,\n\tBrowserPostconditionV1,\n\tBrowserRuntimeV1,\n\tBrowserSetupOutcomeV1,\n\tBrowserSnapshotInputV1,\n\tBrowserStorageInputV1,\n\tBrowserTabsInputV1,\n\tBrowserViewportInputV1,\n} from \"./types.ts\";\n\nfunction objectInput(first: unknown, second: unknown): Record<string, unknown> {\n\tconst value = second === undefined ? first : second;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new Error(\"Plugin tool input must be an object\");\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction toolInvocation(\n\tfirst: unknown,\n\tsecond: unknown,\n): { readonly input: Record<string, unknown>; readonly operationId: string } {\n\treturn {\n\t\tinput: objectInput(first, second),\n\t\toperationId: typeof first === \"string\" && first.trim() !== \"\" ? first : \"profile-runtime-tool-call\",\n\t};\n}\n\nfunction signalFrom(value: unknown): AbortSignal | undefined {\n\treturn value instanceof AbortSignal ? value : undefined;\n}\n\nfunction nonEmptyString(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${label} must be a non-empty string`);\n\treturn value;\n}\n\nfunction requiredInteger(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isInteger(value)) throw new Error(`${label} must be an integer`);\n\treturn value;\n}\n\n/** URL-sanitized, 2048-char bounded error text (pre-split browser contract). */\nfunction browserVisibleError(value: unknown): string {\n\tconst raw = value instanceof Error && value.message.trim() !== \"\" ? value.message : \"Browser operation failed\";\n\tconst sanitized = raw.replace(/https?:\\/\\/[^\\s\"'<>]+/giu, (candidate) => {\n\t\ttry {\n\t\t\tconst parsed = new URL(candidate);\n\t\t\treturn `${parsed.origin}${parsed.pathname.slice(0, 512)}`;\n\t\t} catch {\n\t\t\treturn candidate.slice(0, 512);\n\t\t}\n\t});\n\treturn sanitized.length > 2048 ? `${sanitized.slice(0, 2047)}…` : sanitized;\n}\n\nfunction browserAbort(value: unknown): boolean {\n\tif (value instanceof Error && value.name === \"AbortError\") return true;\n\treturn typeof value === \"object\" && value !== null && (value as { code?: unknown }).code === \"ABORT_ERR\";\n}\n\nconst browserLocatorSchema = Type.Object({\n\tkind: Type.Union([\n\t\tType.Literal(\"css\"),\n\t\tType.Literal(\"id\"),\n\t\tType.Literal(\"role\"),\n\t\tType.Literal(\"text\"),\n\t\tType.Literal(\"test-id\"),\n\t]),\n\tvalue: Type.String({ minLength: 1 }),\n\tname: Type.Optional(Type.String()),\n});\n\nconst browserMouseActionSchema = Type.Union([\n\tType.Literal(\"move\"),\n\tType.Literal(\"click\"),\n\tType.Literal(\"dblclick\"),\n\tType.Literal(\"rightclick\"),\n\tType.Literal(\"wheel\"),\n]);\n\nconst browserMouseButtonSchema = Type.Union([Type.Literal(\"left\"), Type.Literal(\"right\"), Type.Literal(\"middle\")]);\n\nconst browserTabsActionSchema = Type.Union([\n\tType.Literal(\"list\"),\n\tType.Literal(\"new\"),\n\tType.Literal(\"switch\"),\n\tType.Literal(\"close\"),\n]);\n\nconst browserStorageActionSchema = Type.Union([\n\tType.Literal(\"get-cookies\"),\n\tType.Literal(\"set-cookie\"),\n\tType.Literal(\"clear-cookies\"),\n\tType.Literal(\"get-local\"),\n\tType.Literal(\"set-local\"),\n\tType.Literal(\"clear-local\"),\n]);\n\nconst browserViewportActionSchema = Type.Union([Type.Literal(\"set\"), Type.Literal(\"get\")]);\n\nconst browserWaitUntilKindSchema = Type.Union([\n\tType.Literal(\"selector\"),\n\tType.Literal(\"navigation\"),\n\tType.Literal(\"networkidle\"),\n\tType.Literal(\"time\"),\n]);\n\nconst browserConsoleLevelSchema = Type.String({\n\tpattern: \"^(error|warning|info|debug)(,(error|warning|info|debug))*$\",\n});\n\nconst browserPostconditionSchema = Type.Union([\n\tType.Object({\n\t\tkind: Type.Literal(\"url\"),\n\t\tequals: Type.String({ minLength: 1 }),\n\t\tcontains: Type.Optional(Type.String({ minLength: 1 })),\n\t}),\n\tType.Object({\n\t\tkind: Type.Literal(\"url\"),\n\t\tequals: Type.Optional(Type.String({ minLength: 1 })),\n\t\tcontains: Type.String({ minLength: 1 }),\n\t}),\n\tType.Object({\n\t\tkind: Type.Literal(\"element\"),\n\t\tlocator: browserLocatorSchema,\n\t\tstate: Type.Union([Type.Literal(\"visible\"), Type.Literal(\"hidden\"), Type.Literal(\"value\")]),\n\t\tvalue: Type.Optional(Type.String()),\n\t}),\n\tType.Object({\n\t\tkind: Type.Literal(\"text\"),\n\t\tlocator: Type.Optional(browserLocatorSchema),\n\t\tcontains: Type.String({ minLength: 1 }),\n\t}),\n\tType.Object({\n\t\tkind: Type.Literal(\"console\"),\n\t\tcontains: Type.String({ minLength: 1 }),\n\t\tlevel: Type.Optional(Type.String()),\n\t}),\n\tType.Object({\n\t\tkind: Type.Literal(\"network\"),\n\t\tcontainsUrl: Type.String({ minLength: 1 }),\n\t\tstatus: Type.Optional(Type.Integer({ minimum: 100, maximum: 599 })),\n\t}),\n]);\n\nconst browserPostconditionInputSchema = Type.Union([\n\tbrowserPostconditionSchema,\n\tType.Array(browserPostconditionSchema, { minItems: 1, maxItems: 32 }),\n]);\n\nfunction setupResult(operationId: string, outcome: BrowserSetupOutcomeV1): PluginResultV1 {\n\tif (!outcome.ok) {\n\t\treturn buildPluginResultV1({\n\t\t\tversion: 1,\n\t\t\toperationId,\n\t\t\tstatus: \"failed\",\n\t\t\tsummary: \"Playwright browser dependency setup failed\",\n\t\t\tartifacts: [],\n\t\t\tevidence: [],\n\t\t\terror: {\n\t\t\t\tversion: 1,\n\t\t\t\tcode: \"browser_setup_failed\",\n\t\t\t\tmessage: boundedPreviewV1(outcome.message, 8192),\n\t\t\t},\n\t\t});\n\t}\n\treturn buildPluginResultV1({\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: \"succeeded\",\n\t\tsummary: outcome.alreadyInstalled\n\t\t\t? \"Playwright browser dependencies are already installed\"\n\t\t\t: \"Playwright browser dependencies installed\",\n\t\tartifacts: [],\n\t\tevidence: [\n\t\t\t{\n\t\t\t\tversion: 1,\n\t\t\t\tevidenceId: `${operationId}-evidence-1`,\n\t\t\t\tkind: \"log\",\n\t\t\t\toperationId,\n\t\t\t\tsummary: outcome.log.trim() === \"\" ? \"(no output)\" : boundedPreviewV1(outcome.log, 4096),\n\t\t\t},\n\t\t],\n\t\tverification: {\n\t\t\tversion: 1,\n\t\t\tstatus: \"passed\",\n\t\t\tchecks: [outcome.alreadyInstalled ? \"playwright-already-installed\" : \"playwright-install-completed\"],\n\t\t},\n\t});\n}\n\nexport function createBrowserCapabilityFactoryV1(runtime: BrowserRuntimeV1): CapabilityPluginSyncFactory {\n\treturn (api: CapabilityAPI) => {\n\t\tconst readonlyMode = api.host.mode === \"readonly\";\n\t\tlet session: Promise<BrowserDriverV1> | undefined;\n\t\tconst connect = (): Promise<BrowserDriverV1> => {\n\t\t\tif (session === undefined) session = runtime.connect();\n\t\t\treturn session;\n\t\t};\n\t\tconst withSession = async (\n\t\t\toperationId: string,\n\t\t\taction: (active: BrowserDriverV1) => Promise<PluginResultV1>,\n\t\t): Promise<PluginResultV1> => {\n\t\t\tlet active: BrowserDriverV1;\n\t\t\ttry {\n\t\t\t\tactive = await connect();\n\t\t\t} catch (error) {\n\t\t\t\tsession = undefined;\n\t\t\t\treturn buildPluginResultV1({\n\t\t\t\t\tversion: 1,\n\t\t\t\t\toperationId,\n\t\t\t\t\tstatus: browserAbort(error) ? \"cancelled\" : \"failed\",\n\t\t\t\t\tsummary: \"Browser session connection failed\",\n\t\t\t\t\tartifacts: [],\n\t\t\t\t\tevidence: [],\n\t\t\t\t\terror: {\n\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\tcode: browserAbort(error) ? \"cancelled\" : \"browser_connection_failed\",\n\t\t\t\t\t\tmessage: browserAbort(error) ? \"Browser operation was cancelled\" : browserVisibleError(error),\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn await action(active);\n\t\t\t} catch (error) {\n\t\t\t\treturn buildPluginResultV1({\n\t\t\t\t\tversion: 1,\n\t\t\t\t\toperationId,\n\t\t\t\t\tstatus: browserAbort(error) ? \"cancelled\" : \"failed\",\n\t\t\t\t\tsummary: \"Browser operation failed\",\n\t\t\t\t\tartifacts: [],\n\t\t\t\t\tevidence: [],\n\t\t\t\t\terror: {\n\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\tcode: browserAbort(error) ? \"cancelled\" : \"browser_operation_failed\",\n\t\t\t\t\t\tmessage: browserAbort(error) ? \"Browser operation was cancelled\" : browserVisibleError(error),\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t}\n\t\t};\n\t\tconst operationOptions = (operationId: string, third: unknown): BrowserOperationOptionsV1 => ({\n\t\t\toperationId,\n\t\t\t...(signalFrom(third) === undefined ? {} : { signal: signalFrom(third) }),\n\t\t});\n\t\tconst registrations: DisposableRegistration[] = [];\n\t\tif (!readonlyMode) {\n\t\t\tregistrations.push(\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_navigate\",\n\t\t\t\t\tlabel: \"Browser navigate\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Navigate the active browser session and return structured verification evidence. Requires Playwright in <agentDir>/browser-deps; when missing, run browser_setup or follow the returned install commands\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\turl: Type.String({ minLength: 1 }),\n\t\t\t\t\t\twaitUntil: Type.Optional(\n\t\t\t\t\t\t\tType.Union([\n\t\t\t\t\t\t\t\tType.Literal(\"commit\"),\n\t\t\t\t\t\t\t\tType.Literal(\"domcontentloaded\"),\n\t\t\t\t\t\t\t\tType.Literal(\"load\"),\n\t\t\t\t\t\t\t\tType.Literal(\"networkidle\"),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.navigate(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\turl: nonEmptyString(input.url, \"url\"),\n\t\t\t\t\t\t\t\t\t...(input.waitUntil === undefined\n\t\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t\t: { waitUntil: input.waitUntil as BrowserNavigateInputV1[\"waitUntil\"] }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tregistrations.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_snapshot\",\n\t\t\t\tlabel: \"Browser snapshot\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Capture accessibility, DOM, or screenshot evidence from the active browser session (styles capture is not supported in this build)\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tincludeDom: Type.Optional(Type.Boolean()),\n\t\t\t\t\tincludeA11y: Type.Optional(Type.Boolean()),\n\t\t\t\t\tincludeStyles: Type.Optional(Type.Boolean()),\n\t\t\t\t\tincludeScreenshot: Type.Optional(Type.Boolean()),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.snapshot(input as BrowserSnapshotInputV1, operationOptions(operationId, third)),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t\tif (!readonlyMode) {\n\t\t\tregistrations.push(\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_locator_action\",\n\t\t\t\t\tlabel: \"Browser locator action\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Perform a locator-based browser action (click/dblclick/rightclick/fill/type/press/hover/select/check/uncheck/clear/upload/scroll) and return post-action accessibility evidence\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\tlocator: browserLocatorSchema,\n\t\t\t\t\t\taction: Type.Union([\n\t\t\t\t\t\t\tType.Literal(\"click\"),\n\t\t\t\t\t\t\tType.Literal(\"dblclick\"),\n\t\t\t\t\t\t\tType.Literal(\"rightclick\"),\n\t\t\t\t\t\t\tType.Literal(\"fill\"),\n\t\t\t\t\t\t\tType.Literal(\"type\"),\n\t\t\t\t\t\t\tType.Literal(\"press\"),\n\t\t\t\t\t\t\tType.Literal(\"hover\"),\n\t\t\t\t\t\t\tType.Literal(\"select\"),\n\t\t\t\t\t\t\tType.Literal(\"check\"),\n\t\t\t\t\t\t\tType.Literal(\"uncheck\"),\n\t\t\t\t\t\t\tType.Literal(\"clear\"),\n\t\t\t\t\t\t\tType.Literal(\"upload\"),\n\t\t\t\t\t\t\tType.Literal(\"scroll\"),\n\t\t\t\t\t\t]),\n\t\t\t\t\t\tvalue: Type.Optional(Type.String({ maxLength: 4096 })),\n\t\t\t\t\t\tkey: Type.Optional(Type.String({ maxLength: 128 })),\n\t\t\t\t\t\tdelay: Type.Optional(Type.Integer({ minimum: 0, maximum: 60_000 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.locatorAction(\n\t\t\t\t\t\t\t\tinput as unknown as BrowserLocatorActionInputV1,\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tregistrations.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_assert_postcondition\",\n\t\t\t\tlabel: \"Browser assertion\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Assert a URL, element, text, console, or network postcondition (single condition or an array of 1..32, all ANDed) without changing the browser\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tcondition: browserPostconditionInputSchema,\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.assertPostcondition(\n\t\t\t\t\t\t\tinput.condition as BrowserPostconditionV1 | readonly BrowserPostconditionV1[],\n\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t\tregistrations.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_collect_evidence\",\n\t\t\t\tlabel: \"Browser evidence\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Collect console, network, or accessibility-snapshot evidence from the active browser session without changing it\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tkind: Type.Union([Type.Literal(\"console\"), Type.Literal(\"network\"), Type.Literal(\"snapshot\")]),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\tconst kind = input.kind;\n\t\t\t\t\tif (kind !== \"console\" && kind !== \"network\" && kind !== \"snapshot\") {\n\t\t\t\t\t\tthrow new Error(\"kind must be console, network, or snapshot\");\n\t\t\t\t\t}\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.collectEvidence(kind as BrowserEvidenceKindV1, operationOptions(operationId, third)),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t\tregistrations.push(\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_console\",\n\t\t\t\tlabel: \"Browser console\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Read the bounded console ring buffer (page messages plus auto-accepted [dialog] traces) with optional level/contains filters, without changing the browser\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tlevel: Type.Optional(browserConsoleLevelSchema),\n\t\t\t\t\tcontains: Type.Optional(Type.String({ minLength: 1 })),\n\t\t\t\t\tlimit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.consoleEntries(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...(typeof input.level === \"string\" ? { level: input.level } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.contains === \"string\" ? { contains: input.contains } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.limit === \"number\" ? { limit: input.limit } : {}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_network\",\n\t\t\t\tlabel: \"Browser network\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Read the bounded network ring buffer (method/url/status/duration/size when available) with optional containsUrl/status/method filters, without changing the browser\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tcontainsUrl: Type.Optional(Type.String({ minLength: 1 })),\n\t\t\t\t\tstatus: Type.Optional(Type.Integer({ minimum: 100, maximum: 599 })),\n\t\t\t\t\tmethod: Type.Optional(Type.String({ minLength: 1, maxLength: 16 })),\n\t\t\t\t\tlimit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.networkEntries(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t...(typeof input.containsUrl === \"string\" ? { containsUrl: input.containsUrl } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.status === \"number\" ? { status: input.status } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.method === \"string\" ? { method: input.method } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.limit === \"number\" ? { limit: input.limit } : {}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_performance\",\n\t\t\t\tlabel: \"Browser performance\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Read structured page performance metrics (Navigation Timing, paint, resource summary, DOM node count) without changing the browser\",\n\t\t\t\tparameters: Type.Object({}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { operationId } = toolInvocation(first, second);\n\t\t\t\t\treturn withSession(operationId, (active) => active.performance(operationOptions(operationId, third)));\n\t\t\t\t},\n\t\t\t}),\n\t\t\tapi.registerTool({\n\t\t\t\tname: \"browser_wait\",\n\t\t\t\tlabel: \"Browser wait\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Wait until a selector is visible, navigation completes, the network goes idle, or a fixed time passes (max 30s) and report the actual wait duration\",\n\t\t\t\tparameters: Type.Object({\n\t\t\t\t\tuntil: browserWaitUntilKindSchema,\n\t\t\t\t\tlocator: Type.Optional(browserLocatorSchema),\n\t\t\t\t\tms: Type.Optional(Type.Integer({ minimum: 0, maximum: 30_000 })),\n\t\t\t\t}),\n\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\tconst until = input.until;\n\t\t\t\t\tif (until !== \"selector\" && until !== \"navigation\" && until !== \"networkidle\" && until !== \"time\") {\n\t\t\t\t\t\tthrow new Error(\"until must be selector, navigation, networkidle, or time\");\n\t\t\t\t\t}\n\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\tactive.wait(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tuntil,\n\t\t\t\t\t\t\t\t...(until === \"selector\" ? { locator: input.locator as BrowserLocatorV1 | undefined } : {}),\n\t\t\t\t\t\t\t\t...(typeof input.ms === \"number\" ? { ms: input.ms } : {}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t}),\n\t\t);\n\t\tif (!readonlyMode) {\n\t\t\tregistrations.push(\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_evaluate\",\n\t\t\t\t\tlabel: \"Browser evaluate\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Escape hatch: evaluate a JavaScript expression in the page context and return the JSON-serialized result (bounded 64KiB, 30s timeout; dialogs are auto-accepted). Keep it a plain expression; for multi-statement logic use an IIFE string like ((() => { ... return x; })()) — a bare () => ... arrow string is not invoked and yields undefined. Only use this when no built-in browser tool covers the operation\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\texpression: Type.String({ minLength: 1, maxLength: 32_768 }),\n\t\t\t\t\t\targ: Type.Optional(Type.Unknown()),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.evaluate(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\texpression: nonEmptyString(input.expression, \"expression\"),\n\t\t\t\t\t\t\t\t\t...(input.arg === undefined ? {} : { arg: input.arg }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_drag\",\n\t\t\t\t\tlabel: \"Browser drag\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Drag from one element to another (hover source, press, hover target, stepped move to target center, release)\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\tfrom: browserLocatorSchema,\n\t\t\t\t\t\tto: browserLocatorSchema,\n\t\t\t\t\t\tsteps: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.drag(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tfrom: input.from as BrowserLocatorV1,\n\t\t\t\t\t\t\t\t\tto: input.to as BrowserLocatorV1,\n\t\t\t\t\t\t\t\t\t...(typeof input.steps === \"number\" ? { steps: input.steps } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_mouse\",\n\t\t\t\t\tlabel: \"Browser mouse\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Raw mouse actions at viewport coordinates without needing an element (move/click/dblclick/rightclick/wheel); wheel is the basic page-scroll primitive\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\taction: browserMouseActionSchema,\n\t\t\t\t\t\tx: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\ty: Type.Integer({ minimum: 0 }),\n\t\t\t\t\t\tdeltaX: Type.Optional(Type.Integer({ minimum: -100_000, maximum: 100_000 })),\n\t\t\t\t\t\tdeltaY: Type.Optional(Type.Integer({ minimum: -100_000, maximum: 100_000 })),\n\t\t\t\t\t\tbutton: Type.Optional(browserMouseButtonSchema),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.mouse(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\taction: input.action as BrowserMouseInputV1[\"action\"],\n\t\t\t\t\t\t\t\t\tx: requiredInteger(input.x, \"x\"),\n\t\t\t\t\t\t\t\t\ty: requiredInteger(input.y, \"y\"),\n\t\t\t\t\t\t\t\t\t...(typeof input.deltaX === \"number\" ? { deltaX: input.deltaX } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.deltaY === \"number\" ? { deltaY: input.deltaY } : {}),\n\t\t\t\t\t\t\t\t\t...(input.button === undefined ? {} : { button: input.button as BrowserMouseButtonV1 }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_press_key\",\n\t\t\t\t\tlabel: \"Browser press key\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Press a page-level key combination by Playwright key name (e.g. Control+a, Enter, Meta+c) without needing an element\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\tkey: Type.String({ minLength: 1, maxLength: 128 }),\n\t\t\t\t\t\tdelay: Type.Optional(Type.Integer({ minimum: 0, maximum: 60_000 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.pressKey(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tkey: nonEmptyString(input.key, \"key\"),\n\t\t\t\t\t\t\t\t\t...(typeof input.delay === \"number\" ? { delay: input.delay } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_tabs\",\n\t\t\t\t\tlabel: \"Browser tabs\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Manage browser tabs: list (title/url/index), new (optional url), switch by index, or close by index\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\taction: browserTabsActionSchema,\n\t\t\t\t\t\tindex: Type.Optional(Type.Integer({ minimum: 0 })),\n\t\t\t\t\t\turl: Type.Optional(Type.String({ minLength: 1, maxLength: 4096 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.tabs(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\taction: input.action as BrowserTabsInputV1[\"action\"],\n\t\t\t\t\t\t\t\t\t...(typeof input.index === \"number\" ? { index: input.index } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.url === \"string\" ? { url: input.url } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_storage\",\n\t\t\t\t\tlabel: \"Browser storage\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Manage browser storage: cookies (get-cookies/set-cookie/clear-cookies via the context) and localStorage (get-local/set-local/clear-local via the page)\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\taction: browserStorageActionSchema,\n\t\t\t\t\t\tname: Type.Optional(Type.String({ minLength: 1, maxLength: 4096 })),\n\t\t\t\t\t\tvalue: Type.Optional(Type.String({ maxLength: 4096 })),\n\t\t\t\t\t\tdomain: Type.Optional(Type.String({ minLength: 1, maxLength: 255 })),\n\t\t\t\t\t\tpath: Type.Optional(Type.String({ maxLength: 1024 })),\n\t\t\t\t\t\texpires: Type.Optional(Type.Number()),\n\t\t\t\t\t\tkey: Type.Optional(Type.String({ minLength: 1, maxLength: 4096 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.storage(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\taction: input.action as BrowserStorageInputV1[\"action\"],\n\t\t\t\t\t\t\t\t\t...(typeof input.name === \"string\" ? { name: input.name } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.value === \"string\" ? { value: input.value } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.domain === \"string\" ? { domain: input.domain } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.path === \"string\" ? { path: input.path } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.expires === \"number\" ? { expires: input.expires } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.key === \"string\" ? { key: input.key } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_viewport\",\n\t\t\t\t\tlabel: \"Browser viewport\",\n\t\t\t\t\tdescription: \"Set the viewport size (width/height clamped into 320..7680) or read the current one\",\n\t\t\t\t\tparameters: Type.Object({\n\t\t\t\t\t\taction: browserViewportActionSchema,\n\t\t\t\t\t\twidth: Type.Optional(Type.Integer({ minimum: 0, maximum: 100_000 })),\n\t\t\t\t\t\theight: Type.Optional(Type.Integer({ minimum: 0, maximum: 100_000 })),\n\t\t\t\t\t}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\t\t\treturn withSession(operationId, (active) =>\n\t\t\t\t\t\t\tactive.viewport(\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\taction: input.action as BrowserViewportInputV1[\"action\"],\n\t\t\t\t\t\t\t\t\t...(typeof input.width === \"number\" ? { width: input.width } : {}),\n\t\t\t\t\t\t\t\t\t...(typeof input.height === \"number\" ? { height: input.height } : {}),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toperationOptions(operationId, third),\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\tif (!readonlyMode) {\n\t\t\tregistrations.push(\n\t\t\t\tapi.registerTool({\n\t\t\t\t\tname: \"browser_setup\",\n\t\t\t\t\tlabel: \"Browser setup\",\n\t\t\t\t\tdescription:\n\t\t\t\t\t\t\"Install Playwright and the Chromium browser into <agentDir>/browser-deps (npm install playwright --prefix <dir>, then npx playwright install chromium). Reports alreadyInstalled when dependencies are present\",\n\t\t\t\t\tparameters: Type.Object({}),\n\t\t\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\t\t\tconst { operationId } = toolInvocation(first, second);\n\t\t\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\t\treturn buildPluginResultV1({\n\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\toperationId,\n\t\t\t\t\t\t\t\tstatus: \"cancelled\",\n\t\t\t\t\t\t\t\tsummary: \"Browser operation was cancelled\",\n\t\t\t\t\t\t\t\tartifacts: [],\n\t\t\t\t\t\t\t\tevidence: [],\n\t\t\t\t\t\t\t\terror: { version: 1, code: \"cancelled\", message: \"Browser operation was cancelled\" },\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Call through the runtime: extracting the method would drop\n\t\t\t\t\t\t// `this`, and the production class reads `this.options` in setup().\n\t\t\t\t\t\tif (runtime.setup === undefined) {\n\t\t\t\t\t\t\treturn buildPluginResultV1({\n\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\toperationId,\n\t\t\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\t\t\tsummary: \"This browser runtime does not support self-installation\",\n\t\t\t\t\t\t\t\tartifacts: [],\n\t\t\t\t\t\t\t\tevidence: [],\n\t\t\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\t\tcode: \"browser_setup_unsupported\",\n\t\t\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\t\t\"The injected browser runtime exposes no setup face. Install Playwright manually with npm install playwright --prefix <agentDir>/browser-deps and npx playwright install chromium.\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn setupResult(operationId, await runtime.setup());\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\treturn buildPluginResultV1({\n\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\toperationId,\n\t\t\t\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t\t\t\t\tsummary: \"Playwright browser dependency setup failed\",\n\t\t\t\t\t\t\t\tartifacts: [],\n\t\t\t\t\t\t\t\tevidence: [],\n\t\t\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t\t\tversion: 1,\n\t\t\t\t\t\t\t\t\tcode: \"browser_setup_failed\",\n\t\t\t\t\t\t\t\t\tmessage: browserVisibleError(error),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tdispose: () => {\n\t\t\t\tif (session === undefined) return;\n\t\t\t\t// Both a failed connection and a failed close happen after this synchronous\n\t\t\t\t// disposer returns. Consume either rejection so disposal cannot leak a promise.\n\t\t\t\tvoid session.then((active) => active.close()).catch(() => undefined);\n\t\t\t},\n\t\t};\n\t};\n}\n"]}