From f3a2abe1e47c505c999a15d6975e267a9b31872e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91=E6=9C=8D=E5=8A=A1=E9=83=A8-=E5=8F=B6=E6=9E=97?= =?UTF-8?q?=E7=AB=8B?= <1361666059@qq.com> Date: Fri, 28 Aug 2026 14:45:37 +0800 Subject: [PATCH] feat(chrome): hand snapshots to context mode --- AGENTS.md | 20 +- README.md | 4 +- extensions/tool-routing.ts | 6 + package-lock.json | 39 +- package.json | 5 +- pi-chrome/.gitignore | 6 + pi-chrome/CHANGELOG.md | 306 ++ pi-chrome/CONTRIBUTING.md | 50 + pi-chrome/LICENSE | 21 + pi-chrome/README.md | 155 + pi-chrome/SECURITY.md | 38 + pi-chrome/UPSTREAM.md | 23 + pi-chrome/docs/ARCHITECTURE.md | 74 + pi-chrome/docs/COMPARISON.md | 166 ++ pi-chrome/docs/EXAMPLES.md | 166 ++ pi-chrome/docs/FAQ.md | 84 + .../browser-extension/manifest.json | 26 + .../browser-extension/service_worker.js | 2603 +++++++++++++++++ .../browser-extension/snapshot_injected.js | 677 +++++ .../extensions/chrome-profile-bridge/index.ts | 1915 ++++++++++++ pi-chrome/package-lock.json | 38 + pi-chrome/package.json | 86 + pi-chrome/scripts/sync-manifest-version.js | 37 + pi-chrome/src/session-snapshot-store.ts | 140 + pi-chrome/test-suite/README.md | 194 ++ pi-chrome/test-suite/_lib.js | 130 + pi-chrome/test-suite/_style.css | 31 + pi-chrome/test-suite/baseline-dashboard.png | Bin 0 -> 439285 bytes .../test-suite/browsergym-action-space.json | 44 + .../challenges/01-is-trusted-click.html | 28 + .../challenges/02-is-trusted-keyboard.html | 50 + .../challenges/03-webdriver-flag.html | 51 + .../challenges/04-mouse-entropy.html | 34 + .../challenges/05-event-timing.html | 34 + .../challenges/06-click-coordinates.html | 29 + .../challenges/07-pointer-properties.html | 29 + .../challenges/08-keyboard-cadence.html | 37 + .../challenges/09-composition-input.html | 45 + .../challenges/10-user-activation.html | 40 + .../test-suite/challenges/11-honeypot.html | 36 + .../test-suite/challenges/12-fingerprint.html | 59 + .../test-suite/challenges/13-focus-order.html | 31 + .../challenges/14-wheel-scroll.html | 28 + .../challenges/15-drag-drop-datatransfer.html | 73 + .../16-contenteditable-selection.html | 54 + .../challenges/17-paste-clipboard.html | 48 + .../challenges/18-native-select.html | 56 + .../test-suite/challenges/19-hover-dwell.html | 50 + .../challenges/20-react-value-tracker.html | 78 + .../challenges/21-keyboard-modifiers.html | 65 + .../challenges/22-touch-events.html | 66 + .../23-stack-trace-fingerprint.html | 76 + .../challenges/24-viewport-edge-clicks.html | 51 + .../challenges/25-pointer-continuity.html | 62 + .../challenges/26-mousemove-rate.html | 57 + .../challenges/27-scroll-momentum.html | 66 + .../28-intersection-visibility.html | 72 + .../challenges/29-shadow-dom-controls.html | 44 + .../challenges/30-iframe-targeting.html | 44 + .../test-suite/challenges/31-file-upload.html | 30 + .../32-keyboard-tab-navigation.html | 61 + .../33-network-console-capture.html | 33 + .../challenges/34-dialog-handling.html | 28 + .../challenges/35-target-blank-popup.html | 35 + .../challenges/36-modal-focus-trap.html | 49 + .../challenges/37-autocomplete-combobox.html | 42 + .../challenges/38-spa-route-change.html | 40 + .../challenges/39-strict-csp-fallback.html | 14 + .../challenges/39-strict-csp-fallback.js | 27 + .../challenges/40-dynamic-wait-readiness.html | 41 + .../challenges/41-tab-lifecycle.html | 44 + .../challenges/42-strict-csp-evaluate.html | 16 + .../challenges/42-strict-csp-evaluate.js | 21 + .../test-suite/fixtures/pi-chrome-upload.txt | 1 + .../fixtures/sites/mini-shop/cheats.js | 12 + .../fixtures/sites/mini-shop/grader.js | 29 + .../fixtures/sites/mini-shop/index.html | 55 + .../fixtures/sites/mini-shop/tasks.json | 9 + pi-chrome/test-suite/index.html | 193 ++ pi-chrome/test-suite/manifest.json | 1681 +++++++++++ pi-chrome/test-suite/manifest.schema.json | 73 + .../test-suite/notes/browsergym-compat.md | 70 + pi-chrome/test-suite/notes/bypass-ideas.md | 79 + pi-chrome/test-suite/notes/profiles.md | 22 + pi-chrome/test-suite/notes/runner-spec.md | 29 + pi-chrome/test-suite/notes/scoring.md | 44 + .../test-suite/scenarios/choredesk/cheats.js | 49 + .../test-suite/scenarios/choredesk/index.html | 239 ++ pi-chrome/test-suite/serve.sh | 6 + pi-chrome/test-suite/task-manifest.json | 416 +++ .../test-suite/task-manifest.schema.json | 59 + .../unit/automation-target.test.mjs | 395 +++ pi-chrome/test-suite/unit/csp-eval.test.mjs | 192 ++ pi-chrome/test/session-snapshot-store.test.ts | 100 + tests/extension-load.test.ts | 7 +- tests/tool-routing.test.ts | 8 + 96 files changed, 12800 insertions(+), 26 deletions(-) create mode 100644 pi-chrome/.gitignore create mode 100644 pi-chrome/CHANGELOG.md create mode 100644 pi-chrome/CONTRIBUTING.md create mode 100644 pi-chrome/LICENSE create mode 100644 pi-chrome/README.md create mode 100644 pi-chrome/SECURITY.md create mode 100644 pi-chrome/UPSTREAM.md create mode 100644 pi-chrome/docs/ARCHITECTURE.md create mode 100644 pi-chrome/docs/COMPARISON.md create mode 100644 pi-chrome/docs/EXAMPLES.md create mode 100644 pi-chrome/docs/FAQ.md create mode 100644 pi-chrome/extensions/chrome-profile-bridge/browser-extension/manifest.json create mode 100644 pi-chrome/extensions/chrome-profile-bridge/browser-extension/service_worker.js create mode 100644 pi-chrome/extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js create mode 100644 pi-chrome/extensions/chrome-profile-bridge/index.ts create mode 100644 pi-chrome/package-lock.json create mode 100644 pi-chrome/package.json create mode 100644 pi-chrome/scripts/sync-manifest-version.js create mode 100644 pi-chrome/src/session-snapshot-store.ts create mode 100644 pi-chrome/test-suite/README.md create mode 100644 pi-chrome/test-suite/_lib.js create mode 100644 pi-chrome/test-suite/_style.css create mode 100644 pi-chrome/test-suite/baseline-dashboard.png create mode 100644 pi-chrome/test-suite/browsergym-action-space.json create mode 100644 pi-chrome/test-suite/challenges/01-is-trusted-click.html create mode 100644 pi-chrome/test-suite/challenges/02-is-trusted-keyboard.html create mode 100644 pi-chrome/test-suite/challenges/03-webdriver-flag.html create mode 100644 pi-chrome/test-suite/challenges/04-mouse-entropy.html create mode 100644 pi-chrome/test-suite/challenges/05-event-timing.html create mode 100644 pi-chrome/test-suite/challenges/06-click-coordinates.html create mode 100644 pi-chrome/test-suite/challenges/07-pointer-properties.html create mode 100644 pi-chrome/test-suite/challenges/08-keyboard-cadence.html create mode 100644 pi-chrome/test-suite/challenges/09-composition-input.html create mode 100644 pi-chrome/test-suite/challenges/10-user-activation.html create mode 100644 pi-chrome/test-suite/challenges/11-honeypot.html create mode 100644 pi-chrome/test-suite/challenges/12-fingerprint.html create mode 100644 pi-chrome/test-suite/challenges/13-focus-order.html create mode 100644 pi-chrome/test-suite/challenges/14-wheel-scroll.html create mode 100644 pi-chrome/test-suite/challenges/15-drag-drop-datatransfer.html create mode 100644 pi-chrome/test-suite/challenges/16-contenteditable-selection.html create mode 100644 pi-chrome/test-suite/challenges/17-paste-clipboard.html create mode 100644 pi-chrome/test-suite/challenges/18-native-select.html create mode 100644 pi-chrome/test-suite/challenges/19-hover-dwell.html create mode 100644 pi-chrome/test-suite/challenges/20-react-value-tracker.html create mode 100644 pi-chrome/test-suite/challenges/21-keyboard-modifiers.html create mode 100644 pi-chrome/test-suite/challenges/22-touch-events.html create mode 100644 pi-chrome/test-suite/challenges/23-stack-trace-fingerprint.html create mode 100644 pi-chrome/test-suite/challenges/24-viewport-edge-clicks.html create mode 100644 pi-chrome/test-suite/challenges/25-pointer-continuity.html create mode 100644 pi-chrome/test-suite/challenges/26-mousemove-rate.html create mode 100644 pi-chrome/test-suite/challenges/27-scroll-momentum.html create mode 100644 pi-chrome/test-suite/challenges/28-intersection-visibility.html create mode 100644 pi-chrome/test-suite/challenges/29-shadow-dom-controls.html create mode 100644 pi-chrome/test-suite/challenges/30-iframe-targeting.html create mode 100644 pi-chrome/test-suite/challenges/31-file-upload.html create mode 100644 pi-chrome/test-suite/challenges/32-keyboard-tab-navigation.html create mode 100644 pi-chrome/test-suite/challenges/33-network-console-capture.html create mode 100644 pi-chrome/test-suite/challenges/34-dialog-handling.html create mode 100644 pi-chrome/test-suite/challenges/35-target-blank-popup.html create mode 100644 pi-chrome/test-suite/challenges/36-modal-focus-trap.html create mode 100644 pi-chrome/test-suite/challenges/37-autocomplete-combobox.html create mode 100644 pi-chrome/test-suite/challenges/38-spa-route-change.html create mode 100644 pi-chrome/test-suite/challenges/39-strict-csp-fallback.html create mode 100644 pi-chrome/test-suite/challenges/39-strict-csp-fallback.js create mode 100644 pi-chrome/test-suite/challenges/40-dynamic-wait-readiness.html create mode 100644 pi-chrome/test-suite/challenges/41-tab-lifecycle.html create mode 100644 pi-chrome/test-suite/challenges/42-strict-csp-evaluate.html create mode 100644 pi-chrome/test-suite/challenges/42-strict-csp-evaluate.js create mode 100644 pi-chrome/test-suite/fixtures/pi-chrome-upload.txt create mode 100644 pi-chrome/test-suite/fixtures/sites/mini-shop/cheats.js create mode 100644 pi-chrome/test-suite/fixtures/sites/mini-shop/grader.js create mode 100644 pi-chrome/test-suite/fixtures/sites/mini-shop/index.html create mode 100644 pi-chrome/test-suite/fixtures/sites/mini-shop/tasks.json create mode 100644 pi-chrome/test-suite/index.html create mode 100644 pi-chrome/test-suite/manifest.json create mode 100644 pi-chrome/test-suite/manifest.schema.json create mode 100644 pi-chrome/test-suite/notes/browsergym-compat.md create mode 100644 pi-chrome/test-suite/notes/bypass-ideas.md create mode 100644 pi-chrome/test-suite/notes/profiles.md create mode 100644 pi-chrome/test-suite/notes/runner-spec.md create mode 100644 pi-chrome/test-suite/notes/scoring.md create mode 100644 pi-chrome/test-suite/scenarios/choredesk/cheats.js create mode 100644 pi-chrome/test-suite/scenarios/choredesk/index.html create mode 100755 pi-chrome/test-suite/serve.sh create mode 100644 pi-chrome/test-suite/task-manifest.json create mode 100644 pi-chrome/test-suite/task-manifest.schema.json create mode 100644 pi-chrome/test-suite/unit/automation-target.test.mjs create mode 100644 pi-chrome/test-suite/unit/csp-eval.test.mjs create mode 100644 pi-chrome/test/session-snapshot-store.test.ts diff --git a/AGENTS.md b/AGENTS.md index 802e980..c299708 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,10 @@ - `extensions/permission-system.ts`:直接加载仓库内 `pi-permission-system/src/index.ts`,并在权限扩展注册前将 `config/pi-permission-system.json` 同步为全局权威配置。 - `extensions/lsp.ts`:把组合包内 TypeScript Language Server CLI 物化为绝对命令路径,部署 TypeScript/Kotlin/JDT LS 全局配置并加载 `pi-lsp`。 - `extensions/tool-routing.ts`:保留 Pi 默认系统提示词,在每轮开始前按激活工具追加简短路由规则,并提供 `/dump-system-prompt` 将扩展所见的有效提示词写入 `.pi-debug/effective-system-prompt.md`。 -- `pi-chrome@0.15.46`:直接加载上游 Chrome Profile Bridge,通过用户安装的 unpacked MV3 扩展与 `127.0.0.1:17318` loopback bridge 接管当前已登录 Chrome;控制默认锁定,每个 Pi session 必须显式 `/chrome authorize`。 +- `pi-chrome/`:从上游源码导入并由本仓库直接维护,通过用户安装的 unpacked MV3 扩展与 `127.0.0.1:17318` loopback bridge 接管当前已登录 Chrome;控制默认锁定,每个 Pi session 必须显式 `/chrome authorize`。 +- 上游来源: +- 初始导入快照:`017ff4b9a639f0b8b213e58a3f30613fc38edcc8`(`0.15.46`,该 commit 无对应 tag)。 +- 该目录从明确 commit 导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`、`node_modules` 或构建产物。 - `pi-tool-search/`:从上游 `v0.3.6` 源码导入并由本仓库直接维护;从完整工具定义生成并缓存经过校验的 Tool Cards/工作流分组,通过固定工具 + 有界动态组 LRU 加载当前 Pi 的原始 schema。 - 上游来源: - 初始导入快照:`ddfb23646fd3957b791214de278e23aa393c9b13`(`v0.3.6`)。 @@ -63,7 +66,7 @@ - 初始导入快照:`f1d0efd24a7f4ae99d19e10c5f4c3770a3bdd845`(`@firstpick/pi-extension-codex-fast-mode@0.1.1`)。 - 该目录从上游源码仓库导入并由本仓库直接维护,不使用 npm 预编译产物,不是 submodule,也不保留嵌套 `.git`。 - `pi-condense@2.9.1`:总结已完成的工具调用批次,以短 stub 替换历史原始输出,并通过 `context_tree_query` 按需恢复;组合包在用户尚未配置 `contextPrune.enabled` 时默认开启。 -- 根包还固定安装 `@tavily/pi-extension@0.1.2`、`@keenable/pi-search@0.1.2`、`pi-chrome@0.15.46`、`pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0`、`typescript@6.0.3` 和 `pi-context-view`;`@firstpick/pi-extension-codex-fast-mode`、`@gotgenes/pi-permission-system`、`@smoose/pi-notify`、`@tintinweb/pi-subagents`、`pi-ask-user`、`pi-minimal-footer`、`pi-ssh` 与 `pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖。 +- 根包还固定安装 `@tavily/pi-extension@0.1.2`、`@keenable/pi-search@0.1.2`、`pi-hashline-edit@0.8.3`、`typescript-language-server@5.3.0`、`typescript@6.0.3` 和 `pi-context-view`;`@firstpick/pi-extension-codex-fast-mode`、`@gotgenes/pi-permission-system`、`@smoose/pi-notify`、`@tintinweb/pi-subagents`、`pi-ask-user`、`pi-chrome`、`pi-minimal-footer`、`pi-ssh` 与 `pi-tool-search` 分别指向仓库内对应目录的本地 `file:` 依赖。 - `install.sh`:先安装根组合包,再交互检查 Hippo Memory CLI、Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Hippo 项目目录与 `hippo init` 始终由用户自行决定。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。 - `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。升级成功后只能调用可定位的更新后组合包 Node helper 检查 Pi agent `settings.json`:只有 `toolSearch` 完整精确匹配旧版 `3` 组 / `20` 工具默认快照时,才创建从生成起即为权限 `600` 的时间戳备份、以同目录临时文件原子迁移到 `5` / `28` 并写入 `bundleDefaultsVersion: 2`;任何自定义或无效配置必须保留,提交前检测到内容变化必须中止。无法从 `PI_PACKAGE_SOURCE` 定位更新后 helper 时必须保留配置并报告失败,不能回退到可能陈旧的脚本目录 helper。其他组件升级前先查询并比较本地与远端版本,只有版本不同时才下载或替换;Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。 - `uninstall.sh`:先移除根组合包;若检测到 Hippo Memory CLI,再明确询问是否卸载确认属于 npm 全局安装的 `hippo-memory`,默认保留且始终不删除 `.hippo/` 或用户记忆数据。其他共享终端环境和机器工具不卸载。 @@ -76,11 +79,12 @@ - `readCompaction.enabled`、`sourceCodeFilteringEnabled` 和 `smartTruncate.enabled` 当前均默认 `false`,源码读取保持原样。未经用户明确决定,不因节省上下文而改变这些默认值。 - 若以后开启 read 压缩,优先考虑 `readCompaction + smartTruncate`,源码过滤仍独立评估;必须保留精确 `offset/limit` 读取、短文件和行锚点的完整性。 - Context Mode 负责避免批量读取、命令研究和网页原始内容直接撑大上下文;FFF 仍负责精确字面搜索,RTK 仍处理未走 Context Mode 的普通输出。 +- `chrome_snapshot`、`chrome_find` 以及带 `includeSnapshot=true` 的交互工具必须把完整结构化快照写入当前项目 `.pi/chrome-context/` 下的 Session 临时文件,工具结果只返回相对路径与有界摘要;模型必须立即用 `ctx_execute_file` 提取下一步所需信息,不得用 `read` / `cat` 把原始 JSON 重新塞入上下文。每个 Session/目标只保留最新快照,页面交互后旧快照视为失效,Session shutdown、撤销或授权过期时清理。 - Hashline 默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 和 `replaceText` 均关闭;它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。 - 工具路由规则不得替换 Pi 默认系统提示词:代码结构、调用关系和待修改 symbol 优先 CodeGraph,定义/引用/类型/诊断优先 LSP;工作区内大文件探索、分析和总结优先 `ctx_execute_file`,日志、构建与不可预测命令输出优先 Context Mode;字面搜索先用 FFF `find` 收敛文件位置,再在收敛路径内用 `grep`/`multi_grep` 获取行号;真正修改前才用小范围 Hashline `read` 获取新鲜锚点并用锚定 `edit`。在线搜索按场景只先选一个服务:广泛发现、新闻和候选来源用 Tavily,官方/技术/论文和高确定性来源用 Exa,中文、站点限定和日期筛选用 Keenable;只有必要时才跨服务复核。遇到大量结果、截断或上限时继续缩小范围,不得靠提高 limit 或倾倒全部结果解决。 - `ask_user_question` 只用于模型继续任务所必需的用户决定、偏好、确认或澄清;不得重复询问直接用户消息中已有的信息。工具仅支持交互式 TUI、顺序执行和最多 8 题;取消或 abort 必须返回 `cancelled: true`,不得伪造答案,且 `/qna` 不属于该扩展。其成功 `details.answers` 必须保留 `question` / `answer` 字段,以匹配 AutoReview 已有的可信结构化用户交互边界。 - Tool Search 默认常驻核心 `read` / `write` / `edit` / `bash` / `grep` / `find`、`codegraph_explore`、`lsp_diagnostics`、`ctx_execute` / `ctx_execute_file` / `ctx_batch_execute` 与 `tool_search`;Windows 上内置 `powershell` 必须与 `bash` 同属固定本地 Shell 核心工具,非 Windows 平台必须从目录中过滤。首次创建完整默认 `toolSearch` 段时必须通过唯一同目录临时文件以权限 `600` 原子写入并记录 `bundleDefaultsVersion: 2`,已有自定义段只补缺失运行时字段且不得伪造迁移来源标记。其余组合包工具必须优先使用 `pi-tool-search/extensions/bundle-groups.ts` 中提交的权威工作流分组;精确已有分组必须直接激活,不得等待无关未知工具的模型增强。默认最多 5 个动态组、模型组最多 8 个工具、动态工具总数最多 28;`groups` 可原子激活最多 5 个精确组,完整请求超限时不得改变当前状态;加载或组内调用更新 LRU,超限先移除最久未用的非请求组。自然语言查询必须优先精确工具名和已提交 workflow alias,弱匹配或近似同分只能返回候选,不得任意激活;`/tool-search-status` 必须显示当前容量、LRU 顺序与最近淘汰原因。 -- `pi-chrome` 在未授权时不注册 Chrome 工具;显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。Tool Search 重新激活 schema 不能替代上游每次执行时的授权校验。 +- `pi-chrome` 在未授权时不注册 Chrome 工具;显式授权后出现的 21 个 `chrome_*` 工具必须由 Tool Search 分入 `chrome-navigation`、`chrome-interaction`、`chrome-debugging`,不得落入 unknown-tools 模型增强路径。授权撤销或过期时必须停用全部 21 个工具,包括 `chrome_find` / `chrome_inspect`;Tool Search 重新激活 schema 不能替代每次执行时的授权校验。 - 标准组合包工具全部命中预置目录时 Tool Search 不得调用模型、创建用户缓存或发送隐藏 schema。只有存在未识别第三方工具时才允许惰性模型增强;生成结果必须保留全部预置 assignment,模型元数据不得替代真实名称、参数或 schema。缓存必须位于 agent 目录且权限 `600`;模型失败必须继续使用预置目录 + 确定性未知工具分组。`groupOverrides` 优先级高于预置分组。 - Tool Search 必须直接使用当前 `@earendil-works/pi-coding-agent`、`typebox`、`ModelRegistry.complete()` 和 `setActiveTools()`;首次纯增加载使用当前 Pi 的增量结果传播,LRU 替换允许走宿主安全 fallback。不得安装或映射旧 `@mariozechner` runtime,不得恢复 provider payload 改写、代理分发或隐藏 `sendMessage` steer/retry 兼容循环。 - `.pi-debug/` 是 `/dump-system-prompt` 生成的本地诊断目录,不提交到仓库,也不作为组合包运行时配置源。 @@ -200,6 +204,16 @@ - 默认只在 TUI `agent_settled` 后通知,非 TUI 输出完整性、自动 retry/compaction/follow-up 去重、总耗时、短任务阈值和多窗口点击聚焦都属于回归边界。 - 根包必须通过 `file:./pi-notify` 提供依赖并直接加载源码,不得同时加载 npm `@smoose/pi-notify` 实现。 +## `pi-chrome` 开发约定 + +- 扩展入口是 `pi-chrome/extensions/chrome-profile-bridge/index.ts`,Companion MV3 扩展位于其 `browser-extension/`,Session 快照存储位于 `pi-chrome/src/session-snapshot-store.ts`,测试保留在 `pi-chrome/test/` 与 `pi-chrome/test-suite/`,来源记录是 `pi-chrome/UPSTREAM.md`。 +- 同步上游必须从官方 `tianrendong/pi-chrome` 的明确 tag/commit 移植并保留 README、CHANGELOG、SECURITY、文档、测试、作者和 LICENSE;不得导入嵌套 `.git`、`node_modules` 或构建产物。 +- 根包必须通过 `file:./pi-chrome` 提供依赖并直接加载仓库源码,不得同时安装或加载 npm 预编译入口。 +- 完整 Chrome snapshot 不得进入普通工具文本或 `details`;只能原子写入当前项目 `.pi/chrome-context///snapshot.json`,使用哈希目录、POSIX `700` 目录 / `600` 文件,并只返回工作区相对路径和有界元数据。快照不得跨 Session 持久化,Session 启动/关闭、撤销和授权过期必须清理。 +- `chrome_snapshot` 和带 `includeSnapshot=true` 的动作必须引导模型立即使用 `ctx_execute_file`,不得使用 `read` / `cat`;任何可能改变页面或目标的成功操作必须使旧快照失效,`chrome_find` 只可额外返回有界匹配摘要。 +- 修改 bridge、Companion 扩展、授权 lifecycle、快照存储或加载入口后至少运行包内 `npm test`、根 Tool Routing/Tool Search/扩展联合加载测试、`npm pack --dry-run --json` 和实际 packed tarball 隔离安装验证;未经明确要求不得连接或控制真实 Chrome。 + + ## `pi-ssh` 开发约定 - 扩展入口是 `pi-ssh/index.ts`,`ssh2` transport 与 SFTP 实现在 `pi-ssh/src/ssh2-transport.ts`,有界自适应远程搜索位于 `pi-ssh/src/remote-search.ts`,AES-GCM vault 位于 `pi-ssh/src/vault.ts`,选择性导入入口是根 `ssh_config.sh` 与 `pi-ssh/scripts/ssh-config.mjs`,权限桥接位于 `pi-ssh/permission-integration.ts`,测试保留在 `pi-ssh/test/`,来源记录是 `pi-ssh/UPSTREAM.md`。 diff --git a/README.md b/README.md index 02cb5c5..c4e86d2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - `pi-condense@2.9.1`:把已完成的工具调用批次总结为可恢复的短摘要,并通过 `context_tree_query` 按需取回原始输出;组合包首次加载时默认开启。 - 本仓库维护的 `pi-ask-user`:提供模型主动调用的 `ask_user_question`,用一个顺序 TUI 同时支持单题、多题、选择题、文本题、自定义回答与提交前复核。 - 本仓库维护的 `pi-tool-search`:从完整工具定义生成并缓存经过校验的工作流分组,默认以最多 5 个动态组、28 个动态工具的 LRU 策略加载原始完整 schema,并支持原子批量激活。 -- `pi-chrome@0.15.46`:通过显式会话授权接入用户当前已登录的 Chrome Profile,并由 Tool Search 按导航观察、页面交互和调试上传三个工作流按需加载 21 个 `chrome_*` 工具。 +- 本仓库维护的 `pi-chrome`:从上游 `0.15.46` 源码导入,通过显式会话授权接入用户当前已登录的 Chrome Profile;大体积页面快照写入 Session 临时文件并交给 Context Mode 隔离分析,21 个 `chrome_*` 工具仍由 Tool Search 按工作流加载。 - 本仓库维护的 `pi-permission-auto-review`:作为 `pi-permission-system` authorizer,使用 Codex Guardian 风格策略自动复核 `ask` 请求。 - 本仓库维护的 `pi-permission-system`:从 `@gotgenes/pi-permission-system@26.2.1` 源码导入,负责工具、路径、MCP、硬拒绝和兜底权限基线。 - 本仓库维护的 `pi-subagents`:从 `@tintinweb/pi-subagents@0.18.0` 源码导入并默认加载,提供隔离子 Agent、后台执行、steering、resume、嵌套代理与自定义 Agent;通过 mandatory permission wrapper 接入权限 child lifecycle、根 authority `ask` 转发和根 transcript auto-review 身份桥接,其 3 个编排工具由 Tool Search 的 `subagents` 组按需加载。 @@ -145,6 +145,8 @@ CodeGraph 本体不由组合包安装。需要使用 CodeGraph 的机器应自 授权后注册的 21 个 `chrome_*` 工具不会常驻提示词;Tool Search 将它们分为 `chrome-navigation`、`chrome-interaction` 和 `chrome-debugging`。普通网页资料发现仍使用 Tavily/Exa/Keenable,只有需要读取登录态页面或实际操作当前 Chrome 时才加载 Chrome 组。 +`chrome_snapshot`、`chrome_find` 和带 `includeSnapshot=true` 的交互不会再把完整页面结构直接塞进模型上下文:完整 JSON 只写入当前项目 `.pi/chrome-context/` 下按 Session/目标隔离的临时文件,工具结果仅返回相对路径和小型摘要。模型应立即用 `ctx_execute_file` 从该文件提取下一步所需的 uid、文本或状态,不能用 `read` / `cat` 重新读入全文。每个目标只保留最新快照;页面操作后旧快照失效,Session 结束、`/chrome revoke` 或授权过期时清理。 + ### 按需加载工具 schema `pi-tool-search/` 从上游 的 `v0.3.6`、commit `ddfb23646fd3957b791214de278e23aa393c9b13` 导入,并像 `pi-rtk-optimizer/` 一样由本仓库直接维护;它不是 submodule,不加载 npm 预编译入口,也不保留嵌套 `.git`、上游 `.pi` 状态或构建产物。来源、许可证说明和本地差异记录在 `pi-tool-search/UPSTREAM.md`。 diff --git a/extensions/tool-routing.ts b/extensions/tool-routing.ts index e8a8f2a..33645d0 100644 --- a/extensions/tool-routing.ts +++ b/extensions/tool-routing.ts @@ -98,6 +98,12 @@ export function buildToolRoutingSection(selectedTools: SelectedTools): string { ); } + if (hasTool(selectedTools, "chrome_snapshot") && hasTool(selectedTools, "ctx_execute_file")) { + rules.push( + "- After chrome_snapshot or a Chrome action with includeSnapshot=true, immediately analyze the returned workspace-relative capture path with ctx_execute_file; never use read or cat for the raw JSON. Print only the uids, text, or state needed for the next action, and take a fresh snapshot after any interaction because the previous capture may be stale.", + ); + } + if (hasTool(selectedTools, "read")) { rules.push( "- Use read only when exact source is needed or the file is small. Before editing, request the smallest useful offset/limit range; do not read a complete large file merely to copy, compare, hash, count, explore, or summarize it.", diff --git a/package-lock.json b/package-lock.json index 3a54ff6..9056d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "context-mode": "1.0.169", "jiti": "2.7.0", "pi-ask-user": "file:./pi-ask-user", - "pi-chrome": "0.15.46", + "pi-chrome": "file:./pi-chrome", "pi-condense": "2.9.1", "pi-context-view": "0.4.2", "pi-hashline-edit": "0.8.3", @@ -7438,22 +7438,8 @@ "link": true }, "node_modules/pi-chrome": { - "version": "0.15.46", - "resolved": "https://registry.npmjs.org/pi-chrome/-/pi-chrome-0.15.46.tgz", - "integrity": "sha512-4ZG4h4F3OkAwb9Os2LAiMAFqCGdr6cgUZSna0d6L+cR8eUDvI537UMuqLrf4sepy7US5rk3W0dvqy19KGfK3IQ==", - "license": "MIT", - "peerDependencies": { - "@earendil-works/pi-coding-agent": "*", - "typebox": "*" - }, - "peerDependenciesMeta": { - "@earendil-works/pi-coding-agent": { - "optional": true - }, - "typebox": { - "optional": true - } - } + "resolved": "pi-chrome", + "link": true }, "node_modules/pi-condense": { "version": "2.9.1", @@ -9047,6 +9033,25 @@ } } }, + "pi-chrome": { + "version": "0.15.46", + "license": "MIT", + "devDependencies": { + "jiti": "2.7.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*", + "typebox": "*" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-coding-agent": { + "optional": true + }, + "typebox": { + "optional": true + } + } + }, "pi-extension-codex-fast-mode": { "name": "@firstpick/pi-extension-codex-fast-mode", "version": "0.1.1", diff --git a/package.json b/package.json index c05e10a..2b3ca04 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "extensions", "hippo-memory-pi", "pi-ask-user", + "pi-chrome", "pi-extension-codex-fast-mode", "pi-rtk-optimizer", "pi-minimal-footer", @@ -67,7 +68,7 @@ "./extensions/subagents.ts", "./pi-ssh/index.ts", "./pi-ask-user/index.ts", - "./node_modules/pi-chrome/extensions/chrome-profile-bridge/index.ts", + "./pi-chrome/extensions/chrome-profile-bridge/index.ts", "./extensions/tool-routing.ts", "./pi-tool-search/extensions/index.ts" ], @@ -89,7 +90,7 @@ "context-mode": "1.0.169", "jiti": "2.7.0", "pi-ask-user": "file:./pi-ask-user", - "pi-chrome": "0.15.46", + "pi-chrome": "file:./pi-chrome", "pi-condense": "2.9.1", "pi-context-view": "0.4.2", "pi-hashline-edit": "0.8.3", diff --git a/pi-chrome/.gitignore b/pi-chrome/.gitignore new file mode 100644 index 0000000..6c0ac9e --- /dev/null +++ b/pi-chrome/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.pi/ +*.log +.DS_Store +dist/ +build/ diff --git a/pi-chrome/CHANGELOG.md b/pi-chrome/CHANGELOG.md new file mode 100644 index 0000000..bef8661 --- /dev/null +++ b/pi-chrome/CHANGELOG.md @@ -0,0 +1,306 @@ +# Changelog + +All notable user-facing changes to `pi-chrome`. + +## Unreleased — my-pi maintained build + +- **Context Mode snapshot handoff.** `chrome_snapshot`, `chrome_find`, and action results with `includeSnapshot=true` atomically save the full structured snapshot to a private workspace-local `.pi/chrome-context///snapshot.json` file and return only bounded metadata plus a path for `ctx_execute_file`. Each target keeps only its latest capture; interactions invalidate stale captures, and session shutdown/revoke/authorization expiry removes them. +- **Compact find results.** `chrome_find` keeps its top 20 ranked uid/coordinate matches inline while the full page structure stays in the session-local capture. +- **Complete authorization deactivation.** `chrome_find` and `chrome_inspect` are now included in the authoritative Chrome tool-name set so revoke/expiry deactivates all 21 registered tools. + +## 0.15.40 — 2026-06-22 + +- **Automation targets reuse the session tab group.** When `chrome_navigate` / implicit page actions create a new pi-chrome automation tab, it is now created in this session's existing tab-group window when possible and joins that same group, avoiding duplicate same-title `Pi Session: ...` groups. + +## 0.15.39 — 2026-06-07 + +- **Dedicated automation tab/window (no more hijacking your active tab).** When a chrome_* action runs without an explicit target, pi-chrome now opens and reuses a dedicated automation window it owns (falling back to a dedicated tab if a separate window can't be created) instead of navigating whatever tab you currently have open. Your existing tabs/windows are left untouched. Pass `targetId`/`urlIncludes`/`titleIncludes` to act on a specific existing tab. + - **Per session.** Ownership is scoped to the calling Pi session, so concurrent sessions sharing one companion extension each get their own automation window instead of fighting over a tab. + - **Survives `/reload` and Chrome service-worker restarts.** Ownership is tracked by id and mirrored to `chrome.storage.session`, so the window is reused rather than orphaned. + - **Guarded tab management.** `chrome_tab` `activate`/`close`/`group`/`ungroup` with no target now act on the session's automation tab if present, otherwise error — they no longer fall back to (or spawn a throwaway tab to touch) your active tab. + - **Safe, non-blocking cleanup.** The owned target is closed on `/chrome revoke` and on real session end (never on `/reload`); cleanup only ever closes the calling session's own window/tab — never user tabs/windows or another session's target — and is fire-and-forget so it never blocks `/quit`, `/reload`, or session end. +- **One session, one tab group.** `chrome_tab new` now reuses this session's existing Pi tab group even when another Chrome window is focused, creating the tab in that group's window because Chrome tab groups cannot span windows. The old `group:false` / `groupTitle:""` opt-out is ignored; Pi-created tabs are never intentionally left ungrouped, and a grouping failure closes the just-created tab before returning an error. + +## 0.15.38 — 2026-06-07 + +- **Overlay-safe click/fill fallbacks.** `chrome_click` and `chrome_fill` now fall back to DOM-dispatched click/value events when Chrome's debugger input path is blocked by another extension overlay (for example password-manager/autofill UI), unless `domFallback:false` is passed. + +## 0.15.37 — 2026-06-07 + +- **Hardened Chrome input targeting.** `chrome_click`/`chrome_fill`/related input paths now fail fast with resolved tab/CDP target metadata when debugger attach hits a stale or protected target, instead of surfacing bare `chrome-extension://` errors or hanging until the bridge timeout. +- **Internal timeouts and cleanup.** Companion extension commands, debugger attach, CDP commands, and script injection now have shorter internal timeouts with debugger cleanup, so stuck input dispatch returns actionable errors before the 30s bridge timeout. +- **Clear stale uid errors.** Snapshot uids that no longer map to live elements now report `snapshot uid ... is stale; refresh chrome_snapshot`. +- **`chrome_fill` fallback.** If real CDP input is blocked by another extension overlay (for example password-manager/autofill UI), `chrome_fill` falls back to setting the field value through the page DOM and dispatching `input`/`change` events, unless `domFallback:false` is passed. + +## 0.15.36 — 2026-06-03 + +- **Richer page observation.** `chrome_snapshot` now returns a concise, agent-friendly observation — structural layout/context, page hints, visible actions, form fields, a page map, query matches, and a diff of changes since the previous snapshot — instead of a raw JSON dump. New `mode` (`auto`/`interactive`/`forms`/`pageMap`/`text`/`changes`/`full`), `query`, and `maxTextChars` parameters let the agent zoom in instead of dumping the whole page. +- **New `chrome_find` tool.** Find elements, regions, or text by natural-language query (`'merge button'`, `'email error'`) and get ranked matches with stable uids and coordinates. Thin wrapper around `chrome_snapshot({ query })`. +- **New `chrome_inspect` tool.** Inspect one snapshot uid/selector deeply: nearby text, nearby actions, form context, ancestors, and a suggested click target. Falls back to a focused snapshot if the loaded extension predates `page.inspect`. +- **`includeSnapshot` now embeds the formatted snapshot.** `chrome_click`/`chrome_type`/`chrome_fill`/`chrome_key` with `includeSnapshot=true` append the fresh concise snapshot to the tool text so the agent can verify in one round trip. +- **Snapshot logic moved to a packaged `snapshot_injected.js`.** The MAIN-world snapshot/inspect implementation ships as an eval-free packaged script, shared `window.__PI_CHROME_STATE__` (same `el-` uid scheme) with the existing input helpers. + +## 0.15.34 — 2026-06-01 + +- **Every tab Pi uses now joins the session group.** Previously only `chrome_tab new`/`group` created/used the `Pi Session: ` group; tabs Pi drove via `page.*` actions (navigate, click, type, snapshot, screenshot, etc.) on the existing/active tab stayed ungrouped. Now any ungrouped tab Pi interacts with is pulled into this session's group, so the user can see exactly which tabs Pi is driving. Tabs already in a group (the user's or another session's) are left untouched. +- **Fixed punctuation being dropped by `chrome_type`/`chrome_key`.** Printable punctuation derived its `code`/virtual-keyCode from `charCodeAt()`, so `.` mapped to keyCode 46 (VK_DELETE), `-` to 45 (VK_INSERT), shifted symbols to wrong codes, and `code` was the raw char (`"."`) instead of the physical key (`"Period"`). Apps with keydown handlers (e.g. Gmail's filter address field) saw a Delete/Insert key and silently rejected the character. Both the CDP and DOM-event input paths now resolve keys through a proper US-keyboard layout map. + +## 0.15.33 — 2026-05-31 + +- **Background is now the default.** `chrome_*` tools run silently without focusing Chrome unless you opt in. Pass `background: false` per call, or run `/chrome background off`, to bring Chrome forward and watch. Tool/param descriptions and docs updated to match. + +## 0.15.32 — 2026-05-31 + +- **Session tab-group naming tweak.** Per-session groups are now titled `Pi Session: ` (added a colon separator). + +## 0.15.31 — 2026-05-31 + +Per-session tab groups. + +- **Each Pi session gets its own tab group.** Auto-grouped tabs are now named `Pi Session ` using the session's display name, falling back to the session id when unnamed. Multiple Pi sessions driving the same Chrome no longer share one group — each collects its tabs separately. Pass an explicit `groupTitle` to override, or `group:false` / `groupTitle:""` on `action=new` to opt out. + +## 0.15.30 — 2026-05-31 + +Tab grouping for `chrome_tab`. + +- **Pi-opened tabs auto-group.** `action=new` now drops every tab into a shared `Pi` tab group per window by default (created once, then reused), so agent tabs stay visually separated from your own. Opt out per call with `groupTitle:""` or `group:false`. +- **`chrome_tab` can group/ungroup tabs.** New `action=group` (and `action=ungroup`) plus `groupTitle`/`groupColor` params. Grouping reuses an existing same-title group in the window instead of spawning duplicates. Defaults: title `Pi`, color `blue`; color validated against Chrome's 9 group colors. Target an existing tab with `targetId`/`urlIncludes`/`titleIncludes`. +- **Tab listings include group info.** `formatTab` now reports `groupId` and a `group` record (`title`, `color`, `collapsed`, `windowId`, `piGroup`), and `chrome_tab list` prefixes grouped tabs with `[Group Title]`. +- Requires the new `tabGroups` extension permission — reload the companion extension after updating. + +## 0.15.29 — 2026-05-31 + +Strict-CSP support: `chrome_evaluate`, `chrome_snapshot`, `chrome_wait_for`, and `chrome_navigate initScript` now work on pages that block `unsafe-eval`. + +- **CDP-based evaluation bypasses page CSP.** `chrome_evaluate`/`chrome_snapshot` (and all snapshot-driven inspection) previously ran user code in the page MAIN world via the **Function constructor**, which is blocked by `script-src 'self'` without `'unsafe-eval'` — so they returned null/empty (or `EvalError`) on github.com and many bank/SaaS apps. They now evaluate through CDP `Runtime.evaluate`, a DevTools protocol command that is not subject to the page's Content-Security-Policy. Rich return values (undefined/function/symbol/bigint/Error markers, DOMRect expansion) and the expression/statement fallback are preserved. +- **`chrome_wait_for` polls via CDP.** The selector/expression polling loop moved from in-page `new Function()` to service-worker-side CDP evaluation, so waits work under strict CSP too. +- **`chrome_navigate initScript` injects via CDP.** Document-start init scripts now register with `Page.addScriptToEvaluateOnNewDocument` instead of `new Function()` on `webNavigation.onCommitted`, so seeding localStorage / stubbing `Date.now` works under strict CSP. +- **Tests.** Added a Node unit harness (`test-suite/unit/csp-eval.test.mjs`, run via `npm test`) validating the evaluate/execute/waitFor refactor, and an in-browser regression challenge (42 `strict-csp-evaluate`) that reads a JS-only secret under strict CSP. Updated challenge 39's notes and docs (FAQ, EXAMPLES, COMPARISON, primer) which previously stated eval/snapshot fail under strict CSP. + +## 0.15.28 — 2026-05-31 + +Low-risk reliability fixes from a long-session bug report. + +- **Bridge self-heals when the owning session dies.** A Pi session running in shared-client mode used to fail every `chrome_*` call with a bare `fetch failed` once the session that owned `127.0.0.1:17318` exited. The client now detects the unreachable owner, takes over the bridge port, and re-runs the command locally instead of staying stuck. +- **Actionable timeout messages.** A 30s timeout now says *why*: extension not polling (not installed/closed), polling but didn't pick up the command, or picked it up but never returned a result (long-running action / failed result post) — instead of one generic message. +- **`chrome_type` / `chrome_fill` DOM path no longer throws `pressKeyInPage is not defined`.** The helper is now included in the injected MAIN-world helper set and its callers await it. +- **`getBoundingClientRect()` / DOMRect now serialize in `chrome_evaluate`.** DOMRect-like values return `{x,y,width,height,top,right,bottom,left}` instead of `{}`. +- **Clearer stale-target errors.** A missing `targetId` now lists the current tabs and suggests re-targeting via `chrome_tab list` or `urlIncludes`/`titleIncludes`, instead of a bare "No matching Chrome tab found". +- **`pageMutated=false` no longer reads as failure.** Click/type/fill summaries explain it's a coarse heuristic that can miss real effects, and suggest verifying with `includeSnapshot`. + +## 0.15.26 — 2026-05-16 + +- **Documentation accuracy.** README, FAQ, examples, comparison, and test-suite docs now describe the 41-challenge suite, gate buckets, strict-CSP fallback, and current human-vs-extension limitations. +- **Published benchmark assets.** The npm package now includes `test-suite/` so the documented benchmark pages are available from installed packages, not only from the repository checkout. + +## 0.15.25 — 2026-05-16 + +- **Reload after older installs.** `/reload` now recovers from stale singleton state left by pi-chrome 0.15.19 and earlier instead of skipping the freshly loaded extension. +- **Test suite coverage.** Added gate buckets plus strict-CSP fallback, dynamic wait readiness, and explicit tab lifecycle challenges. + +## 0.15.24 — 2026-05-16 + +- **Unload Chrome tools on lock.** `chrome_*` tools now deactivate when `/chrome revoke` runs or timed authorization expires, keeping the prompt/tool list small after Chrome control locks again. + +## 0.15.23 — 2026-05-16 + +- **Attribution.** The 0.15.22 features below are pulled from Dani Bednarski's fork (`DaniBedz/pi-chrome`). Thank you, Dani. + +## 0.15.22 — 2026-05-16 + +Features in this release are pulled from Dani Bednarski's fork (`DaniBedz/pi-chrome`). Thank you, Dani. + +- **Earlier page-load capture.** Companion extension now injects console/network instrumentation at `document_start`, so initial React render errors and early API calls show up in `chrome_list_console_messages` / `chrome_list_network_requests`. +- **Quieter locked state.** Startup no longer shows a persistent Chrome bridge notification/status item before authorization; status bar appears only when Chrome control is authorized. +- **Lazy tool registration.** `chrome_*` tools and primer are registered only after `/chrome authorize`, reducing prompt/tool overhead while Chrome control is locked. + +## 0.15.21 — 2026-05-16 + +### Reverted 0.16.x and 0.17.x lines + +- Versions 0.16.0 through 0.17.2 were published to npm and subsequently unpublished. 0.17.3 was prepared locally but never published. The work introduced in those versions — mandatory pairing, signed-envelope auth, standalone bridge daemon, idempotent onboard, etc. — is reachable only via git tags (`v0.16.0` … `v0.17.3`) and is not in the current main branch. +- This release is **tree-equivalent to 0.15.20** with a version-only bump so future patch releases can ship cleanly. + +## 0.15.20 — 2026-05-15 + +- **Interruptible `chrome_*` tools.** All `chrome_*` tools now honor the agent harness `AbortSignal`, so pressing Esc aborts in-flight bridge calls (including the long-polling `chrome_wait_for`) immediately instead of waiting out the full `timeoutMs`. + +## 0.15.19 — 2026-05-14 + +- **Simpler package description.** README hero and npm/pi.dev description now use the same concise authorization-focused sentence. + +## 0.15.18 — 2026-05-14 + +- **Cleaner package description.** npm/pi.dev description now focuses on the existing Chrome profile and explicit authorization model, avoiding implementation details. + +## 0.15.17 — 2026-05-14 + +- **Docs accuracy pass.** Updated README, FAQ, comparison, contributing notes, and package metadata for the current real-input-only, terminal-authorized tool surface. +- **Input verification fix.** `includeSnapshot=true` now works for `chrome_click`, `chrome_type`, `chrome_fill`, and `chrome_key`, returning the Chrome-input result plus a fresh snapshot. + +## 0.15.16 — 2026-05-14 + +- **Visible `/chrome` loading state.** Bare `/chrome` and `/chrome status` now immediately say “Checking Chrome connection…” before probing the companion extension, so a slow Chrome bridge no longer looks like the command did nothing. + +## 0.15.15 — 2026-05-14 + +- **Terminal authorization restored.** `/chrome authorize` is back to terminal-based confirmation. Removed the browser-side Chrome consent page and companion-extension consent polling. + +## 0.15.14 — 2026-05-14 + +- **Clearer consent wait state.** After the Chrome approval page opens, Pi now says “Approve or deny the Chrome approval page to continue” instead of looking stuck at the launch step. + +## 0.15.13 — 2026-05-14 + +- **Fix Chrome-side consent hang.** `/chrome authorize` now launches the browser consent page as a short command, then polls for the decision. This avoids holding one long extension command open while the user reads/clicks the page, which could leave Pi stuck at “Opening Chrome approval page…”. + +## 0.15.12 — 2026-05-14 + +- **Docs accuracy.** Clarified that the bundled Chrome extension currently polls `127.0.0.1:17318`; custom bridge ports are not supported without editing/reloading the extension source. Also softened the unpacked-extension rationale to avoid overstating Web Store limitations and fixed stale strict-CSP guidance for `chrome_evaluate`. + +## 0.15.11 — 2026-05-14 + +- **README cleanup.** Removed the Playwright/CDP/Selenium comparison table and low-signal Composes with / Contributing sections from the package page because they are noisy and easy to drift. + +## 0.15.10 — 2026-05-14 + +- **Browser-side Chrome consent.** `/chrome authorize` now opens a Pi Chrome Connector approval page inside Chrome showing duration, workspace, process id, and extension/package versions. Chrome control unlocks only after the user approves there; denying, closing the tab, or timeout leaves control locked. +- **README cleanup.** Removed npm/download/license shield badges from the package page because they are noisy and easy to drift. + +## 0.15.9 — 2026-05-14 + +- **Tighter `/chrome` menu.** Removed the redundant “Connection status” item from the interactive `/chrome` menu because connection/auth/background are already shown in the menu header. `/chrome status` remains available as a slash command. + +## 0.15.8 — 2026-05-14 + +- **Simpler `/chrome` submenus.** Authorize menu now offers 15 minutes, 30 minutes, indefinite, and custom minutes only. Background menu now offers only foreground/background. Esc from a submenu returns to the main `/chrome` menu. + +## 0.15.7 — 2026-05-14 + +- **Grouped `/chrome` menu.** Bare `/chrome` now opens a status dashboard with grouped actions: authorize, lock, status, doctor, background/watch mode, and onboard. Authorize/background open submenus instead of showing one flat command list. + +## 0.15.6 — 2026-05-14 + +- **Bare `/chrome` is now a command menu.** Running `/chrome` shows interactive options for every `/chrome ...` command, including authorize/revoke/status/doctor/onboard/background variants. + +## 0.15.5 — 2026-05-14 + +- **Chrome control authorization.** `chrome_*` tools are locked until the user runs `/chrome authorize` in the current Pi session. Grants can be one command, 15 minutes, 1 hour, or the session; `/chrome revoke` locks control again and `/chrome status` shows auth state. +- **Browser-origin bridge hardening.** The loopback bridge no longer sends wildcard CORS headers. `/command` accepts only local-process requests, while extension polling/result endpoints reject non-extension browser origins, blocking ordinary web pages from driving or draining the bridge through `127.0.0.1`. + +## 0.15.4 — 2026-05-14 + +- **Quiet renamed to background.** Public UX now uses `/chrome background [on|off|toggle|status]` and docs/status say “run in background” instead of “quiet mode”. + +## 0.15.3 — 2026-05-14 + +- **Chrome real input only.** Public trusted/synthetic mode controls were removed. Interactive tools now always use Chrome's real input layer; `/chrome clicks` and public `trusted` parameters are gone. + +## 0.15.2 — 2026-05-13 + +- **Recipe prompts rewritten in user-language.** Earlier recipes leaked tool names into the `You:` prompts ("Use `chrome_tab list` to find my GitHub notifications tab…"), implying users need to know the tool catalog before they can ask anything. Prompts now read as natural intent; the agent trace below each one still shows the `chrome_*` primitives the agent picked. Affects the 30-second try-this block, all 3 hero recipes (PR triage / Linear standup / Bug repro), and 3 of the 6 collapsed recipes (auth-only data pull, network forensics, file upload). + +## 0.15.1 — 2026-05-13 + +- **Architecture diagram now renders on pi.dev.** Replaced Unicode box-drawing characters (`┌─┐│└┘┬▼`) with plain ASCII (`+ - | v`). Pi.dev's monospace font was dropping the horizontal `─` glyphs, leaving the diagram as floating vertical bars. ASCII renders everywhere. +- **`author` switched to object form.** Was `"tianrendong (Earendil Inc.)"` — npm's author-string spec parses `(parens)` as the URL slot, so `"Earendil Inc."` ended up in `author.url`. Now `{ "name": "tianrendong", "company": "Earendil Inc." }`. + +## 0.15.0 — 2026-05-13 + +- **README rewrite — top-3 recipes as terminal mockups.** PR triage, Linear standup, and Bug-repro-with-evidence each get a copy-pasteable prompt → tool trace → result block modeled on the hero example. The other six recipes (form auto-fill, admin cross-check, visual diff, auth-only data pull, network forensics, file upload) collapsed into a `
` block so the section sells before it catalogs. +- **Comparison table rewritten.** Dropped the all-✅ "Works on strict-CSP pages" row (zero signal). New table leads with "Time from `pi install` → first useful action on your real account" (~60s vs. hours) and "Survives MFA / SSO without code" (✅ already logged in). Multi-session row reframed as the bolded "Multiple agents drive the same Chrome at once". Footnote ² rewritten to highlight mode-aware scoring + open invitation for competing tools to PR their scores. +- **Section reorder: sells before catalogs.** New flow: hero → 60-second install → 30-second try-this → killer recipes → comparison → honest results → tool catalog → click/watch modes (with Diagnostics folded in) → architecture → benchmark suite → security model & why unpacked (combined) → composes-with → roadmap → contributing → license. Hero blockquote now precedes shields badges so pi.dev no longer scrapes a broken-image badge as the description. Package `description` shortened to 255 chars so pi.dev hero stops truncating mid-word. `author` set to `"tianrendong (Earendil Inc.)"`. + +## 0.14.9 + +- Primer (agent system prompt) now teaches the **trusted-mode escape hatch** explicitly. Previously the bridge would hit a CSP-locked page (github.com, banks, many SaaS apps), `chrome_evaluate`/`chrome_snapshot` would throw `EvalError: 'unsafe-eval' is not an allowed source of script`, and the agent would conclude *"bridge can't drive this page"* and ask the user for a fallback. New primer makes three things self-discoverable: (1) `trusted: true` on click/type/key/fill/hover/drag/scroll dispatches through chrome.debugger / CDP and bypasses page CSP entirely, (2) the recipe for strict-CSP pages is `chrome_screenshot` + trusted input at viewport coordinates, (3) when synthetic input produces no `pageMutated` or you see a CSP/eval error, **escalate to `trusted: true` yourself instead of asking the user**. Also corrects the old claim that `chrome_evaluate` works without `'unsafe-eval'` (it does not — Function constructor is gated by `script-src`). +- Add `scripts/sync-manifest-version.js` wired to npm's `version` + `prepublishOnly` lifecycle hooks. Bumping the package version with `npm version ` now auto-syncs `extensions/chrome-profile-bridge/browser-extension/manifest.json` and stages it into the version commit — kills the recurring drift class (cf. 0.14.4, 0.14.8, this fix). + +## 0.14.8 + +- Repo moved to its own home: https://github.com/tianrendong/pi-chrome. No code changes; updated `repository`, `homepage`, and `bugs` URLs in `package.json`. + +## 0.14.7 + +- Replace "30+ challenges" hand-wave in README + COMPARISON.md with the accurate framing from chrome-benchmark: **38 primitive challenges + 4 hermetic BrowserGym-style long-horizon tasks**, scored by **expected-outcome-by-mode** (not raw PASS count). Explains why a synthetic-events tool isn't supposed to satisfy a clipboard user-activation gate — matching that expectation is the pass. + +## 0.14.6 + +- Fix Browser Use license in `docs/COMPARISON.md`: MIT (not Apache-2.0). Confirmed against upstream LICENSE on GitHub. + +## 0.14.5 + +- `docs/COMPARISON.md` rewritten with a three-axis landscape (drivers / agent frameworks / cloud providers). Adds Browser Use, Stagehand, Skyvern, Magnitude, Alumnium, OpenAI Operator, Project Mariner, Surfer 2, Anthropic Computer Use, Browserbase, Steel.dev, Hyperbrowser, Anchor, Browserless. Adds Interop section, public-benchmark cheat sheet (WebArena, WorkArena++, BrowseComp, Mind2Web 2, WebChoreArena, MiniWoB++, BrowserGym). +- README gains a one-liner pointing at the new three-axis framing. +- Sourced from `benchmark-search` session research. No code changes. + +## 0.14.4 + +- Sync `manifest.json` version to match `package.json` (0.14.3 shipped with stale manifest, would trigger spurious `/chrome doctor` drift warnings). No code or behavior changes. + +## 0.14.3 + +- Documentation & discoverability overhaul. +- New README: hero, alternatives comparison table, 20-tool reference grouped by job, killer recipes, architecture diagram, honest-results explainer, benchmark suite plug. +- `docs/COMPARISON.md` — deep comparison vs Playwright / Puppeteer / Stagehand / browser-use / Selenium. +- `docs/EXAMPLES.md` — ready-to-paste agent prompts (PR triage, Linear standup, bug repro, network forensics, multi-tab admin cross-check, etc.). +- `docs/FAQ.md` — covers Brave/Arc, incognito, detection, banner, multi-session, CSP, file uploads, common envelope causes. +- `CHANGELOG.md`, `CONTRIBUTING.md`, `SECURITY.md`, `LICENSE` added. +- `package.json`: `homepage`, `repository`, `bugs`, expanded keywords for npm search. +- No code changes to the tools or extension. + +## 0.14.2 + +- Recover from foreign-extension input overlays so input tools don't get hijacked by other Chrome extensions. + +## 0.14.1 + +- Harder `attachDebugger` retry path so trusted clicks survive transient Chrome debugger contention. +- New `trusted.debug` diagnostic surface. + +## 0.14.0 + +- Bare `/chrome` opens a `SettingsList` dialog. `space` cycles values, `enter` saves. + +## 0.13.0 + +- Flattened `/chrome` tree. Cycle-on-pick for `clicks` / `quiet`. Status header at the top of the picker. + +## 0.12.1 + +- Fixed anti-automation regressions across 7 benchmark challenges (`07/13/21/27/28`). + +## 0.12.0 + +- Unified all slash commands under `/chrome` (`/chrome doctor`, `/chrome onboard`, `/chrome clicks`, `/chrome quiet`). + +## 0.11.x + +- Plain-English audit pass across `/chrome doctor`, `/chrome onboard`, `/chrome quiet`, README. +- `chrome_tap` (real CDP touch events). +- Smoother `trustedScroll`; CDP debugger auto-recovers from detach. +- Smart-auto trusted mode default. Extension renamed to **Pi Chrome Connector**. +- Per-event scroll delta cap so IntersectionObserver thresholds land naturally. + +## 0.10.x + +- Trusted-input mode via `chrome.debugger` (CDP) — opt-in, indistinguishable-from-human clicks/keys. +- `chrome_key` modifiers (Cmd+V, Ctrl+Shift+Tab, etc.) for trusted chords. +- Interactive `/chrome-trusted` picker (later folded into `/chrome`). + +## 0.9.x + +- Humanized synthetic input (pointer paths, key cadence variance). +- Anti-automation benchmark test suite landed in `test-suite/`. + +## 0.8.0 + +- `chrome_evaluate` no longer returns `null` for valid expressions — dedicated MAIN-world `evaluateInTab` pipeline with statement-mode fallback and tagged envelope for `undefined`/errors/symbols/bigints. +- Truthful action result envelopes: `isTrusted`, `defaultPrevented`, `elementVisible`, `occludedBy`, `valueMatches`, `pageMutated`. +- Extended `/chrome doctor`: bridge mode/URL, extension version drift, MAIN-world helper injection, `navigator.webdriver` fingerprint, CDP availability probe. +- Removed misleading `returnByValue` param. Implemented `chrome_screenshot.fullPage` via tile stitching. +- Autoplay-gate heuristic for `chrome_click`. + +## 0.7.0 + +- Initial public `pi-chrome` release. Companion Chrome extension + local bridge + first tool set. diff --git a/pi-chrome/CONTRIBUTING.md b/pi-chrome/CONTRIBUTING.md new file mode 100644 index 0000000..3b8ff0f --- /dev/null +++ b/pi-chrome/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to pi-chrome + +Thanks for considering a contribution. pi-chrome aims to be the **de-facto browser-control toolkit for Pi agents** — that means a few non-negotiables. + +## Non-negotiables + +1. **No re-login.** Every change must keep working against the user's already-signed-in Chrome profile. Anything that requires a fresh profile or extra auth steps is out of scope. +2. **Verifiable action results.** Input tools must return structured details and support `includeSnapshot` where verification matters. Agents need enough evidence to avoid blind retries. +3. **Chrome real input.** Interactive controls use Chrome's input layer through `chrome.debugger`; do not re-expose synthetic/untrusted input as public UX. +4. **Benchmarks gate features.** Add a page in `test-suite/` that fails before your change and passes after. We accept PRs faster when there's a green/red verdict to point at. + +## Local dev + +```bash +# Link from a checkout +pi install ./pi-chrome + +# Run the benchmark dashboard +cd test-suite +python3 -m http.server 8765 +# open http://127.0.0.1:8765/ in the Chrome window pi-chrome controls +``` + +## Adding a new tool + +1. Register in `extensions/chrome-profile-bridge/index.ts` (the `register*Tool` calls near line 840+). +2. Implement the handler in `extensions/chrome-profile-bridge/browser-extension/service_worker.js`. +3. Return structured details and support `includeSnapshot` for user-visible state changes when relevant. +4. Add a benchmark page under `test-suite/challenges/` and a manifest entry. +5. Update `README.md` "What an agent gets" table. +6. Add a `CHANGELOG.md` entry. + +## Filing a bug + +Include: + +- `/chrome doctor` output +- `pi-chrome` version + extension version (the `doctor` output prints both) +- The exact tool call + the result envelope you got +- Page URL or a minimal repro page in `test-suite/` + +## Releasing + +- Bump `package.json` version. +- Move `CHANGELOG.md` notes from the working section to the new version header. +- `npm publish --access public`. + +## Code of conduct + +Be kind, be precise, ship things. PRs that break the "no re-login" promise will be closed with a note explaining which non-negotiable they hit. diff --git a/pi-chrome/LICENSE b/pi-chrome/LICENSE new file mode 100644 index 0000000..a975012 --- /dev/null +++ b/pi-chrome/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 pi-chrome contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pi-chrome/README.md b/pi-chrome/README.md new file mode 100644 index 0000000..3680220 --- /dev/null +++ b/pi-chrome/README.md @@ -0,0 +1,155 @@ +# pi-chrome + +> Let [Pi](https://pi.dev) use your existing signed-in Chrome profile after explicit authorization. + +**MIT · 0 runtime deps · loopback-only bridge (`127.0.0.1:17318`) · inspectable unpacked Chrome extension.** Review [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension) before loading. Verify setup with `/chrome doctor`. + +```text +You: "Find my open GitHub PR tab, summarize review state, and screenshot failing CI." +Agent: chrome_tab(list) → chrome_snapshot(...) → ctx_execute_file(capture) → chrome_screenshot(...) + ✓ 3 reviewers, 1 change requested, CI red on iOS. Saved → .pi/chrome-screenshots/ci.png +You: [keeps coding — agent never asked you to log in] +``` + +`pi-chrome` runs through a small Chrome extension inside the Chrome profile **you already use** — including sites where you're already signed in. Agents can inspect or control Chrome only after you run `/chrome authorize` in current Pi session. + +--- + +## Install + +```bash +pi install npm:pi-chrome +``` + +In Pi: + +```text +/chrome onboard +``` + +This opens `chrome://extensions` and copies bundled extension path. In Chrome Extensions: + +1. Turn on **Developer mode**. +2. Click **Load unpacked**. +3. Open path field with **Cmd+Shift+G** on macOS or **Ctrl+L** on Windows/Linux. +4. Paste copied path. +5. Press Enter. + +Reload Pi so installed package loads: + +```text +/reload +``` + +Check bridge: + +```text +/chrome doctor +``` + +You should see: + +```text +✓ Chrome is connected (...) +``` + +Authorize current session: + +```text +/chrome authorize +/chrome doctor +``` + +Second doctor run should show all checks passing. + +--- + +## What it can do + +- Read and summarize pages you're already signed into. +- Click, type, fill forms, scroll, drag, tap, and upload files. +- Capture screenshots for bugs, PRs, and demos. +- Inspect console logs and captured `fetch`/`XMLHttpRequest` responses. +- Manage tabs without taking over your active window. + +Tool parameters and gotchas are documented inline in Pi. + +### Session-local snapshots and Context Mode + +In this maintained build, `chrome_snapshot`, `chrome_find`, and actions with `includeSnapshot=true` keep the full structured page snapshot out of the normal tool result. They atomically save only the latest capture for each target under: + +```text +.pi/chrome-context///snapshot.json +``` + +The tool returns a workspace-relative path plus bounded metadata. Analyze that path immediately with `ctx_execute_file` and print only the uids, text, or state needed for the next browser action. Do **not** use `read` or `cat` on the capture, because that would put the raw page back into model context. + +Captures are deliberately ephemeral: a successful page interaction invalidates the old target capture, and session shutdown, `/chrome revoke`, or authorization expiry removes the session files. `chrome_find` still returns up to 20 compact ranked matches for direct use while storing the full capture privately. + +--- + +## Safety model + +Chrome control is locked by default. Authorize per Pi session: + +```text +/chrome authorize # 15 minutes +/chrome authorize 30m # custom duration +/chrome authorize indefinite +/chrome revoke # lock again +/chrome status +``` + +Safety properties: + +- Extension runs in your real Chrome profile and has broad tab/scripting permissions. Install only from trusted package source. +- Pi side binds to `127.0.0.1:17318` only; no default network exposure. +- Bridge rejects browser-origin command requests, so ordinary web pages cannot drive it through CORS. +- Each Pi session gets its own automation target; user tabs/windows are not closed by cleanup. +- `/chrome revoke` closes only calling session's automation target. + +Security details: [`SECURITY.md`](./SECURITY.md). Architecture details: [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md). + +--- + +## Commands + +```text +/chrome onboard # guided setup +/chrome doctor # connectivity + version + eval checks +/chrome status # connection + auth + background state +/chrome authorize [duration] +/chrome revoke +/chrome background on # default: don't steal focus +/chrome background off # foreground/watch mode +/chrome background status +``` + +If loaded extension is older than installed `pi-chrome`, `/chrome doctor` tells you to reload it from `chrome://extensions`. + +--- + +## Limits + +`pi-chrome` works best on web-page workflows exposed through DOM, screenshots, tabs, network, console, and Chrome input. It is not full OS automation. + +Current limits include native Chrome/OS surfaces, print/save dialogs, permission bubbles, password-manager prompts, cross-origin iframe DOM access, CAPTCHA/bot challenges, passkeys/security keys/biometrics, rich multitouch/pinch/stylus gestures, and arbitrary desktop apps. + +For strict-CSP pages, use screenshots + coordinate input when snapshot/evaluate paths are blocked. + +--- + +## Docs + +- Examples: [`docs/EXAMPLES.md`](./docs/EXAMPLES.md) +- FAQ: [`docs/FAQ.md`](./docs/FAQ.md) +- Comparison: [`docs/COMPARISON.md`](./docs/COMPARISON.md) +- Security: [`SECURITY.md`](./SECURITY.md) +- Benchmark suite: [`test-suite/README.md`](./test-suite/README.md) +- Architecture: [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) + +--- + +## License + +MIT. See [LICENSE](./LICENSE). diff --git a/pi-chrome/SECURITY.md b/pi-chrome/SECURITY.md new file mode 100644 index 0000000..ffc2c06 --- /dev/null +++ b/pi-chrome/SECURITY.md @@ -0,0 +1,38 @@ +# Security policy + +## Reporting a vulnerability + +Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong/pi-chrome/issues, or contact the maintainer directly if the issue is sensitive. Please do **not** include exploit details in a public issue without coordinating first. + +## Threat model + +`pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against: + +- Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.) +- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account. + +`pi-chrome` **is** designed to: + +- Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`). +- Surface every action with an honest result envelope so the agent can't silently do the wrong thing. +- Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session. +- Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome. + +## The companion extension + +The Chrome extension under `extensions/chrome-profile-bridge/browser-extension/` runs with broad permissions: `tabs`, `scripting`, `debugger`, `webNavigation`, etc. **Only install it from a package source you trust.** Read the source before loading. Pin a known-good commit if you're security-sensitive. + +## Defaults + +- Loopback bridge only. No remote port. No telemetry. +- Chrome real input layer for interactive controls. +- Chrome control locked by default; `/chrome authorize` unlocks current Pi session after terminal confirmation, `/chrome revoke` locks it again. +- Run-in-background optional; tab/window focus is observable by default (the user can see Pi acting). + +## Custom ports + +The bundled Chrome extension currently polls `127.0.0.1:17318`. Custom bridge ports are not supported without editing the extension source and reloading it. + +## Supported versions + +The latest minor on npm is supported. Security patches will be released as soon as practical. diff --git a/pi-chrome/UPSTREAM.md b/pi-chrome/UPSTREAM.md new file mode 100644 index 0000000..a9b8587 --- /dev/null +++ b/pi-chrome/UPSTREAM.md @@ -0,0 +1,23 @@ +# Upstream source + +- Official repository: +- Imported npm version: `pi-chrome@0.15.46` +- Imported commit: `017ff4b9a639f0b8b213e58a3f30613fc38edcc8` +- Commit date: `2026-07-07T01:30:54-04:00` +- Import basis: the `gitHead` recorded in the official npm metadata for `pi-chrome@0.15.46`; no upstream tag points at this commit. +- Initial local import: 2026-08-28 + +The tracked upstream source, tests, documentation, scripts, and license were imported from the official Git repository. Nested `.git`, dependency directories, coverage, and build output are not retained. + +## Local maintenance + +This directory is maintained directly by the `my-pi` bundle and is not a submodule. The root package loads `extensions/chrome-profile-bridge/index.ts` from this directory and depends on it through `file:./pi-chrome`; the npm prebuilt extension entry must not be loaded in parallel. + +Local changes after the import: + +- Chrome snapshots are written atomically to owner-only, session-local files under the active project's `.pi/chrome-context/` directory instead of returning raw page structure to model context. +- Each session/target keeps only its latest snapshot; browser interactions invalidate stale captures and session shutdown removes the session directory. +- `chrome_find` returns compact ranked matches while keeping the complete structured snapshot in the session-local capture for optional `ctx_execute_file` analysis. +- `includeSnapshot=true` on click/type/fill/key saves the post-action snapshot and returns its capture path rather than embedding the snapshot in the tool result. + +When syncing upstream, compare against the imported commit and port changes selectively. Do not overwrite these local lifecycle, privacy, and Context Mode integrations wholesale. diff --git a/pi-chrome/docs/ARCHITECTURE.md b/pi-chrome/docs/ARCHITECTURE.md new file mode 100644 index 0000000..4b2a561 --- /dev/null +++ b/pi-chrome/docs/ARCHITECTURE.md @@ -0,0 +1,74 @@ +# pi-chrome architecture + +`pi-chrome` connects Pi to your existing Chrome profile through a local-only bridge and an unpacked Chrome extension. + +```text + +----------------------+ +--------------------------+ + | Pi agent (terminal) | -- 127.0.0.1:17318 ->| Chrome extension | + | chrome_* tools | | (your real profile) | + +-----------+----------+ +-------------+------------+ + | same machine | + v v + Other Pi sessions Tabs you already have open + share same bridge (GitHub, Linear, Stripe, etc.) +``` + +## Components + +- **Pi extension** — exposes `chrome_*` tools and `/chrome` commands inside Pi. +- **Loopback bridge** — listens on `127.0.0.1:17318`; no external network bind by default. +- **Chrome companion extension** — loaded unpacked into your real Chrome profile. +- **Chrome debugger / CDP** — drives input, screenshots, network/console observation, and evaluation. + +## Session model + +Multiple Pi sessions can use same Chrome companion extension. First session opens local bridge; later sessions detect it and pipe commands through. + +Each Pi session owns its own automation target: + +- First chrome action without explicit target opens dedicated automation window. +- If separate window cannot be created, pi-chrome falls back to dedicated tab. +- Target survives `/reload` and Chrome service-worker restarts. +- Ownership is tracked by id and mirrored to `chrome.storage.session`. +- Cleanup closes only calling session's own target, never user tabs/windows or other sessions' targets. + +To point pi-chrome at an existing tab, pass `targetId`, `urlIncludes`, or `titleIncludes`. + +## Tab management guards + +`chrome_tab` management actions are guarded: + +- `activate`, `close`, `group`, and `ungroup` without explicit target act on session automation tab if it exists. +- If no automation tab exists, operation errors instead of touching your active tab. + +## Background mode + +By default, chrome calls run in background so Chrome does not steal focus. + +```text +/chrome background on # background mode +/chrome background off # foreground/watch mode +``` + +Per-call `background: false` brings Chrome forward for that action. Per-call `background: true` forces background. + +## Authorization + +Bridge connection alone is not enough. Chrome control stays locked until current Pi session runs: + +```text +/chrome authorize +``` + +Authorization expires after configured duration, on `/chrome revoke`, or when Pi exits. + +## Unpacked extension choice + +`pi-chrome` ships browser extension source as an unpacked folder on purpose: + +- easy to inspect before loading +- no Web Store release delay +- MIT-licensed source in repo +- `/chrome doctor` can compare loaded extension version against installed package + +Loaded extension has broad tab/scripting permissions inside profile where it is installed. Install only from trusted package source. diff --git a/pi-chrome/docs/COMPARISON.md b/pi-chrome/docs/COMPARISON.md new file mode 100644 index 0000000..16cd81a --- /dev/null +++ b/pi-chrome/docs/COMPARISON.md @@ -0,0 +1,166 @@ +# pi-chrome vs. the rest of the browser-automation landscape + +This is the honest "which tool when" page. The browser-automation space has three different layers and people often compare across them — let's not do that. + +We benchmark in public — see [`../test-suite/`](../test-suite). Where exact scores matter (WebVoyager, WorkArena++, BrowseComp, Mind2Web 2), check the live leaderboards; they shift monthly. + +--- + +## TL;DR + +| You are… | Use… | +| -------------------------------------------------------------- | ------------------------------- | +| A Pi agent operator who wants the agent to use **your real Chrome** (logged-in tabs, cookies, extensions) | **pi-chrome** | +| Building a Pi/LLM agent and want low-level browser primitives | **pi-chrome** | +| Writing deterministic end-to-end tests in CI | Playwright / Cypress | +| Building a hosted scraping/agent fleet on isolated profiles | Playwright / Puppeteer + Browserbase / Steel | +| Want a turnkey "natural-language agent" with built-in loop | Browser Use / Stagehand / Skyvern | +| Want a hosted, vendor-managed agent | OpenAI Operator / Project Mariner / Surfer | +| Debugging your own app from your editor without leaving your real session | **pi-chrome** | + +`pi-chrome` is **primitives**, not an opinionated agent loop. Think of it as **"Playwright for the Chrome you're already signed into"** — and pluggable under any agent framework above the line. + +--- + +## The three axes you should compare on + +| Axis | Examples | What it gives you | +| --------------------------------- | --------------------------------------------------------------------- | ----------------------------- | +| **1. Driver / transport** | Playwright, Puppeteer, Selenium, CDP raw, `puppeteer-stealth`, **pi-chrome** | low-level tools (click, type, navigate) | +| **2. Agent framework** | Browser Use, Stagehand, Skyvern, Magnitude, Alumnium, LangGraph-with-Playwright, Operator, Mariner, Surfer | LLM loop, planning, NL API | +| **3. Cloud browser provider** | Browserbase, Steel.dev, Hyperbrowser, Anchor, Browserless | managed Chromes, sessions, quotas | + +`pi-chrome` is **axis 1**. You can wrap it with any axis-2 agent framework, or run it directly from a Pi agent's `chrome_*` tool calls. It explicitly does NOT compete with axis 3 — it runs locally inside *your* Chrome. + +--- + +## Axis 1 — drivers (where pi-chrome lives) + +| Tool | Transport | Profile | Browser input | Banner when controlling | Default detectable as bot | +| --------------------------------- | ------------------------------------------ | ---------------------------------- | -------------------- | ----------------------------------- | ------------------------- | +| Playwright | CDP (own driver) | throwaway by default | always | always ("controlled by test software") | yes (webdriver flag, automation flags) | +| Puppeteer | CDP | throwaway by default | always | always | yes | +| Selenium | WebDriver / BiDi | throwaway | partial (BiDi improves) | always | most detectable | +| puppeteer-stealth / playwright-extra | CDP + patches | throwaway | always | always | medium (patches flags) | +| Raw CDP | direct devtools protocol | either (needs `--remote-debugging-port`) | always | always | yes | +| **pi-chrome** | **Chrome extension bridge → local loopback** | **your real Chrome profile, signed-in cookies, extensions, history** | **always for input tools** | **while Chrome input is attached** | **low (real profile + Chrome input)¹** | + +¹ pi-chrome uses `chrome.debugger` for browser input and shows Chrome's banner like other CDP-based tools. The [`test-suite/`](../test-suite) grades browser-control behavior against common detection signals. + +### What makes pi-chrome different on this axis + +1. **Profile attach, not driver launch.** Every other driver fights cookie persistence, login walls, MFA, and extension state. pi-chrome inherits all of it because it *is* your Chrome. +2. **Chrome input against your real profile.** Interactive tools use CDP input for reliability while still controlling the Chrome profile you already use. +3. **Extension bridge transport.** No `--remote-debugging-port`, no throwaway Chromium. Survives Chrome auto-updates. Works alongside your normal Chrome usage. +4. **Structured action results.** Input tools return target coordinates/tags and can include a fresh snapshot (`includeSnapshot`) so agents can verify state instead of blindly retrying. +5. **Multi-session shared bridge.** Planner + worker + audit Pi sessions all drive the same Chrome concurrently. +6. **Stable element uids.** `chrome_snapshot` returns deterministic uids you can pass to subsequent actions — similar to BrowserGym's `bid`, but built into the snapshot tool itself. + +--- + +## Axis 2 — agent frameworks (built on top of axis 1) + +These wrap a driver with an LLM loop. They are **higher-level than pi-chrome** and **complementary**, not competitors. + +| Framework | Driver underneath | Approach | Open source | +| ------------------------ | ------------------------------ | --------------------------------------------------------------------------------------------- | --------------- | +| **Browser Use** | Playwright | DOM + a11y tree → LLM → action JSON. Open-source leader; widely cited on WebVoyager. | MIT (Python) | +| **Stagehand** (Browserbase) | Playwright | Natural-language `.act()` / `.observe()` / `.extract()`; deterministic + AI mix. | MIT (TypeScript)| +| **Skyvern** | Playwright + own DOM model | Vision-first + DOM; YAML workflows for form/workflow automation. | AGPL (Python) | +| **Magnitude** | Playwright | NL test authoring; QA-focused. | open | +| **Alumnium** | Selenium / Playwright | Test-author NL → agent. QA-focused. | open | +| **LangGraph / AutoGen + Playwright** | Playwright | Generic agent graph + browser tools. | open | +| **OpenAI Operator** | proprietary | OpenAI's own VLM + browser; ChatGPT-integrated. | closed, hosted | +| **Project Mariner** (Google) | proprietary Chrome integration | Google's own VLM Chrome experiment. | closed | +| **Surfer 2 / Surfer-H** (H Company) | proprietary | Hosted proprietary agent stack. | closed, hosted | +| **Anthropic Computer Use** | OS-level screenshots + mouse/keyboard | Broader than browser; OS-level events. | closed (API) | + +**Why pi-chrome is not on this list:** it's intentionally **not an agent**. There's no LLM loop, no `.act("click the blue button")`. Pi handles the loop; pi-chrome provides the primitives. This means: + +- You can use pi-chrome under Browser Use, Stagehand, LangGraph, or any other agent framework (see [Interop](#interop)). +- You don't pay for an opinion you don't want. +- Agent improvements compound across all your workflows because the primitives stay stable. + +--- + +## Axis 3 — cloud browser providers (orthogonal) + +| Provider | What it sells | +| ----------------- | -------------------------------------------------------- | +| **Browserbase** | Managed browsers; pairs with Stagehand. | +| **Steel.dev** | Managed browsers + public agent leaderboards. | +| **Hyperbrowser** | Managed browsers, session APIs. | +| **Anchor Browser**| Managed browsers. | +| **Browserless** | Managed Chrome, scraping focus. | + +**pi-chrome doesn't compete here.** It runs locally in *your* Chrome. The right framing if someone asks: *"no cloud cost, no session-handoff, no rate limits — and the agent runs against your real logged-in state."* If you need fleets of isolated Chromes in CI, you want one of these. + +--- + +## Interop + +`pi-chrome` exposes tools that any Pi agent can call. If you want to use it from outside Pi: + +1. The local bridge speaks HTTP JSON over `127.0.0.1:17318` (default). The API is internal; use the Pi tool surface unless you are building an adapter. +2. Tool surface mirrors Playwright closely (click/type/navigate/snapshot/screenshot/evaluate/wait_for) so adapter code is short. +3. `includeSnapshot` on input tools lets agent harnesses verify state after actions. + +If you want a first-class pi-chrome adapter for Browser Use / Stagehand / LangGraph, file an issue with your use case. + +--- + +## "But Playwright has `storageState` / Puppeteer has user-data-dir" + +Yes — you can export cookies and replay them, or point Playwright at your existing profile directory. In practice for agent workflows that breaks down fast: + +1. **OAuth + SSO** providers (Okta, Google, GitHub) often pin sessions to TLS fingerprints, device IDs, and browser-extension state that doesn't survive replay or a parallel Chrome instance. +2. **MFA** tokens expire mid-run. +3. **Internal admin tools** hard-pin to your real device. +4. **Pointing Playwright at your real `user-data-dir`** requires closing your normal Chrome (Chrome won't share the profile lock). pi-chrome doesn't fight you for the profile because it lives *inside* it. +5. **Watching the agent work** in your real window is a different UX than a hidden parallel Chrome. Demos, pair-driving, and confidence-building all want axis-1-with-attach. + +--- + +## "Is this safer than CDP?" + +Different security boundary, not strictly safer. + +- **CDP-based tools** require `chrome --remote-debugging-port=...`. That port is unauthenticated and exposes the whole browser to any local process. Easy to misconfigure. +- **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session. **Only install the bundled extension if you trust the source you got the npm package from.** + +If your threat model excludes extensions with broad permissions, neither approach is a fit — you want a sandboxed CI runner. + +--- + +## Public benchmarks worth knowing (for axis 2 / axis 3 comparison) + +Pi-chrome itself ships a benchmark suite ([`../test-suite/`](../test-suite)) of **42 primitive challenges** plus **4 hermetic BrowserGym-style long-horizon tasks** covering trusted input, pointer humanization, keyboard fidelity, drag/drop, Shadow DOM, iframes, file uploads, strict-CSP screenshot fallback and CDP eval/snapshot bypass, dynamic waits, tab lifecycle, network observability, fingerprint leaks, and agent-safety honeypots. Scoring tracks expected outcomes per challenge instead of raw PASS count, with `core`, `conditional`, and `quality` gate buckets. That's **driver-level** grading. + +For **agent-level** comparison (axis 2), the public benchmarks worth citing: + +| Benchmark | What it measures | Notes | +| ---------------- | ------------------------------------------------- | ------------------------------------------------ | +| **WebArena** (CMU) | Hermetic, programmatic graders | Gold standard for reproducibility. | +| **WorkArena++** (ServiceNow) | Enterprise SaaS workflows | Hardest realistic benchmark; <5% frontier. | +| **BrowseComp** (OpenAI) | Hard info-retrieval | Not saturated. | +| **Mind2Web 2** (NeurIPS '25) | Long-horizon, rubric-tree judge | New, well-designed. | +| **WebChoreArena** | Tedious cross-page workflows | Reflects real ops work. | +| **WebVoyager** | Live web tasks | **Saturated** — 90%+ scores common; cite only as smoke test. | +| **VisualWebArena** | Multimodal | | +| **MiniWoB++** | Classic unit-task suite | | +| **BrowserGym + AgentLab** | Research harness covering the above | The de-facto research API; pi-chrome's snapshot uid is comparable to BrowserGym's `bid`. | + +Cite live leaderboards rather than hard-coded numbers; agent scores shift monthly. + +--- + +## Reproducing pi-chrome's driver-level claims + +Run [`../test-suite/`](../test-suite) against any browser-control tool. Each challenge exposes `window.__verdict` / `window.__reason` / `window.__events`, so any tool (Playwright, Puppeteer, Selenium, Stagehand, pi-chrome) can grade itself deterministically. Headline release scoring should use the `core` gate; `conditional` depends on declared environment capabilities, and `quality` tracks adversarial/humanization regressions. + +```bash +cd test-suite && python3 -m http.server 8765 +# open http://127.0.0.1:8765/ in the Chrome window the tool controls +``` + +If you build a competing tool, please open a PR with your scores. diff --git a/pi-chrome/docs/EXAMPLES.md b/pi-chrome/docs/EXAMPLES.md new file mode 100644 index 0000000..434d0f8 --- /dev/null +++ b/pi-chrome/docs/EXAMPLES.md @@ -0,0 +1,166 @@ +# pi-chrome examples + +Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`, then `/chrome authorize`. Each one uses Chrome tabs and accounts you already have. + +## Daily workflow + +### PR triage + +```text +Use chrome_tab list to find my GitHub notifications tab. +chrome_snapshot it. Group PRs by: + - awaiting my review + - blocked on me (changes requested back) + - mergeable (approved + green CI) +Output a 5-bullet ranked triage. Do not click anything. +``` + +### Linear standup + +```text +Open my Linear current cycle in the active tab. +chrome_snapshot, then write yesterday/today/blockers +in the exact format my standup channel uses. +``` + +### Slack catch-up + +```text +For each unread channel in my Slack tab, chrome_snapshot, +extract the top 3 messages that mention me or my team, +and summarize what I missed in <100 words total. +``` + +## Debugging + +### Reproduce a customer bug with evidence + +```text +1. chrome_navigate to https://staging.acme.com/orders/ +2. chrome_snapshot +3. Click "Refund" with chrome_click +4. Use chrome_list_network_requests to capture the API call +5. chrome_get_network_request on the failing one — give me the response body +6. chrome_screenshot the error state to ./repro/refund-bug.png +``` + +### Visual diff local vs staging + +```text +chrome_screenshot http://localhost:3000/pricing → ./diff/local.png +chrome_screenshot https://staging.acme.com/pricing → ./diff/staging.png +Then read both, describe layout differences in plain English. +``` + +### Console + network forensics + +```text +Reproduce the checkout bug on the active tab. +After the failure: + - chrome_list_console_messages + - chrome_list_network_requests +Cross-reference the timestamps and tell me what broke first. +``` + +## Admin / ops + +### Multi-tab cross-check + +```text +I have Stripe, Postmark, and our internal admin open in 3 tabs. +For user , chrome_snapshot each tab in turn and find +any field where state disagrees. Output a 3-column table. +``` + +### Bulk gentle action (safe form-fill, no submit) + +```text +Open our vendor portal "Add Vendor" form. +For each row in ./vendors.csv: + - chrome_fill the form + - chrome_screenshot it + - STOP before submit + - chrome_evaluate "history.back()" to return to the list +I will review screenshots and submit manually. +``` + +### Auth-only data pull + +```text +My analytics dashboard is open and the cookie auth would die in headless mode. +chrome_evaluate to read window.__APP_STATE__.dashboardData +and dump today's KPIs as JSON. +``` + +## Demos / PRs + +### Capture screenshots for a PR description + +```text +On localhost:3000/feature-x: + - empty state → ./pr/01-empty.png + - filled state → ./pr/02-filled.png + - error state (delete the API key from devtools first) → ./pr/03-error.png +Save each with chrome_screenshot. Output a markdown block I can paste into the PR. +``` + +### Record a guided demo flow + +```text +On my staging app: +1. Walk the new-onboarding flow start to finish +2. After each chrome_click or chrome_navigate, chrome_screenshot +3. Save numbered PNGs under ./demo/ +4. Write narration captions for each step +``` + +## Forms with frameworks + +### React controlled inputs + +```text +Use `chrome_fill` for React inputs when you want to replace the full value. +Pass `includeSnapshot=true` to verify the component re-rendered with the new value. +``` + +### File upload without the native picker + +```text +chrome_upload_file paths=[./fixtures/avatar.png] selector="input[type=file]" +# Uses Chrome file-input control. No native file picker opens. +``` + +### Drag-to-reorder lists + +```text +chrome_drag fromUid=row-3 toUid=row-1 +# Uses Chrome pointer drag through its input layer. +``` + +## Multi-session patterns + +`pi-chrome` shares one bridge across all Pi sessions on the same machine. Useful patterns: + +### Planner + Worker + +- **Planner session** stays high level: "find the bug, decide the fix." +- **Worker session** runs the actual `chrome_*` tools. +- Both see the same Chrome state because they're both pointing at your real profile. + +### Watcher + +A third Pi session can run `chrome_snapshot` periodically in `background: true` mode and post summaries via `pi-qq` — handy for long-running flows. + +## Chrome input + +Interactive tools use Chrome's real input layer by default: clicks, typing, fill, keys, hover, drag, scroll, and touch. This is reliable for: + +- sign-in flows +- guarded buttons +- audio/video controls +- fullscreen and other user-activation checks +- pages where DOM injection/evaluate is limited, if the agent can use screenshots + coordinates + +Strict CSP note: `chrome_snapshot`/`chrome_evaluate` work even on pages that disallow `unsafe-eval`, because they run via CDP `Runtime.evaluate` (not page-level `eval`/`new Function`), which is not subject to page CSP. `chrome_screenshot`, tab/navigation tools, and real input also work under any CSP. + +Chrome may show its debugger banner while pi-chrome is attached. diff --git a/pi-chrome/docs/FAQ.md b/pi-chrome/docs/FAQ.md new file mode 100644 index 0000000..a450a93 --- /dev/null +++ b/pi-chrome/docs/FAQ.md @@ -0,0 +1,84 @@ +# pi-chrome FAQ + +## Does this work with Brave / Arc / Edge / Vivaldi? + +Yes. Any Chromium-based browser that supports unpacked extensions and the `chrome.debugger` API will work. The extension is named "Pi Chrome Connector" but the source is browser-agnostic. Firefox / WebKit are out of scope (different extension models). + +## Will it slow my browser down? + +The companion extension is idle when no Pi command is in flight. It uses Manifest V3 service worker activation, so it wakes for a request and goes back to sleep. No content script is injected globally. + +## Does it work in Chrome incognito? + +By default no — extensions need explicit "Allow in incognito" permission. Toggle it on `chrome://extensions` if you want pi-chrome to see incognito tabs. We don't recommend it for sensitive work. + +## Will sites detect that I'm automating? + +Interactive controls use Chrome's real input layer via CDP, so normal user-activation gates are satisfied and input is closer to real browser use than DOM-dispatched events. pi-chrome also shapes pointer/keyboard/scroll behavior, but this is not a guarantee of undetectability. Some detectors check for the `chrome.debugger` API attached, and Chrome will show the "Chrome is being debugged" banner. + +The [`test-suite/`](../test-suite) grades browser-control behavior against common detection signals. Its `quality` bucket is adversarial signal, not a blanket promise that every site will treat automation as human. + +## Why do I see a banner saying "Pi Chrome Connector started debugging this browser"? + +That's Chrome's built-in warning when an extension uses `chrome.debugger`. pi-chrome uses Chrome's input layer for interactive controls, so the banner appears while attached. + +## Can a malicious page escape and access my other tabs? + +No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only and rejects browser-origin command requests, so ordinary web pages cannot use CORS to drive it. + +Chrome control is also locked per Pi session until you run `/chrome authorize`; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account. + +## Can multiple Pi sessions use it at once? + +Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` before its chrome_* tools work. Each session also owns its **own** dedicated automation window (ownership is keyed by session id inside the one extension), so concurrent sessions never navigate into or close each other's tabs. + +## Does pi-chrome navigate my current tab? + +No. The first chrome_* action that has no explicit target opens a **dedicated automation window** that pi-chrome owns (falling back to a dedicated tab only if a separate window can't be created), and reuses it for the rest of the session. Your existing tabs and windows are never reused or overwritten. Pass `targetId`/`urlIncludes`/`titleIncludes` to deliberately act on a tab you already have open. + +The window survives `/reload` and Chrome service-worker restarts because ownership is tracked by id and mirrored to `chrome.storage.session`. It is closed when you run `/chrome revoke` and on real session end (not on `/reload`); cleanup only ever closes that session's own window/tab. + +**After a full browser restart**, `chrome.storage.session` is cleared by Chrome. If Chrome's session-restore reopens the old automation window, pi-chrome no longer recognizes it (its tracking is gone), so it is left alone as an ordinary window — pi-chrome will open a fresh dedicated window for the new run rather than reclaim or close the restored one. pi-chrome never closes a window it can't positively identify as its own, so a user window is never at risk. + +## Why ship as an unpacked extension? + +pi-chrome ships as an unpacked extension so the source and broad browser permissions are easy to inspect and update with the npm package. The downside: you load it manually from `chrome://extensions` and reload it after package updates. + +## What happens when I update pi-chrome? + +`/chrome doctor` will warn you if the loaded extension is older than the installed `pi-chrome`. Reload it from `chrome://extensions` to pick up the new version. Updates that add Chrome permissions may require re-approval once. + +## What's the install footprint? + +- Pi side: one extension that registers 19 tools and a few slash commands. +- Chrome side: one unpacked extension, ~2000 LOC of plain JavaScript, no dependencies. + +## Can I script it without Pi? + +The Pi-facing tools are thin wrappers around an HTTP bridge at `127.0.0.1:17318`. You could call it directly from any process, but the API is internal and may change. If you need a stable scripting interface, file an issue and we'll consider stabilizing. + +## What can humans do that pi-chrome cannot? + +pi-chrome controls web pages through Chrome extension APIs, page inspection, screenshots, and browser input. It is not full OS-level human control. Known gaps include native Chrome/OS dialogs (print/save-as, some permission bubbles, password-manager prompts), arbitrary OS app interaction, visual CAPTCHA challenges, hardware-backed auth (passkeys/security keys/biometrics), rich multi-touch/pinch/stylus gestures, and DOM inspection inside cross-origin iframes. Some of these can still be handled with screenshot + coordinate input or user assistance, but they are not first-class deterministic workflows. + +## Does `chrome_evaluate` work on strict-CSP pages? + +Yes. `chrome_evaluate` and `chrome_snapshot` run in the page's MAIN world through CDP `Runtime.evaluate`, which is a DevTools protocol command and is **not** subject to the page's Content-Security-Policy. They work even on pages that block `'unsafe-eval'` (e.g. github.com and many bank/SaaS apps). `chrome_navigate`'s `initScript` injects at document_start via CDP and likewise bypasses CSP. `chrome_screenshot`, tab tools, and real Chrome input also keep working under any CSP. + +## How do I tell whether a click or type worked? + +Use `includeSnapshot=true` on `chrome_click`, `chrome_type`, `chrome_fill`, or `chrome_key`. The tool returns the Chrome-input result plus a fresh snapshot, so the agent can verify text, URL, visible elements, or form values before continuing. + +If the page did not change, take a fresh snapshot or screenshot and check for overlays, disabled controls, stale element uids, or app-side validation. + +## How do I attach a file to a React file input? + +`chrome_upload_file` — uses Chrome DevTools file-input control and fires `input` + `change` events. It does **not** open the native file picker. Works with React/Vue/Angular controlled inputs. + +## Can it record videos? + +Not yet. Screenshots only. Video recording is on the roadmap. + +## How do I file a good bug report? + +Include `/chrome doctor` output, the exact tool call, and the result envelope. If the page is public, link to it; if private, distill it into a benchmark page under `test-suite/challenges/`. See [CONTRIBUTING.md](../CONTRIBUTING.md). diff --git a/pi-chrome/extensions/chrome-profile-bridge/browser-extension/manifest.json b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/manifest.json new file mode 100644 index 0000000..e5fbe61 --- /dev/null +++ b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 3, + "name": "Pi Chrome Connector", + "version": "0.15.46", + "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.", + "permissions": [ + "tabs", + "tabGroups", + "scripting", + "storage", + "activeTab", + "alarms", + "webNavigation", + "debugger" + ], + "host_permissions": [ + "", + "http://127.0.0.1:17318/*" + ], + "background": { + "service_worker": "service_worker.js" + }, + "action": { + "default_title": "Pi Chrome Connector" + } +} diff --git a/pi-chrome/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/service_worker.js new file mode 100644 index 0000000..e057a4b --- /dev/null +++ b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/service_worker.js @@ -0,0 +1,2603 @@ +const BRIDGE_URL = "http://127.0.0.1:17318"; +const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`; +const POLL_ERROR_BACKOFF_MS = 2000; +const DEFAULT_GROUP_COLOR = "blue"; +const PI_GROUP_RE = /^Pi(\b|\s*-)/i; +const VALID_GROUP_COLORS = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]); +const COMMAND_TIMEOUT_MS = 25_000; +const CDP_COMMAND_TIMEOUT_MS = 5_000; +const SCRIPTING_TIMEOUT_MS = 8_000; +const ATTACH_TIMEOUT_MS = 3_000; +let polling = false; + +// =================== pi-chrome automation target ownership =================== +// pi-chrome must never hijack the user's active tab. When a page/navigation action runs without +// an explicit target (targetId/urlIncludes/titleIncludes), we route it to a dedicated automation +// target that pi-chrome created and owns. We prefer a separate Chrome window so the user's +// windows are left untouched; if the windows API is unavailable we fall back to a dedicated tab. +// +// Ownership is SESSION-SCOPED, keyed by the calling Pi session's `sessionKey` (forwarded on the +// wire). One Chrome extension / service worker brokers commands for *all* Pi sessions (see the +// client/server bridge in index.ts), so a single global target would make concurrent sessions +// fight over one window. A per-session map gives each session its own isolated window and lets +// cleanup close exactly that session's target — never another session's, never a user's. +// +// State is mirrored to chrome.storage.session so a service-worker restart (MV3 can suspend the +// worker at any time) re-hydrates ownership instead of orphaning the window it already created. +// storage.session is cleared on browser restart; any window restored by Chrome's session-restore +// is then untracked and simply left alone (we only ever close ids we still recognize as ours). +const automationTargets = new Map(); // sessionKey -> { windowId?: number, tabId: number } +const DEFAULT_SESSION_KEY = "__default__"; +const AUTOMATION_STORAGE_KEY = "piChromeAutomationTargets"; +let automationHydrated = false; + +function sessionKeyOf(params) { + return params && typeof params.sessionKey === "string" && params.sessionKey + ? params.sessionKey + : DEFAULT_SESSION_KEY; +} + +// Re-hydrate the in-memory ownership map from storage.session once per worker lifetime. Best +// effort: storage may be unavailable on old Chrome, and a failure just means we may create a +// fresh window (a harmless orphan) rather than reusing one. +async function hydrateAutomationTargets() { + if (automationHydrated) return; + automationHydrated = true; + try { + const stored = await chrome.storage?.session?.get?.(AUTOMATION_STORAGE_KEY); + const saved = stored && stored[AUTOMATION_STORAGE_KEY]; + if (saved && typeof saved === "object") { + for (const [key, value] of Object.entries(saved)) { + if (value && typeof value.tabId === "number") { + automationTargets.set(key, { + windowId: typeof value.windowId === "number" ? value.windowId : undefined, + tabId: value.tabId, + }); + } + } + } + } catch { + // Ignore: treat as "no persisted state". + } +} + +async function persistAutomationTargets() { + try { + const obj = {}; + for (const [key, value] of automationTargets) { + obj[key] = { windowId: typeof value.windowId === "number" ? value.windowId : null, tabId: value.tabId }; + } + await chrome.storage?.session?.set?.({ [AUTOMATION_STORAGE_KEY]: obj }); + } catch { + // Ignore: persistence is an optimization, not a correctness requirement. + } +} + +// True if `tabId` is a pi-chrome-owned automation tab. Pass `sessionKey` to check a specific +// session; omit it to check ownership across *any* session (used as a safety predicate so we +// never operate on a user-created tab). Never infers ownership from "active". +function isPiChromeOwnedTarget(tabId, sessionKey) { + if (typeof tabId !== "number") return false; + if (sessionKey !== undefined) { + const t = automationTargets.get(sessionKey); + return !!t && t.tabId === tabId; + } + for (const t of automationTargets.values()) if (t.tabId === tabId) return true; + return false; +} + +// Create a fresh automation target for `sessionKey`. If this session already has a tab group, +// create the tab inside that group's window so one Pi session keeps one Chrome tab group (Chrome +// groups cannot span windows). If no group exists yet, prefer an isolated window; fall back to a +// tab. When the tab is created in a pre-existing group window, leave windowId unset so cleanup only +// closes our tab, never that whole window. +async function createAutomationTarget(sessionKey, groupTitle) { + const existingGroup = groupTitle ? await findGroupRecordByTitle(groupTitle) : null; + if (existingGroup && typeof existingGroup.windowId === "number") { + const tab = await chrome.tabs.create({ url: "about:blank", active: false, windowId: existingGroup.windowId }); + automationTargets.set(sessionKey, { windowId: undefined, tabId: typeof tab.id === "number" ? tab.id : undefined }); + await persistAutomationTargets(); + return tab; + } + if (chrome.windows && typeof chrome.windows.create === "function") { + try { + const win = await chrome.windows.create({ url: "about:blank", focused: false }); + const created = win && Array.isArray(win.tabs) ? win.tabs[0] : undefined; + if (created && typeof created.id === "number") { + automationTargets.set(sessionKey, { windowId: typeof win.id === "number" ? win.id : undefined, tabId: created.id }); + await persistAutomationTargets(); + return created; + } + } catch { + // Window creation can fail (policy, headless, etc.); fall back to a dedicated tab below. + } + } + // Tab fallback: the tab lives in a pre-existing (user/shared) window we did NOT create, so we + // must leave windowId unset — cleanup then closes only our tab, never the user's window. + const tab = await chrome.tabs.create({ url: "about:blank", active: false }); + automationTargets.set(sessionKey, { windowId: undefined, tabId: typeof tab.id === "number" ? tab.id : undefined }); + await persistAutomationTargets(); + return tab; +} + +// Return the session's owned automation target if it still exists, else null. Robust to the user +// (or Chrome) having closed it: a stale entry is forgotten so callers can recreate cleanly. +async function resolveOwnedAutomationTarget(sessionKey) { + await hydrateAutomationTargets(); + const t = automationTargets.get(sessionKey); + if (!t || typeof t.tabId !== "number") return null; + const existing = await chrome.tabs.get(t.tabId).catch(() => null); + if (existing && typeof existing.id === "number") return existing; + automationTargets.delete(sessionKey); + await persistAutomationTargets(); + return null; +} + +// Return the session's dedicated automation target, creating it on first use (or after the user +// closed it). Used by page/navigation actions that need a live surface to drive. +async function getOrCreateAutomationTarget(sessionKey, groupTitle) { + return (await resolveOwnedAutomationTarget(sessionKey)) || createAutomationTarget(sessionKey, groupTitle); +} + +// Close only the session's pi-chrome-owned window/tab, and only if it still exists. Never touches +// user tabs/windows or other sessions' targets. Safe to call repeatedly and when nothing exists. +async function cleanupAutomationTarget(sessionKey) { + await hydrateAutomationTargets(); + const t = automationTargets.get(sessionKey); + automationTargets.delete(sessionKey); + await persistAutomationTargets(); + if (!t) return { closedWindowId: null, closedTabId: null }; + const { windowId, tabId } = t; + if (typeof windowId === "number" && chrome.windows && typeof chrome.windows.remove === "function") { + const win = await chrome.windows.get(windowId).catch(() => null); + if (win) { + await chrome.windows.remove(windowId).catch(() => {}); + return { closedWindowId: windowId, closedTabId: typeof tabId === "number" ? tabId : null }; + } + } + if (typeof tabId === "number") { + const tab = await chrome.tabs.get(tabId).catch(() => null); + if (tab) { + await chrome.tabs.remove(tabId).catch(() => {}); + return { closedWindowId: null, closedTabId: tabId }; + } + } + return { closedWindowId: null, closedTabId: null }; +} + +function withTimeout(promise, ms, label, onTimeout) { + let timer; + return Promise.race([ + Promise.resolve(promise).finally(() => clearTimeout(timer)), + new Promise((_, reject) => { + timer = setTimeout(async () => { + try { await onTimeout?.(); } catch {} + reject(new Error(`${label} timed out after ${ms}ms`)); + }, ms); + }), + ]); +} + +// =================== Chrome input (CDP) layer =================== +// Tracks which tabs we have attached chrome.debugger to. +const attachedTabs = new Map(); // tabId -> { detachAt: number, pointer: {x,y} } +const INPUT_IDLE_DETACH_MS = 15_000; +const CDP_VERSION = "1.3"; + +function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } +function rng(min, max) { return min + Math.random() * (max - min); } + +function inputStatus() { + return { + attachedTabs: Array.from(attachedTabs.keys()), + permissionGranted: typeof chrome !== "undefined" && !!chrome.debugger, + }; +} + +// Last few attach failures, kept for diagnostics. +const attachDebugLog = []; +function recordAttachEvent(entry) { + attachDebugLog.push({ ...entry, t: Date.now() }); + if (attachDebugLog.length > 20) attachDebugLog.shift(); +} + +function normalPageTarget(target, tabId) { + const url = String(target?.url || ""); + return target?.tabId === tabId && target?.type === "page" && !url.startsWith("chrome://") && !url.startsWith("chrome-extension://") && !url.startsWith("devtools://"); +} + +async function pageDebuggeeForTab(tabId) { + const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))).catch(() => []); + const target = targets.find((t) => normalPageTarget(t, tabId)); + return target?.id ? { targetId: target.id } : { tabId }; +} + +async function debuggerAttachRaw(tabId, preferredDebuggee) { + const debuggee = preferredDebuggee || { tabId }; + await withTimeout( + chrome.debugger.attach(debuggee, CDP_VERSION), + ATTACH_TIMEOUT_MS, + `Chrome debugger attach to tab ${tabId}`, + async () => { + attachedTabs.delete(tabId); + try { await chrome.debugger.detach(debuggee); } catch {} + }, + ); + return debuggee; +} + +async function attachDebugger(tabId) { + if (!chrome.debugger) throw new Error("chrome.debugger API unavailable; reload the extension to grant the new permission"); + if (attachedTabs.has(tabId)) { + const entry = attachedTabs.get(tabId); + entry.detachAt = Date.now() + INPUT_IDLE_DETACH_MS; + return entry; + } + // Before each attach, force-detach any stale CDP target this extension owns on the tab. + // Chrome sometimes keeps a half-dead session around (extension reload mid-attach, etc.) and + // surfaces it as "Cannot access a chrome-extension://" on the next attach attempt. + try { + const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))); + for (const tgt of targets) { + if (tgt.tabId === tabId && tgt.attached) { + recordAttachEvent({ kind: "stale-target-found", tabId, target: { id: tgt.id, type: tgt.type, url: tgt.url, extensionId: tgt.extensionId } }); + try { await chrome.debugger.detach({ tabId }); } catch {} + await sleep(80); + break; + } + } + } catch {} + let attachedDebuggee = null; + const attemptAttach = async (debuggee) => { + try { + attachedDebuggee = await debuggerAttachRaw(tabId, debuggee); + return null; + } catch (error) { + return error; + } + }; + const retryPageTargetIfExtensionBlocked = async (err, kind) => { + if (!/Cannot access a chrome-extension:\/\/ URL of different extension/i.test(String(err?.message || err))) return err; + const pageDebuggee = await pageDebuggeeForTab(tabId); + recordAttachEvent({ kind, tabId, debuggee: pageDebuggee }); + return attemptAttach(pageDebuggee); + }; + let err = await attemptAttach(); + if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry"); + if (err) { + const msg = String(err?.message || err); + const transient = /Cannot access a chrome-extension|Cannot access contents of|No tab with id|Debugger is not attached|Another debugger|Target closed/i.test(msg); + const tabSnapshot = await chrome.tabs.get(tabId).catch(() => null); + recordAttachEvent({ kind: "attach-failed", tabId, message: msg, tabUrl: tabSnapshot?.url, transient }); + if (!transient) throw err; + if (!tabSnapshot || (tabSnapshot.url || "").startsWith("chrome://") || (tabSnapshot.url || "").startsWith("chrome-extension://")) { + throw new Error(`Chrome can't attach the debugger to this tab (${tabSnapshot?.url ?? "unknown"}). Open a normal http(s) tab and try again.`); + } + await sleep(180); + err = await attemptAttach(); + if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry2"); + if (err) { + recordAttachEvent({ kind: "attach-retry-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url }); + // One more try after a longer settle. Some Chrome builds need ~500ms after a navigation + // for content-script registration on the tab to drain before chrome.debugger.attach + // will accept the target. + await sleep(500); + err = await attemptAttach(); + if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry3"); + if (err) { + recordAttachEvent({ kind: "attach-retry2-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url }); + const meta = await describeInputTarget(tabId); + throw new Error(`Chrome debugger attach failed for tab ${tabId}: ${String(err.message || err)}${targetMetaSuffix(meta)}`); + } + } + } + recordAttachEvent({ kind: "attached", tabId, debuggee: attachedDebuggee }); + // Seed pointer in a plausible "just left the address bar" location. + const entry = { detachAt: Date.now() + INPUT_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 }, debuggee: attachedDebuggee || { tabId } }; + attachedTabs.set(tabId, entry); + return entry; +} + +async function describeInputTarget(tabId) { + const tab = await chrome.tabs.get(Number(tabId)).catch(() => null); + const active = (await chrome.tabs.query({ active: true, lastFocusedWindow: true }).catch(() => []))[0] || null; + let targets = []; + try { targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))); } catch {} + return { + resolvedTab: tab ? { id: tab.id, windowId: tab.windowId, url: tab.url, status: tab.status, title: tab.title, active: tab.active } : null, + activeTab: active ? { id: active.id, windowId: active.windowId, url: active.url, status: active.status, title: active.title, active: active.active } : null, + attachedTabs: Array.from(attachedTabs.keys()), + cdpTargets: targets.map((t) => ({ id: t.id, tabId: t.tabId, type: t.type, url: t.url, attached: t.attached, extensionId: t.extensionId })), + }; +} + +function targetMetaSuffix(meta) { + return `\nTarget metadata: ${JSON.stringify(meta).slice(0, 4000)}`; +} + +async function inputDebug(params) { + const requested = params?.targetId ? await describeInputTarget(Number(params.targetId)) : await describeInputTarget(-1); + return { + extensionVersion: chrome.runtime.getManifest().version, + extensionId: chrome.runtime.id, + ...requested, + recentAttachEvents: attachDebugLog.slice(), + }; +} + +async function detachDebugger(tabId) { + const entry = attachedTabs.get(tabId); + if (!entry) return; + attachedTabs.delete(tabId); + try { await chrome.debugger.detach(entry.debuggee || { tabId }); } catch {} +} + +async function detachAll() { + const ids = Array.from(attachedTabs.keys()); + await Promise.all(ids.map(detachDebugger)); +} + +if (chrome.debugger && chrome.debugger.onDetach) { + chrome.debugger.onDetach.addListener(({ tabId }, reason) => { + if (tabId !== undefined) attachedTabs.delete(tabId); + if (reason === "canceled_by_user") { + console.warn(`[pi-chrome] debugger canceled by user on tab ${tabId}; Chrome input will reattach on next call`); + } + }); +} + +setInterval(() => { + const now = Date.now(); + for (const [tabId, entry] of attachedTabs) { + if (entry.detachAt && entry.detachAt < now) { + void detachDebugger(tabId); + } + } +}, 5000); + +function cdpRaw(tabId, method, params) { + const debuggee = attachedTabs.get(tabId)?.debuggee || { tabId }; + return withTimeout(new Promise((resolve, reject) => { + chrome.debugger.sendCommand(debuggee, method, params || {}, (result) => { + if (chrome.runtime.lastError) reject(new Error(`${method}: ${chrome.runtime.lastError.message}`)); + else resolve(result); + }); + }), CDP_COMMAND_TIMEOUT_MS, `CDP ${method}`, async () => { + attachedTabs.delete(tabId); + try { await chrome.debugger.detach(debuggee); } catch {} + }); +} + +function executeScriptTimed(options, label) { + return withTimeout(chrome.scripting.executeScript(options), SCRIPTING_TIMEOUT_MS, label || "chrome.scripting.executeScript"); +} + +// Wraps cdpRaw with one auto-recover on detached/closed sessions: +// chrome.debugger.attach can stay cached in attachedTabs even after Chrome killed +// the session (tab nav, devtools opened/closed, etc). Recover by detaching the +// stale entry and re-attaching, then retry the command once. +// Find foreign chrome-extension targets currently anchored to the tab. Password managers, +// autofill helpers, and other input-attached extensions create type:"other" CDP targets +// whose URL is chrome-extension:///... When that target is in focus, CDP refuses +// our Input.dispatchMouseEvent calls with "Cannot access a chrome-extension:// URL of +// different extension" — surfacing a cryptic error to the user. +async function findForeignExtensionTargets() { + try { + const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || []))); + return targets.filter((t) => { + const url = String(t.url || ""); + if (!url.startsWith("chrome-extension://")) return false; + if (t.extensionId === chrome.runtime.id) return false; + return true; + }); + } catch { + return []; + } +} + +function extractForeignExtId(targets) { + for (const t of targets) { + if (t.extensionId && t.extensionId !== chrome.runtime.id) return t.extensionId; + const m = String(t.url || "").match(/chrome-extension:\/\/([a-p]+)\//); + if (m && m[1] !== chrome.runtime.id) return m[1]; + } + return null; +} + +async function dismissOverlayViaEscape(tabId) { + // Esc routes through key dispatcher (target-by-focus), not by mouse coordinates, so it + // works even when a foreign chrome-extension popup is intercepting pointer events. + try { + await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 }); + await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 }); + await sleep(120); + } catch {} +} + +async function cdp(tabId, method, params) { + try { + return await cdpRaw(tabId, method, params); + } catch (error) { + const msg = String(error?.message || error); + const isStale = /Debugger is not attached|Detached while|Target closed|No tab with id/i.test(msg); + const isForeignExtBlock = /Cannot access a chrome-extension:\/\/ URL of different extension/i.test(msg); + if (isForeignExtBlock && /Input\./.test(method)) { + // Foreign chrome-extension popup (autofill, password manager) is hijacking input. + // Try once: dismiss via Esc, then retry. + const before = await findForeignExtensionTargets(); + recordAttachEvent({ kind: "foreign-ext-detected", tabId, method, foreignExtId: extractForeignExtId(before), targetCount: before.length }); + await dismissOverlayViaEscape(tabId); + try { + return await cdpRaw(tabId, method, params); + } catch (retryErr) { + const retryMsg = String(retryErr?.message || retryErr); + if (/Cannot access a chrome-extension:\/\/ URL of different extension/i.test(retryMsg)) { + const after = await findForeignExtensionTargets(); + const id = extractForeignExtId(after) || extractForeignExtId(before) || "unknown"; + throw new Error( + `Another Chrome extension (${id}) has an input overlay on this page (e.g. a password manager / autofill popup). \n` + + `pi-chrome tried to dismiss it with Escape but it reappeared. Disable that extension on this page, close its popup, or focus the field via Tab instead of clicking.`, + ); + } + throw retryErr; + } + } + if (!isStale) throw error; + attachedTabs.delete(tabId); + await attachDebugger(tabId).catch(() => undefined); + return cdpRaw(tabId, method, params); + } +} + +// cdpEval: evaluate a JavaScript expression string in the page's MAIN world via CDP +// Runtime.evaluate. Runtime.evaluate is a DevTools protocol command and is NOT subject to +// the page's Content-Security-Policy, so it works on pages that ship `script-src 'self'` +// without `'unsafe-eval'` (which blocks `eval`/`new Function`). Ensures the debugger is +// attached first. Returns the raw CDP result ({ result, exceptionDetails }). +async function cdpEval(tabId, expression, opts) { + await attachDebugger(tabId); + return cdp(tabId, "Runtime.evaluate", { + expression, + returnByValue: true, + awaitPromise: true, + userGesture: true, + ...(opts || {}), + }); +} + +function cdpExceptionText(details) { + if (!details) return ""; + return String( + details.exception?.description || + details.exception?.value || + details.text || + "", + ); +} + +function cdpIsSyntaxError(details) { + if (!details) return false; + const className = String(details.exception?.className || ""); + return className === "SyntaxError" || /SyntaxError/.test(cdpExceptionText(details)); +} + +// Resolve target -> {x, y, rect} in viewport coords by running tiny script in tab. +async function resolveTargetInTab(tabId, params) { + const results = await executeScriptTimed({ + target: { tabId, frameIds: [0] }, + world: "MAIN", + func: (selector, uid, x, y) => { + const state = window.__PI_CHROME_STATE__; + let el = null; + if (uid) { + el = state && state.elements ? state.elements[uid] : null; + if (!el || !el.isConnected) return { found: false, staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href }; + } else if (selector) { + el = document.querySelector(selector); + } + if (el) { + el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); + const r = el.getBoundingClientRect(); + return { x: r.left + r.width / 2, y: r.top + r.height / 2, rect: { left: r.left, top: r.top, width: r.width, height: r.height }, tag: el.tagName, found: true }; + } + if (typeof x === "number" && typeof y === "number") return { x, y, rect: null, tag: null, found: true }; + return { found: false }; + }, + args: [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null], + }, `resolve input target in tab ${tabId}`); + const v = results?.[0]?.result; + if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot"); + if (!v || !v.found) throw new Error("Could not resolve target element for Chrome input"); + return v; +} + +function pickInsideRect(rect) { + if (!rect) return null; + const insetX = Math.min(rect.width * 0.35, Math.max(2, rect.width / 2 - 1)); + const insetY = Math.min(rect.height * 0.35, Math.max(2, rect.height / 2 - 1)); + return { + x: rect.left + rect.width / 2 + rng(-insetX, insetX), + y: rect.top + rect.height / 2 + rng(-insetY, insetY), + }; +} + +async function cdpMoveTo(tabId, x, y) { + const entry = attachedTabs.get(tabId); + const startX = entry?.pointer?.x ?? Math.max(20, Math.min(400, x - 200)); + const startY = entry?.pointer?.y ?? Math.max(20, Math.min(400, y - 200)); + const n = Math.max(18, Math.min(42, Math.round(Math.hypot(x - startX, y - startY) / 18))); + for (let i = 1; i <= n; i++) { + const t = i / n; + const ease = t * t * (3 - 2 * t); + const wobble = Math.sin(t * Math.PI) * 8; + const px = startX + (x - startX) * ease + rng(-wobble, wobble); + const py = startY + (y - startY) * ease + rng(-wobble, wobble); + await cdp(tabId, "Input.dispatchMouseEvent", { + type: "mouseMoved", x: px, y: py, button: "none", buttons: 0, pointerType: "mouse", + }); + await sleep(rng(5, 16)); + } + if (entry) entry.pointer = { x, y }; +} + +function cdpModifiersFor(mods) { + let m = 0; + if (mods?.altKey) m |= 1; + if (mods?.ctrlKey) m |= 2; + if (mods?.metaKey) m |= 4; + if (mods?.shiftKey) m |= 8; + return m; +} + +// Resolve a single printable character to { code, keyCode, needShift } on a US layout. +// Self-contained (maps defined inline) so it can be serialized into the page via +// HELPER_FUNCS for the DOM-event fallback as well as used by the CDP path. +// Using charCodeAt() for punctuation is wrong: e.g. "." is charCode 46 which collides +// with VK_DELETE, "-" is 45 (VK_INSERT), so app keydown handlers misfire and drop input. +function usKeyLayoutForChar(ch) { + const PUNCT = { + "`": { code: "Backquote", keyCode: 192 }, "~": { code: "Backquote", keyCode: 192, shift: true }, + "-": { code: "Minus", keyCode: 189 }, "_": { code: "Minus", keyCode: 189, shift: true }, + "=": { code: "Equal", keyCode: 187 }, "+": { code: "Equal", keyCode: 187, shift: true }, + "[": { code: "BracketLeft", keyCode: 219 }, "{": { code: "BracketLeft", keyCode: 219, shift: true }, + "]": { code: "BracketRight", keyCode: 221 }, "}": { code: "BracketRight", keyCode: 221, shift: true }, + "\\": { code: "Backslash", keyCode: 220 }, "|": { code: "Backslash", keyCode: 220, shift: true }, + ";": { code: "Semicolon", keyCode: 186 }, ":": { code: "Semicolon", keyCode: 186, shift: true }, + "'": { code: "Quote", keyCode: 222 }, "\"": { code: "Quote", keyCode: 222, shift: true }, + ",": { code: "Comma", keyCode: 188 }, "<": { code: "Comma", keyCode: 188, shift: true }, + ".": { code: "Period", keyCode: 190 }, ">": { code: "Period", keyCode: 190, shift: true }, + "/": { code: "Slash", keyCode: 191 }, "?": { code: "Slash", keyCode: 191, shift: true }, + " ": { code: "Space", keyCode: 32 }, + }; + // Shifted digit symbols share the digit's physical code + keyCode. + const SHIFT_DIGIT = { ")": "0", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9" }; + if (/^[a-z]$/.test(ch)) return { code: `Key${ch.toUpperCase()}`, keyCode: ch.toUpperCase().charCodeAt(0), needShift: false }; + if (/^[A-Z]$/.test(ch)) return { code: `Key${ch}`, keyCode: ch.charCodeAt(0), needShift: true }; + if (/^[0-9]$/.test(ch)) return { code: `Digit${ch}`, keyCode: ch.charCodeAt(0), needShift: false }; + if (SHIFT_DIGIT[ch]) { const d = SHIFT_DIGIT[ch]; return { code: `Digit${d}`, keyCode: d.charCodeAt(0), needShift: true }; } + const p = PUNCT[ch]; + if (p) return { code: p.code, keyCode: p.keyCode, needShift: !!p.shift }; + // Unknown char (e.g. unicode): keep text-driven insertion, avoid bogus keyCode collisions. + return { code: ch, keyCode: 0, needShift: false }; +} + +function cdpKeyInfo(key, shifted) { + // Map common keys to CDP key event init fields. Returns { code, key, windowsVirtualKeyCode, text }. + const SPECIAL = { + Enter: { code: "Enter", windowsVirtualKeyCode: 13, text: "\r" }, + Tab: { code: "Tab", windowsVirtualKeyCode: 9, text: "\t" }, + Backspace: { code: "Backspace", windowsVirtualKeyCode: 8, text: "" }, + Delete: { code: "Delete", windowsVirtualKeyCode: 46, text: "" }, + Escape: { code: "Escape", windowsVirtualKeyCode: 27, text: "" }, + ArrowLeft: { code: "ArrowLeft", windowsVirtualKeyCode: 37, text: "" }, + ArrowUp: { code: "ArrowUp", windowsVirtualKeyCode: 38, text: "" }, + ArrowRight: { code: "ArrowRight", windowsVirtualKeyCode: 39, text: "" }, + ArrowDown: { code: "ArrowDown", windowsVirtualKeyCode: 40, text: "" }, + Shift: { code: "ShiftLeft", windowsVirtualKeyCode: 16, text: "" }, + Control: { code: "ControlLeft", windowsVirtualKeyCode: 17, text: "" }, + Alt: { code: "AltLeft", windowsVirtualKeyCode: 18, text: "" }, + Meta: { code: "MetaLeft", windowsVirtualKeyCode: 91, text: "" }, + " ": { code: "Space", windowsVirtualKeyCode: 32, text: " " }, + }; + if (SPECIAL[key]) return { key, ...SPECIAL[key] }; + if (key.length === 1) { + const ch = key; + const layout = usKeyLayoutForChar(ch); + return { key: ch, code: layout.code, windowsVirtualKeyCode: layout.keyCode, text: ch }; + } + return { key, code: key, windowsVirtualKeyCode: 0, text: "" }; +} + +async function cdpTypeChar(tabId, ch) { + const needShift = /^[A-Z]$/.test(ch) || "~!@#$%^&*()_+{}|:\"<>?".includes(ch); + let modifiers = 0; + if (needShift) { + await cdp(tabId, "Input.dispatchKeyEvent", { type: "keyDown", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, modifiers: 8 }); + modifiers = 8; + await sleep(rng(8, 22)); + } + const info = cdpKeyInfo(ch); + await cdp(tabId, "Input.dispatchKeyEvent", { + type: "keyDown", key: info.key, code: info.code, + windowsVirtualKeyCode: info.windowsVirtualKeyCode, nativeVirtualKeyCode: info.windowsVirtualKeyCode, + text: info.text, unmodifiedText: info.text, modifiers, + }); + await sleep(rng(25, 90)); + await cdp(tabId, "Input.dispatchKeyEvent", { + type: "keyUp", key: info.key, code: info.code, + windowsVirtualKeyCode: info.windowsVirtualKeyCode, modifiers, + }); + if (needShift) { + await sleep(rng(5, 18)); + await cdp(tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, modifiers: 0 }); + } + await sleep(rng(35, 130)); +} + +async function domClickFallback(tabId, params, cause) { + const results = await executeScriptTimed({ + target: { tabId, frameIds: [0] }, + world: "MAIN", + func: (selector, uid, x, y) => { + const state = window.__PI_CHROME_STATE__; + let el = uid && state && state.elements ? state.elements[uid] : null; + if (uid && (!el || !el.isConnected)) return { staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href }; + if (!el && selector) el = document.querySelector(selector); + if (!el && typeof x === "number" && typeof y === "number") el = document.elementFromPoint(x, y); + if (!el) throw new Error(`DOM fallback target not found: ${uid || selector || `${x},${y}`}`); + el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); + const rect = el.getBoundingClientRect(); + const eventInit = { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, button: 0, buttons: 1 }; + el.dispatchEvent(new PointerEvent("pointerdown", { ...eventInit, pointerId: 1, pointerType: "mouse", isPrimary: true })); + el.dispatchEvent(new MouseEvent("mousedown", eventInit)); + if (typeof el.focus === "function") el.focus({ preventScroll: true }); + el.dispatchEvent(new PointerEvent("pointerup", { ...eventInit, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 0 })); + el.dispatchEvent(new MouseEvent("mouseup", { ...eventInit, buttons: 0 })); + el.click(); + return { tag: el.tagName, url: location.href }; + }, + args: [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null], + }, `DOM click fallback in tab ${tabId}`); + const v = results?.[0]?.result; + if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot"); + return { input: "dom-fallback", reason: String(cause?.message || cause).slice(0, 500), tag: v?.tag }; +} + +async function chromeInputClick(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + try { + await attachDebugger(tab.id); + const resolved = await resolveTargetInTab(tab.id, params); + const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y }; + await cdpMoveTo(tab.id, point.x, point.y); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 }); + await sleep(rng(45, 140)); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" }); + // Reset :focus-visible if the click landed on a focusable element. CDP-driven pointer + // focus can leave :focus-visible=true in Chromium, which trips heuristics that expect + // Reset focus styling after pointer click when possible. + if (params.selector || params.uid) { + await executeScriptTimed({ + target: { tabId: tab.id, frameIds: [0] }, + world: "MAIN", + func: (sel, uid) => { + const state = window.__PI_CHROME_STATE__; + let el = null; + if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid]; + else if (sel) el = document.querySelector(sel); + if (el && typeof el.focus === "function" && el === document.activeElement) { + try { el.blur(); el.focus({ preventScroll: true, focusVisible: false }); } catch {} + } + }, + args: [params.selector ?? null, params.uid ?? null], + }, `reset focus style in tab ${tab.id}`).catch(() => undefined); + } + return { input: "chrome", x: point.x, y: point.y, tag: resolved.tag }; + } catch (error) { + if (params.domFallback === false) throw error; + return domClickFallback(tab.id, params, error); + } +} + +async function chromeInputHover(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + const resolved = await resolveTargetInTab(tab.id, params); + const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y }; + await cdpMoveTo(tab.id, point.x, point.y); + await sleep(rng(80, 220)); + return { input: "chrome", x: point.x, y: point.y, tag: resolved.tag }; +} + +async function chromeInputKey(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + const key = String(params.key || ""); + if (!key) throw new Error("chrome.key: missing key"); + const mods = params.modifiers || {}; + const modBits = cdpModifiersFor(mods); + // Press modifiers in standard order, then key, then release in reverse. + const modOrder = []; + if (mods.metaKey) modOrder.push({ key: "Meta", code: "MetaLeft", vk: 91 }); + if (mods.ctrlKey) modOrder.push({ key: "Control", code: "ControlLeft", vk: 17 }); + if (mods.altKey) modOrder.push({ key: "Alt", code: "AltLeft", vk: 18 }); + if (mods.shiftKey) modOrder.push({ key: "Shift", code: "ShiftLeft", vk: 16 }); + for (const m of modOrder) { + await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: m.key, code: m.code, windowsVirtualKeyCode: m.vk, modifiers: modBits }); + await sleep(rng(6, 18)); + } + const info = cdpKeyInfo(key); + // When modifiers are active, browsers usually emit "rawKeyDown" (no text) so chords like Cmd+V don't insert the literal char. + const downType = modBits ? "rawKeyDown" : "keyDown"; + await cdp(tab.id, "Input.dispatchKeyEvent", { + type: downType, key: info.key, code: info.code, + windowsVirtualKeyCode: info.windowsVirtualKeyCode, nativeVirtualKeyCode: info.windowsVirtualKeyCode, + text: modBits ? "" : info.text, unmodifiedText: modBits ? "" : info.text, modifiers: modBits, + }); + await sleep(rng(25, 90)); + await cdp(tab.id, "Input.dispatchKeyEvent", { + type: "keyUp", key: info.key, code: info.code, + windowsVirtualKeyCode: info.windowsVirtualKeyCode, modifiers: modBits, + }); + for (const m of modOrder.reverse()) { + await sleep(rng(5, 18)); + await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: m.key, code: m.code, windowsVirtualKeyCode: m.vk, modifiers: 0 }); + } + return { input: "chrome", key: info.key, modifiers: mods }; +} + +async function chromeInputType(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + if (params.selector || params.uid) { + // Focus target by clicking it first. + const resolved = await resolveTargetInTab(tab.id, params); + const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y }; + await cdpMoveTo(tab.id, point.x, point.y); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 }); + await sleep(rng(45, 110)); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" }); + await sleep(rng(50, 120)); + } + const text = String(params.text || ""); + for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch); + if (params.pressEnter) { + await cdpTypeChar(tab.id, "\r").catch(() => undefined); + await chromeInputKey({ ...params, key: "Enter" }); + } + return { input: "chrome", length: text.length }; +} + +async function domFillFallback(tabId, params, cause) { + if (!(params.selector || params.uid)) throw cause; + const results = await executeScriptTimed({ + target: { tabId, frameIds: [0] }, + world: "MAIN", + func: async (selector, uid, text, submit) => { + const state = window.__PI_CHROME_STATE__; + let el = uid && state && state.elements ? state.elements[uid] : null; + if (uid && (!el || !el.isConnected)) return { staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href }; + if (!el && selector) el = document.querySelector(selector); + if (!el) throw new Error(`DOM fallback target not found: ${uid || selector}`); + el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); + if (typeof el.focus === "function") el.focus({ preventScroll: true }); + const value = String(text ?? ""); + if ("value" in el) { + const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set; + if (setter) setter.call(el, value); + else el.value = value; + } else if (el.isContentEditable) { + el.textContent = value; + } else { + throw new Error(`DOM fallback target is not fillable: <${el.tagName.toLowerCase()}>`); + } + el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value })); + el.dispatchEvent(new Event("change", { bubbles: true })); + if (submit) { + const form = el.closest("form"); + if (form) form.requestSubmit ? form.requestSubmit() : form.submit(); + else document.querySelector("button,[type=submit]")?.click(); + } + return { valueMatches: "value" in el ? el.value === value : el.textContent === value, tag: el.tagName, url: location.href }; + }, + args: [params.selector ?? null, params.uid ?? null, params.text ?? "", params.submit === true], + }, `DOM fill fallback in tab ${tabId}`); + const v = results?.[0]?.result; + if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot"); + return { input: "dom-fallback", length: String(params.text || "").length, valueMatches: v?.valueMatches, reason: String(cause?.message || cause).slice(0, 500), tag: v?.tag }; +} + +async function chromeInputFill(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + try { + await attachDebugger(tab.id); + if (!(params.selector || params.uid)) throw new Error("chrome.fill: selector or uid required"); + const resolved = await resolveTargetInTab(tab.id, params); + const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y }; + await cdpMoveTo(tab.id, point.x, point.y); + // Triple-click selects all in input fields. + for (let i = 1; i <= 3; i++) { + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse", force: 0.5 }); + await sleep(rng(20, 60)); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" }); + await sleep(rng(20, 60)); + } + // Delete selection. + await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 }); + await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 }); + await sleep(rng(20, 60)); + const text = String(params.text || ""); + for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch); + if (params.submit) await chromeInputKey({ ...params, key: "Enter" }); + return { input: "chrome", length: text.length }; + } catch (error) { + if (params.domFallback === false) throw error; + return domFillFallback(tab.id, params, error); + } +} + +async function chromeInputScroll(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + const resolved = (params.selector || params.uid) ? await resolveTargetInTab(tab.id, params) : { x: 100, y: 100, rect: null }; + const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x; + const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y; + const totalY = params.deltaY || 0, totalX = params.deltaX || 0; + // Profile mimics a trackpad flick: short ramp-up (~15% of events), then geometric decay + // with a ~12% drop per event. Gives momentum tail tests something to find, and the small + // tail deltas (a handful of <20px events) put IntersectionObserver thresholds in range. + const peak = Math.max(Math.abs(totalY), Math.abs(totalX)); + // Aim peak event ~22px so cumulative wheel approach to target seeds low-ratio IO samples. + const PEAK_TARGET = 22; + const w = []; + // Build weights for an arbitrary n, then iterate to find an n where peak * (w_peak/sum) <= PEAK_TARGET. + function build(n) { + const arr = []; + const peakIdx = Math.max(1, Math.floor(n * 0.15)); + for (let i = 0; i < n; i++) { + if (i <= peakIdx) arr.push(0.5 + 0.5 * (i / peakIdx)); // 0.5 → 1.0 + else arr.push(Math.pow(0.88, i - peakIdx)); // ~12% drop per step + } + return arr; + } + let n = Math.max(12, params.steps || 24); + for (let attempt = 0; attempt < 8; attempt++) { + const arr = build(n); + const s = arr.reduce((a, b) => a + b, 0); + const peakStep = peak * (Math.max(...arr) / s); + if (peakStep <= PEAK_TARGET || n >= 240) { + w.length = 0; + w.push(...arr); + break; + } + n = Math.ceil(n * 1.4); + } + if (w.length === 0) w.push(...build(n)); + const sumW = w.reduce((a, b) => a + b, 0); + for (let i = 0; i < n; i++) { + const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW); + await cdp(tab.id, "Input.dispatchMouseEvent", { + type: "mouseWheel", x, y, deltaX: dx, deltaY: dy, pointerType: "mouse", + }); + // Sleep one+ frame so IntersectionObserver / rAF samples can run between events. + await sleep(rng(22, 48)); + } + return { input: "chrome", deltaX: totalX, deltaY: totalY, steps: n }; +} + +async function chromeInputTap(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + const resolved = (params.selector || params.uid || (typeof params.x === "number" && typeof params.y === "number")) + ? await resolveTargetInTab(tab.id, params) + : null; + if (!resolved || !resolved.found) throw new Error("chrome.tap: target not found"); + const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y }; + const tp = { x: point.x, y: point.y, radiusX: 8, radiusY: 8, rotationAngle: 0, force: 0.5, id: 1 }; + await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [tp] }); + await sleep(rng(40, 110)); + await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] }); + return { input: "chrome", x: point.x, y: point.y, tag: resolved.tag }; +} + +async function chromeInputDrag(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + const from = await resolveTargetInTab(tab.id, { selector: params.fromSelector ?? null, uid: params.fromUid ?? null, x: params.fromX ?? null, y: params.fromY ?? null }); + const to = await resolveTargetInTab(tab.id, { selector: params.toSelector ?? null, uid: params.toUid ?? null, x: params.toX ?? null, y: params.toY ?? null }); + const fp = from.rect ? pickInsideRect(from.rect) : { x: from.x, y: from.y }; + const tp = to.rect ? pickInsideRect(to.rect) : { x: to.x, y: to.y }; + await cdpMoveTo(tab.id, fp.x, fp.y); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 }); + await sleep(rng(60, 140)); + const steps = params.steps || 20; + for (let i = 1; i <= steps; i++) { + const t = i / steps; + const ease = t * t * (3 - 2 * t); + const wobble = Math.sin(t * Math.PI) * 6; + const x = fp.x + (tp.x - fp.x) * ease + rng(-wobble, wobble); + const y = fp.y + (tp.y - fp.y) * ease + rng(-wobble, wobble); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "left", buttons: 1, pointerType: "mouse" }); + await sleep(rng(10, 26)); + } + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: tp.x, y: tp.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" }); + return { input: "chrome", from: fp, to: tp, steps }; +} + +async function chromeInputUpload(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + await attachDebugger(tab.id); + if (!(params.selector || params.uid)) throw new Error("chrome.upload: selector or uid required"); + const paths = Array.isArray(params.paths) ? params.paths.map(String) : []; + if (!paths.length) throw new Error("chrome.upload: no file paths provided"); + const expression = `(() => { + const selector = ${JSON.stringify(params.selector ?? null)}; + const uid = ${JSON.stringify(params.uid ?? null)}; + const state = window.__PI_CHROME_STATE__; + const el = uid && state && state.elements ? state.elements[uid] : (selector ? document.querySelector(selector) : null); + if (!el || el.tagName !== "INPUT" || el.type !== "file") throw new Error("Target must be "); + el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); + return el; + })()`; + const evaluated = await cdp(tab.id, "Runtime.evaluate", { expression, objectGroup: "pi-chrome-upload", includeCommandLineAPI: false, returnByValue: false }); + if (evaluated.exceptionDetails) throw new Error(evaluated.exceptionDetails.text || "Could not resolve file input"); + const objectId = evaluated.result?.objectId; + if (!objectId) throw new Error("Could not resolve file input object"); + await cdp(tab.id, "DOM.enable", {}).catch(() => undefined); + const requested = await cdp(tab.id, "DOM.requestNode", { objectId }); + if (!requested.nodeId) throw new Error("Could not resolve file input node"); + await cdp(tab.id, "DOM.setFileInputFiles", { nodeId: requested.nodeId, files: paths }); + await cdp(tab.id, "Runtime.callFunctionOn", { + objectId, + functionDeclaration: `function() { this.dispatchEvent(new Event("input", { bubbles: true })); this.dispatchEvent(new Event("change", { bubbles: true })); return this.files ? this.files.length : 0; }`, + returnByValue: true, + }).catch(() => undefined); + await cdp(tab.id, "Runtime.releaseObject", { objectId }).catch(() => undefined); + return { input: "chrome", uploaded: paths.map((path) => ({ path })) }; +} +// =============================================================== + + +function armKeepaliveAlarm() { + chrome.alarms.create("pi-bridge-keepalive", { periodInMinutes: 0.5 }); +} + +chrome.runtime.onInstalled.addListener(() => { + chrome.action.setBadgeText({ text: "pi" }); + chrome.action.setBadgeBackgroundColor({ color: "#4f46e5" }); + armKeepaliveAlarm(); + void pollLoop(); +}); + +chrome.runtime.onStartup.addListener(() => { + armKeepaliveAlarm(); + void pollLoop(); +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === "pi-bridge-keepalive") void pollLoop(); +}); + +chrome.action.onClicked.addListener(() => { + armKeepaliveAlarm(); + void pollLoop(); +}); + +armKeepaliveAlarm(); + +setInterval(() => { + void pollLoop(); +}, 1000); + +async function pollLoop() { + if (polling) return; + polling = true; + try { + while (true) { + const response = await fetch(`${BRIDGE_URL}/next?name=${encodeURIComponent(CLIENT_NAME)}`, { + cache: "no-store", + }); + if (!response.ok) throw new Error(`bridge /next HTTP ${response.status}`); + const expected = response.headers.get("x-pi-chrome-version"); + const ours = chrome.runtime.getManifest().version; + if (expected && expected !== ours && isVersionOlder(ours, expected)) { + console.warn(`[pi-chrome] extension v${ours} behind pi-chrome v${expected}; reloading extension`); + try { chrome.runtime.reload(); } catch {} + return; + } + const payload = await response.json(); + if (payload.type === "command") await handleCommand(payload.command); + } + } catch (error) { + await sleep(POLL_ERROR_BACKOFF_MS); + } finally { + polling = false; + } +} + +async function handleCommand(command) { + try { + const result = await withTimeout( + dispatch(command.action, command.params ?? {}), + COMMAND_TIMEOUT_MS, + command.action || "Chrome command", + () => detachAll(), + ); + await postResult({ id: command.id, ok: true, result }); + } catch (error) { + await postResult({ id: command.id, ok: false, error: error?.message ?? String(error) }); + } +} + +async function postResult(result) { + await fetch(`${BRIDGE_URL}/result`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(result), + }); +} + +function isVersionOlder(a, b) { + const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0); + const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0); + const n = Math.max(pa.length, pb.length); + for (let i = 0; i < n; i++) { + const x = pa[i] ?? 0, y = pb[i] ?? 0; + if (x < y) return true; + if (x > y) return false; + } + return false; +} + +function cleanGroupTitle(value) { + const text = String(value || "Pi").replace(/\s+/g, " ").trim().slice(0, 80); + return text || "Pi"; +} + +function cleanGroupColor(value) { + const color = String(value || DEFAULT_GROUP_COLOR).toLowerCase(); + return VALID_GROUP_COLORS.has(color) ? color : DEFAULT_GROUP_COLOR; +} + +async function groupRecord(groupId) { + if (typeof groupId !== "number" || groupId < 0 || !chrome.tabGroups) return null; + const group = await chrome.tabGroups.get(groupId).catch(() => null); + if (!group) return null; + return { + id: group.id, + title: group.title || "", + color: group.color || "", + collapsed: Boolean(group.collapsed), + windowId: group.windowId, + piGroup: Boolean(group.title && PI_GROUP_RE.test(group.title)), + }; +} + +// Find existing tab groups whose title matches `title` (case-insensitive). +// Same-window lookup is used when grouping an already-created tab. Any-window lookup is used before +// creating a new Pi tab so one Pi session keeps one tab group and new tabs are created in that +// group's window (Chrome tab groups cannot span windows). +async function findGroupByTitle(windowId, title) { + if (!chrome.tabGroups) return null; + const wanted = cleanGroupTitle(title).toLowerCase(); + const groups = await chrome.tabGroups.query({ windowId }).catch(() => []); + const match = groups.find((g) => (g.title || "").trim().toLowerCase() === wanted); + return match ? match.id : null; +} + +async function findGroupRecordByTitle(title) { + if (!chrome.tabGroups) return null; + const wanted = cleanGroupTitle(title).toLowerCase(); + const groups = await chrome.tabGroups.query({}).catch(() => []); + return groups.find((g) => (g.title || "").trim().toLowerCase() === wanted) || null; +} + +// Add `tab` to a tab group, then set title/color. If the tab is ungrouped, reuse an +// existing same-title group in its window when present, otherwise create a new group. +async function groupTab(tab, title, color) { + if (!chrome.tabGroups) throw new Error("chrome.tabGroups API unavailable; reload the extension after granting the tabGroups permission"); + if (!tab || typeof tab.id !== "number") throw new Error("No tab to group"); + const groupTitle = cleanGroupTitle(title); + let groupId = tab.groupId; + if (typeof groupId !== "number" || groupId < 0) { + const existing = await findGroupByTitle(tab.windowId, groupTitle); + groupId = existing !== null + ? await chrome.tabs.group({ groupId: existing, tabIds: [tab.id] }) + : await chrome.tabs.group({ tabIds: [tab.id] }); + } + await chrome.tabGroups.update(groupId, { title: groupTitle, color: cleanGroupColor(color), collapsed: false }); + const grouped = await chrome.tabs.get(tab.id); + return { tab: await formatTab(grouped), group: await groupRecord(groupId) }; +} + +async function dispatch(action, params) { + switch (action) { + case "tab.version": + return { + extensionId: chrome.runtime.id, + extensionVersion: chrome.runtime.getManifest().version, + bridgeUrl: BRIDGE_URL, + userAgent: navigator.userAgent, + }; + case "tab.list": { + const tabs = await chrome.tabs.query({}); + return Promise.all(tabs.map(formatTab)); + } + case "tab.new": { + // Every Pi-opened tab must join a tab group. There is intentionally no opt-out: an ungrouped + // Pi-created tab is easy to lose among user tabs. If grouping fails after creation, close the + // tab best-effort before surfacing the error so tab.new never leaves an ungrouped Pi tab. + const groupTitle = params.groupTitle || "Pi"; + const existingGroup = await findGroupRecordByTitle(groupTitle); + const createParams = { url: params.url || "about:blank", active: true }; + if (existingGroup && typeof existingGroup.windowId === "number") createParams.windowId = existingGroup.windowId; + const tab = await chrome.tabs.create(createParams); + try { + return await groupTab(tab, groupTitle, params.groupColor); + } catch (error) { + if (typeof tab.id === "number") await chrome.tabs.remove(tab.id).catch(() => {}); + throw error; + } + } + case "tab.activate": { + // Management actions never auto-create an automation target (createOwnedTarget:false): with + // no explicit target they act on an owned target if one exists, else error — they must never + // fall back to (or spawn a tab just to touch) the user's active tab. + const tab = await getTabByParams(params, { createOwnedTarget: false }); + await chrome.windows.update(tab.windowId, { focused: true }); + return formatTab(await chrome.tabs.update(tab.id, { active: true })); + } + case "tab.group": { + const tab = await getTabByParams(params, { createOwnedTarget: false }); + return groupTab(tab, params.groupTitle || "Pi", params.groupColor); + } + case "tab.ungroup": { + const tab = await getTabByParams(params, { createOwnedTarget: false }); + if (typeof tab.groupId === "number" && tab.groupId >= 0) await chrome.tabs.ungroup(tab.id); + return formatTab(await chrome.tabs.get(tab.id)); + } + case "tab.close": { + const tab = await getTabByParams(params, { createOwnedTarget: false }); + await chrome.tabs.remove(tab.id); + return { closed: tab.id }; + } + case "page.snapshot": + return snapshotInTab(params); + case "page.inspect": + return inspectInTab(params); + case "page.evaluate": + return evaluateInTab(params); + case "page.click": + return withOptionalSnapshot(params, chromeInputClick); + case "page.hover": + return chromeInputHover(params); + case "page.drag": + return chromeInputDrag(params); + case "page.upload": + return chromeInputUpload(params); + case "page.type": + return withOptionalSnapshot(params, chromeInputType); + case "page.fill": + return withOptionalSnapshot(params, chromeInputFill); + case "page.key": + return withOptionalSnapshot(params, chromeInputKey); + case "page.scroll": + return chromeInputScroll(params); + case "page.tap": + return chromeInputTap(params); + case "input.status": + return inputStatus(); + case "input.debug": + return inputDebug(params); + case "page.console.list": + return executeInTab(params, listConsoleMessages, [params.clear === true]); + case "page.network.list": + return executeInTab(params, listNetworkRequests, [params.includePreservedRequests === true, params.clear === true]); + case "page.network.get": + return executeInTab(params, getNetworkRequest, [params.requestId]); + case "page.waitFor": { + // Poll from the service worker via CDP (bypasses CSP). The old approach ran the polling + // loop in-page with new Function() for expression checks, which fails under strict CSP. + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + const timeoutMs = params.timeoutMs || 10000; + const intervalMs = params.intervalMs || 250; + const started = Date.now(); + while (Date.now() - started < timeoutMs) { + let ok = false; + try { + const expr = params.kind === "selector" + ? `!!document.querySelector(${JSON.stringify(params.value)})` + : params.value; + ok = Boolean(await evaluateInTab({ ...params, expression: expr, foreground: false })); + } catch { + ok = false; + } + if (ok) return { elapsedMs: Date.now() - started }; + await sleep(intervalMs); + } + throw new Error(`Timed out after ${timeoutMs}ms waiting for ${params.kind}: ${params.value}`); + } + case "page.probe": + // Lightweight capability probe for /chrome-doctor. Runs in MAIN world. + return executeInTab(params, probePage, []); + case "page.navigate": { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + if (params.initScript) { + // Register a one-shot document_start content script. We register, navigate, wait, then unregister. + await registerInitScript(tab.id, params.initScript); + } + const wait = params.waitUntilLoad !== false ? waitForTabComplete(tab.id, params.timeoutMs || 15000) : Promise.resolve(undefined); + const updated = await chrome.tabs.update(tab.id, { url: params.url }); + try { + await wait; + } finally { + if (params.initScript) await unregisterInitScript(tab.id).catch(() => undefined); + } + return await formatTab(await chrome.tabs.get(updated.id)); + } + case "page.screenshot": + return takeScreenshot(params); + case "automation.status": { + // Report this session's owned automation target (ids only). Used for diagnostics/tests. + await hydrateAutomationTargets(); + const t = automationTargets.get(sessionKeyOf(params)); + return { windowId: t?.windowId ?? null, tabId: t?.tabId ?? null }; + } + case "automation.cleanup": + // Close only THIS session's pi-chrome-owned window/tab. Never touches user tabs/windows or + // another Pi session's target. + return cleanupAutomationTarget(sessionKeyOf(params)); + default: + throw new Error(`Unknown action: ${action}`); + } +} + +async function formatTab(tab) { + return { + id: tab.id, + windowId: tab.windowId, + active: tab.active, + highlighted: tab.highlighted, + title: tab.title || "", + url: tab.url || "", + status: tab.status, + pinned: tab.pinned, + incognito: tab.incognito, + groupId: typeof tab.groupId === "number" ? tab.groupId : -1, + group: await groupRecord(tab.groupId), + }; +} + +// Resolve which Chrome tab an action targets. +// +// Explicit targeting (targetId / urlIncludes / titleIncludes) is unchanged: callers can still act +// on any existing tab, including a user tab, when they ask for it by name. Only the implicit +// "no target given" case changed — it used to grab the user's *active* tab (and page.navigate +// would then overwrite it); it now resolves to this Pi session's dedicated automation target. +// +// `createOwnedTarget` controls the implicit case: +// - true (default): create the automation target on first use. Used by every page/content +// action — page.navigate, click/type/fill/key/hover/drag/scroll/tap/upload, snapshot, +// inspect, evaluate, screenshot, waitFor, console/network list, probe. These need a live +// surface to drive, so auto-creating is correct and they no longer touch the user's tab. +// - false: do NOT create. Used by tab.activate/close/group/ungroup (tab *management*): with no +// explicit target they operate on an already-owned automation target if one exists, else +// throw asking for an explicit target — so e.g. `chrome_tab close` can never silently close +// the user's active tab the way it used to, and never spawns a throwaway tab just to close it. +async function getTabByParams(params, { createOwnedTarget = true } = {}) { + const tabs = await chrome.tabs.query({}); + let tab; + if (params.targetId !== undefined) { + const id = Number(params.targetId); + tab = await chrome.tabs.get(id).catch(() => null); + if (!tab?.id) { + // Chrome tab ids are not stable across reloads/navigations; a long session can hold a + // stale id. Surface the current tabs so the caller can re-target instead of guessing. + const listed = tabs + .filter((candidate) => candidate.id !== undefined) + .slice(0, 20) + .map((candidate) => ` ${candidate.id}${candidate.active ? " *" : ""}\t${(candidate.title || "(untitled)").slice(0, 60)}\t${candidate.url || ""}`) + .join("\n"); + throw new Error( + `No Chrome tab with id ${id} (it was likely closed or replaced). ` + + `Re-target with chrome_tab list, or pass urlIncludes/titleIncludes instead of targetId.\n` + + `Current tabs:\n${listed || " (none)"}`, + ); + } + } else if (params.urlIncludes) { + tab = tabs.find((candidate) => (candidate.url || "").includes(params.urlIncludes)); + } else if (params.titleIncludes) { + tab = tabs.find((candidate) => (candidate.title || "").includes(params.titleIncludes)); + } else { + // No explicit target: use this session's dedicated automation target instead of hijacking the + // user's active tab. This keeps human browsing and Pi automation separated — navigating here + // never replaces whatever the user currently has open. Callers that *want* a specific + // existing tab pass targetId/urlIncludes/titleIncludes above. + const sessionKey = sessionKeyOf(params); + tab = createOwnedTarget + ? await getOrCreateAutomationTarget(sessionKey, params.sessionGroupTitle) + : await resolveOwnedAutomationTarget(sessionKey); + if (!tab) { + throw new Error( + "No target tab specified and this Pi session has no automation tab yet. " + + "Pass targetId/urlIncludes/titleIncludes, or run chrome_navigate first.", + ); + } + } + if (!tab?.id) throw new Error("No matching Chrome tab found"); + const url = tab.url || ""; + if (url.startsWith("chrome://") || url.startsWith("chrome-extension://") || url.startsWith("devtools://")) { + throw new Error(`Chrome blocks extension automation on protected URL: tab=${tab.id} url=${url}`); + } + // Tabs Pi interacts with (page.* actions) join this session's group so the user can see exactly + // which tabs Pi is driving. We only adopt *ungrouped* tabs — never hijack a tab the user (or + // another Pi session) already grouped, since groupTab would otherwise rename that group. + if (params.joinSessionGroup && params.sessionGroupTitle) { + await joinSessionGroup(tab, params.sessionGroupTitle); + } + return tab; +} + +// Add an ungrouped tab to the session's tab group (reusing it by title, else creating it). +// No-op when the tab is already grouped or tabGroups is unavailable. +async function joinSessionGroup(tab, title) { + if (!chrome.tabGroups || typeof tab.id !== "number") return; + if (typeof tab.groupId === "number" && tab.groupId >= 0) return; + try { + await groupTab(tab, title); + } catch { + // Grouping is best-effort; never block the actual page action on a grouping failure. + } +} + +// Helper sources that get concatenated into the injected MAIN-world script. Kept as separate +// functions so callers below can reference them by `.toString()`. The helpers do not perform any +// eval themselves — they're plain function declarations. +const HELPER_FUNCS = [ + getPiChromeState, + rememberElement, + elementBySelectorOrUid, + installPiChromeInstrumentation, + resolvePoint, + dispatchInputEvents, + setNativeValue, + normalizeKey, + isElementVisible, + occluderAt, + pageHash, + pointerEventSequence, + sleepPage, + rand, + dispatchPointerLikeEvent, + humanMoveTo, + humanClickPoint, + usKeyLayoutForChar, + printableKeyCode, + dispatchKeyEvent, + typeCharacter, + pressKeyInPage, + scrollPage, +]; + +async function executeInTab(params, func, args) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + + // Phase 1: define the helpers and the action function as page globals via CDP + // Runtime.evaluate. This bypasses page CSP (no `eval`/`new Function`), which is the + // root cause of snapshot/click/etc silently failing on `script-src 'self'` sites. + // Each helper is a named function declaration, assigned to window. so the action + // (which references helpers by bare name) resolves them as globals at call time. + const assignments = HELPER_FUNCS.map((helper) => `window.${helper.name}=${helper.toString()}`).join(";\n"); + const actionAssign = `window.__piAction=(${func.toString()})`; + const defineRes = await cdpEval(tab.id, `(()=>{${assignments};\n${actionAssign};})()`); + if (defineRes.exceptionDetails) { + throw new Error(`Failed to inject Chrome page helpers: ${cdpExceptionText(defineRes.exceptionDetails) || "unknown error"}`); + } + + // Phase 2: run the action via chrome.scripting.executeScript. The `func:` form is + // injected by Chrome itself (not `new Function`), so it is CSP-safe, and it lets Chrome + // serialize the invocation args. The wrapper references window.__piAction defined above. + const results = await executeScriptTimed({ + target: { tabId: tab.id }, + world: "MAIN", + func: async (invocationArgs) => { + try { + return { ok: true, value: await window.__piAction(...invocationArgs) }; + } catch (error) { + return { ok: false, error: error?.stack || error?.message || String(error) }; + } + }, + args: [args || []], + }, `execute page action in tab ${tab.id}`); + const first = results?.[0]; + if (first?.error) { + const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error)); + throw new Error(message); + } + const envelope = first?.result; + if (envelope && typeof envelope === "object" && envelope.ok === false) { + throw new Error(envelope.error || "Chrome page script failed"); + } + return envelope?.value; +} + +// Serializer for page.evaluate results. Embedded (via .toString()) into the CDP-evaluated +// expression so we can return rich markers for values that don't survive returnByValue +// (undefined/function/symbol/bigint/Error), plus expand DOMRect-like objects whose fields +// are non-enumerable. Kept as a standalone function so it stays editable/lintable. +function piEvalStringify(v) { + if (v === undefined) return { kind: "undefined" }; + if (typeof v === "function") return { kind: "function", source: v.toString().slice(0, 500) }; + if (typeof v === "symbol") return { kind: "symbol", description: v.description }; + if (typeof v === "bigint") return { kind: "bigint", value: v.toString() }; + if (v instanceof Error) return { kind: "error", name: v.name, message: v.message, stack: v.stack }; + // DOMRect/DOMRectReadOnly (and getBoundingClientRect results) have non-enumerable + // properties, so JSON.stringify yields `{}`. Expand the fields explicitly. + if ((typeof DOMRectReadOnly !== "undefined" && v instanceof DOMRectReadOnly) || + (typeof DOMRect !== "undefined" && v instanceof DOMRect) || + (v && typeof v === "object" && typeof v.toJSON === "function" && + typeof v.width === "number" && typeof v.height === "number" && typeof v.top === "number")) { + return { x: v.x, y: v.y, width: v.width, height: v.height, top: v.top, right: v.right, bottom: v.bottom, left: v.left }; + } + return v; +} + +// Dedicated executor for page.evaluate. Uses CDP Runtime.evaluate (via cdpEval) which is not +// subject to the page's CSP, fixing `chrome_evaluate` silently returning null / failing on +// pages that ship `script-src 'self'` without `'unsafe-eval'` (which blocks `eval`/`new Function`). +async function evaluateInTab(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + const expression = String(params.expression ?? ""); + const stringifySrc = `(${piEvalStringify.toString()})`; + // Wrap the user expression so the result is run through piEvalStringify in-page before it + // crosses the returnByValue boundary. Try expression form first (so `1+1` / `document.title` + // work without `return`); on a SyntaxError fall back to statement form for multi-statement + // bodies (loops, var decls, etc), matching the previous new Function() two-form behavior. + const buildWrapper = (form) => `(async () => { const __s=${stringifySrc}; const __v = await ${form}; return __s(__v); })()`; + const exprForm = `(async () => (${expression}))()`; + const stmtForm = `(async () => { ${expression} })()`; + + let res = await cdpEval(tab.id, buildWrapper(exprForm)); + if (res.exceptionDetails && cdpIsSyntaxError(res.exceptionDetails)) { + res = await cdpEval(tab.id, buildWrapper(stmtForm)); + } + if (res.exceptionDetails) { + throw new Error(`chrome_evaluate failed: ${cdpExceptionText(res.exceptionDetails) || "evaluation failed"}`); + } + const result = res.result; + if (!result || result.type === "undefined") return undefined; + const v = result.value; + // Unwrap special markers produced by piEvalStringify. + if (v && typeof v === "object" && !Array.isArray(v)) { + if (v.kind === "undefined") return undefined; + if (v.kind === "function") return `[Function: ${v.source}]`; + if (v.kind === "symbol") return `[Symbol: ${v.description}]`; + if (v.kind === "bigint") return v.value; + if (v.kind === "error") throw new Error(`${v.name}: ${v.message}\n${v.stack || ""}`); + } + return v; +} + +async function withOptionalSnapshot(params, actionFn) { + const result = await actionFn(params); + if (params.includeSnapshot) { + const snapshot = await snapshotInTab({ ...params, foreground: false }); + return { result, snapshot }; + } + return result; +} + +// Snapshot/inspect run from a packaged MAIN-world script (snapshot_injected.js) injected via +// chrome.scripting.executeScript({ files }). That file is free of eval/new Function, so it works +// on strict-CSP pages, and it installs globalThis.__piChromeSnapshotPage / __piChromeInspectTarget. +// It shares window.__PI_CHROME_STATE__ (same el- uid scheme) with the CDP-injected input helpers. +async function snapshotInTab(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + const args = [ + params.maxElements || 80, + params.containingText ?? null, + params.roleFilter ?? null, + params.nearUid ?? null, + params.mode || "auto", + params.query ?? null, + params.maxTextChars ?? null, + ]; + await executeScriptTimed({ + target: { tabId: tab.id, frameIds: [0] }, + world: "MAIN", + files: ["snapshot_injected.js"], + }, `inject snapshot script in tab ${tab.id}`); + const results = await executeScriptTimed({ + target: { tabId: tab.id, frameIds: [0] }, + world: "MAIN", + func: async (invocationArgs) => { + try { + const snapshotPage = globalThis.__piChromeSnapshotPage; + if (typeof snapshotPage !== "function") throw new Error("snapshot_injected.js did not install __piChromeSnapshotPage"); + return { ok: true, value: await snapshotPage(...invocationArgs) }; + } catch (error) { + return { ok: false, error: error?.stack || error?.message || String(error) }; + } + }, + args: [args], + }, `run snapshot script in tab ${tab.id}`); + const first = results?.[0]; + if (first?.error) { + const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error)); + throw new Error(message); + } + const envelope = first?.result; + if (envelope && typeof envelope === "object" && envelope.ok === false) { + throw new Error(envelope.error || "Chrome snapshot script failed"); + } + const value = envelope?.value; + return value && typeof value === "object" ? { ...value, targetId: tab.id } : value; +} + +async function inspectInTab(params) { + if (!params.uid && !params.selector) throw new Error("chrome_inspect requires uid or selector"); + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + const args = [params.uid ?? null, params.selector ?? null, params.scrollIntoView === true]; + await executeScriptTimed({ + target: { tabId: tab.id, frameIds: [0] }, + world: "MAIN", + files: ["snapshot_injected.js"], + }, `inject inspect script in tab ${tab.id}`); + const results = await executeScriptTimed({ + target: { tabId: tab.id, frameIds: [0] }, + world: "MAIN", + func: async (invocationArgs) => { + try { + const inspectTarget = globalThis.__piChromeInspectTarget; + if (typeof inspectTarget !== "function") throw new Error("snapshot_injected.js did not install __piChromeInspectTarget"); + return { ok: true, value: await inspectTarget(...invocationArgs) }; + } catch (error) { + return { ok: false, error: error?.stack || error?.message || String(error) }; + } + }, + args: [args], + }, `run inspect script in tab ${tab.id}`); + const first = results?.[0]; + if (first?.error) { + const message = typeof first.error === "string" ? first.error : (first.error.message || JSON.stringify(first.error)); + throw new Error(message); + } + const envelope = first?.result; + if (envelope && typeof envelope === "object" && envelope.ok === false) { + throw new Error(envelope.error || "Chrome inspect script failed"); + } + return envelope?.value; +} + +// One-shot init script registry, scoped per tab. The source is registered with CDP +// Page.addScriptToEvaluateOnNewDocument, which runs it at document_start in the page's MAIN +// world and is NOT subject to page CSP (the old func:(code)=>new Function(code) path was +// blocked by `script-src 'self'`). page.navigate registers before the nav and unregisters +// after load, so only the intended navigation receives the script. +const initScriptIds = new Map(); // tabId -> CDP script identifier +async function registerInitScript(tabId, source) { + await attachDebugger(tabId); + await cdp(tabId, "Page.enable", {}).catch(() => undefined); + const result = await cdp(tabId, "Page.addScriptToEvaluateOnNewDocument", { source }); + if (result && result.identifier !== undefined) initScriptIds.set(tabId, result.identifier); +} +async function unregisterInitScript(tabId) { + const identifier = initScriptIds.get(tabId); + if (identifier === undefined) return; + initScriptIds.delete(tabId); + await cdp(tabId, "Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => undefined); +} + +// Always inject early console/network capture at document_start on every navigation. +// Catches console messages, errors, and network requests that fire during page load, +// before chrome_snapshot or chrome_evaluate install the instrumentation normally. +// The function installEarlyCapture sets __piChromeWrapped flags so the post-hoc +// installPiChromeInstrumentation() call is idempotent. +if (chrome.webNavigation && chrome.webNavigation.onCommitted) { + chrome.webNavigation.onCommitted.addListener((details) => { + if (details.frameId !== 0) return; + chrome.scripting.executeScript({ + target: { tabId: details.tabId, frameIds: [0] }, + world: "MAIN", + injectImmediately: true, + func: installEarlyCapture, + args: [], + }).catch(() => undefined); + }); +} + +async function bringToFront(tab) { + await chrome.windows.update(tab.windowId, { focused: true }); + await chrome.tabs.update(tab.id, { active: true }); +} + +function waitForTabComplete(tabId, timeoutMs) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + chrome.tabs.onUpdated.removeListener(listener); + reject(new Error(`Timed out after ${timeoutMs}ms waiting for tab ${tabId} to load`)); + }, timeoutMs); + const listener = (updatedTabId, changeInfo) => { + if (updatedTabId === tabId && changeInfo.status === "complete") { + clearTimeout(timer); + chrome.tabs.onUpdated.removeListener(listener); + resolve(true); + } + }; + chrome.tabs.onUpdated.addListener(listener); + }); +} + +async function takeScreenshot(params) { + const tab = await getTabByParams(params); + if (params.foreground) await bringToFront(tab); + let previousActiveId; + if (!tab.active) { + const activeBefore = await chrome.tabs.query({ active: true, windowId: tab.windowId }); + previousActiveId = activeBefore[0]?.id; + await chrome.tabs.update(tab.id, { active: true }); + } + try { + if (params.fullPage) { + // Tile-stitched full page capture: scroll, capture, paste, repeat. + const tiles = await executeInTab({ ...params, foreground: false }, captureFullPageTiles, []); + // captureFullPageTiles only computes scroll positions / metrics; we capture per scroll here + // (chrome.tabs.captureVisibleTab can't be called from MAIN world). + const captured = []; + for (const tile of tiles.tiles) { + await executeInTab({ ...params, foreground: false }, scrollToY, [tile.scrollY]); + // Small settle delay; many sites have on-scroll animations / lazy-load. + await sleep(120); + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { + format: params.format || "png", + quality: params.format === "jpeg" ? params.quality : undefined, + }); + captured.push({ y: tile.y, dataUrl }); + } + await executeInTab({ ...params, foreground: false }, scrollToY, [tiles.originalScrollY]); + return { + fullPage: true, + tab: await formatTab(tab), + dimensions: { width: tiles.width, height: tiles.height, viewportHeight: tiles.viewportHeight, dpr: tiles.dpr }, + tiles: captured, + }; + } + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { + format: params.format || "png", + quality: params.format === "jpeg" ? params.quality : undefined, + }); + return { dataUrl, tab: await formatTab(tab) }; + } finally { + if (previousActiveId !== undefined && previousActiveId !== tab.id) { + await chrome.tabs.update(previousActiveId, { active: true }).catch(() => undefined); + } + } +} + +// --------------------------------------------------------------------------- +// MAIN-world helpers (function declarations injected into the page). +// --------------------------------------------------------------------------- + +function getPiChromeState() { + const state = window.__PI_CHROME_STATE__ || { + nextElementUid: 1, + elements: {}, + console: [], + network: [], + nextRequestId: 1, + instrumentationInstalled: false, + }; + window.__PI_CHROME_STATE__ = state; + return state; +} + +function rememberElement(element) { + const state = getPiChromeState(); + if (!element.__piChromeUid) element.__piChromeUid = "el-" + state.nextElementUid++; + state.elements[element.__piChromeUid] = element; + return element.__piChromeUid; +} + +function elementBySelectorOrUid(selector, uid) { + if (uid) { + const element = getPiChromeState().elements[uid]; + if (!element || !element.isConnected) throw new Error(`No live element for uid: ${uid}. Take a fresh chrome_snapshot.`); + return element; + } + if (selector) { + const element = document.querySelector(selector); + if (!element) throw new Error(`No element matches selector: ${selector}`); + return element; + } + return null; +} + +function isElementVisible(element) { + if (!element || !element.getBoundingClientRect) return false; + const style = getComputedStyle(element); + if (style.visibility === "hidden" || style.display === "none") return false; + const rect = element.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return false; + if (rect.bottom < 0 || rect.right < 0) return false; + if (rect.top > innerHeight || rect.left > innerWidth) return false; + return true; +} + +function occluderAt(x, y, expected) { + const top = document.elementFromPoint(x, y); + if (!top || top === expected) return null; + if (expected && expected.contains(top)) return null; + if (top.contains(expected)) return null; + return { + tag: top.tagName.toLowerCase(), + id: top.id || undefined, + className: typeof top.className === "string" ? top.className : undefined, + }; +} + +function pageHash() { + // Cheap rolling hash used for `pageMutated`. Combines first 4kb of body innerText with the + // current values of inputs/textareas (which are not part of innerText) and the count of + // descendants of . This catches: text changes, input value edits, and DOM structure + // changes — the three things a click/type/fill might cause. + const body = document.body; + const text = (body ? body.innerText : "").slice(0, 4000); + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0; + if (body) { + const inputs = body.querySelectorAll("input,textarea,select"); + let valueBlob = ""; + for (let i = 0; i < inputs.length && valueBlob.length < 4000; i++) { + const v = inputs[i].value; + if (typeof v === "string") valueBlob += v + "\x00"; + } + for (let i = 0; i < valueBlob.length; i++) h = (h * 31 + valueBlob.charCodeAt(i)) | 0; + h = (h * 31 + body.getElementsByTagName("*").length) | 0; + } + return h; +} + +function sleepPage(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function rand(min, max) { + return min + Math.random() * (max - min); +} + +function dispatchPointerLikeEvent(element, type, x, y, prevX, prevY, opts = {}) { + const isPointer = type.startsWith("pointer"); + const Ctor = isPointer ? PointerEvent : MouseEvent; + const isMove = type === "pointermove" || type === "mousemove"; + const isUpOrClick = type === "pointerup" || type === "mouseup" || type === "click"; + const init = { + bubbles: true, + cancelable: true, + view: window, + clientX: x, + clientY: y, + screenX: x + (window.screenX || 0), + screenY: y + (window.screenY || 0), + movementX: Number.isFinite(prevX) ? x - prevX : 0, + movementY: Number.isFinite(prevY) ? y - prevY : 0, + button: 0, + buttons: isMove || isUpOrClick ? 0 : 1, + }; + if (isPointer) { + init.pointerType = "mouse"; + init.pointerId = 1; + init.isPrimary = true; + init.width = 1; + init.height = 1; + init.pressure = opts.pressure ?? (type === "pointerdown" ? 0.5 : 0); + init.tangentialPressure = 0; + init.tiltX = 0; + init.tiltY = 0; + } + const ev = new Ctor(type, init); + element.dispatchEvent(ev); + return ev.defaultPrevented; +} + +function pointerEventSequence(element, x, y, sequence) { + let defaultPrevented = false; + const state = getPiChromeState(); + const prevX = state.pointer?.x; + const prevY = state.pointer?.y; + for (const type of sequence) { + defaultPrevented = dispatchPointerLikeEvent(element, type, x, y, prevX, prevY) || defaultPrevented; + } + state.pointer = { x, y, t: performance.now() }; + return defaultPrevented; +} + +async function humanMoveTo(x, y, steps) { + const state = getPiChromeState(); + const startX = Number.isFinite(state.pointer?.x) ? state.pointer.x : rand(12, Math.max(24, innerWidth - 12)); + const startY = Number.isFinite(state.pointer?.y) ? state.pointer.y : rand(12, Math.max(24, innerHeight - 12)); + const n = steps || Math.max(12, Math.min(42, Math.round(Math.hypot(x - startX, y - startY) / 18))); + let prevX = startX, prevY = startY; + let defaultPrevented = false; + for (let i = 1; i <= n; i++) { + const t = i / n; + const ease = t * t * (3 - 2 * t); + const wobble = Math.sin(t * Math.PI) * 8; + const px = startX + (x - startX) * ease + rand(-wobble, wobble); + const py = startY + (y - startY) * ease + rand(-wobble, wobble); + const el = document.elementFromPoint(px, py) || document.body || document.documentElement; + defaultPrevented = dispatchPointerLikeEvent(el, "pointermove", px, py, prevX, prevY) || defaultPrevented; + defaultPrevented = dispatchPointerLikeEvent(el, "mousemove", px, py, prevX, prevY) || defaultPrevented; + prevX = px; prevY = py; + await sleepPage(rand(4, 18)); + } + state.pointer = { x, y, t: performance.now() }; + return defaultPrevented; +} + +function humanClickPoint(point) { + if (!point.rect) return { x: point.x, y: point.y }; + const rect = point.rect; + const insetX = Math.min(rect.width * 0.35, Math.max(2, rect.width / 2 - 1)); + const insetY = Math.min(rect.height * 0.35, Math.max(2, rect.height / 2 - 1)); + return { + x: rect.left + rect.width / 2 + rand(-insetX, insetX), + y: rect.top + rect.height / 2 + rand(-insetY, insetY), + }; +} + +function installPiChromeInstrumentation() { + const state = getPiChromeState(); + if (state.instrumentationInstalled) return; + state.instrumentationInstalled = true; + const pushConsole = (level, args) => { + state.console.push({ + id: state.console.length + 1, + level, + timestamp: Date.now(), + url: location.href, + args: Array.from(args).map((arg) => { + try { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack }; + return JSON.parse(JSON.stringify(arg)); + } catch { + return String(arg); + } + }), + }); + if (state.console.length > 500) state.console.splice(0, state.console.length - 500); + }; + for (const level of ["debug", "log", "info", "warn", "error"]){ + const original = console[level]; + if (typeof original !== "function" || original.__piChromeWrapped) continue; + const wrapped = function(...args) { + pushConsole(level, args); + return original.apply(this, args); + }; + wrapped.__piChromeWrapped = true; + console[level] = wrapped; + } + window.addEventListener("error", (event) => pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno])); + window.addEventListener("unhandledrejection", (event) => pushConsole("unhandledrejection", [event.reason])); + + const trimBody = (text) => typeof text === "string" && text.length > 200000 ? text.slice(0, 200000) + `\n[truncated ${text.length - 200000} chars]` : text; + const record = (entry) => { + state.network.push(entry); + if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000); + return entry; + }; + if (window.fetch && !window.fetch.__piChromeWrapped) { + const originalFetch = window.fetch.bind(window); + const wrappedFetch = async (...args) => { + const id = "req-" + state.nextRequestId++; + const startedAt = Date.now(); + const input = args[0]; + const init = args[1] || {}; + const url = typeof input === "string" ? input : input?.url; + const method = (init.method || input?.method || "GET").toUpperCase(); + const entry = record({ id, type: "fetch", method, url: String(url || ""), startedAt, pageUrl: location.href, status: "pending" }); + try { + const response = await originalFetch(...args); + entry.status = response.status; + entry.statusText = response.statusText; + entry.ok = response.ok; + entry.responseUrl = response.url; + entry.durationMs = Date.now() - startedAt; + entry.responseHeaders = Array.from(response.headers.entries()); + response.clone().text().then((text) => { + entry.responseBody = trimBody(text); + entry.responseBodyTruncated = typeof text === "string" && text.length > 200000; + }).catch((error) => { entry.responseBodyError = error?.message || String(error); }); + return response; + } catch (error) { + entry.error = error?.message || String(error); + entry.durationMs = Date.now() - startedAt; + throw error; + } + }; + wrappedFetch.__piChromeWrapped = true; + window.fetch = wrappedFetch; + } + if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) { + const originalOpen = XMLHttpRequest.prototype.open; + const originalSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") }; + return originalOpen.call(this, method, url, ...rest); + }; + XMLHttpRequest.prototype.open.__piChromeWrapped = true; + XMLHttpRequest.prototype.send = function(body) { + const id = "req-" + state.nextRequestId++; + const startedAt = Date.now(); + const info = this.__piChromeRequest || {}; + const entry = record({ id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt, pageUrl: location.href, status: "pending" }); + this.addEventListener("loadend", () => { + entry.status = this.status; + entry.statusText = this.statusText; + entry.responseUrl = this.responseURL; + entry.durationMs = Date.now() - startedAt; + try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch {} + try { + if (typeof this.responseText === "string") { + entry.responseBody = trimBody(this.responseText); + entry.responseBodyTruncated = this.responseText.length > 200000; + } + } catch (error) { entry.responseBodyError = error?.message || String(error); } + }); + this.addEventListener("error", () => { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; }); + return originalSend.call(this, body); + }; + } +} + +// Early-capture version of installPiChromeInstrumentation, designed to be injected +// at document_start via webNavigation.onCommitted. Wraps console, fetch, and XHR +// before the page's own JavaScript runs, so page-load errors are captured. +// Sets __piChromeWrapped flags so the post-hoc installPiChromeInstrumentation() +// sees them and skips (idempotent). +// NOTE: This function is self-contained — it does NOT close over any outer scope +// because it gets serialized by chrome.scripting.executeScript({func: ...}). +function installEarlyCapture() { + if (window.__piChromeEarlyCaptureInstalled) return; + window.__piChromeEarlyCaptureInstalled = true; + var state = window.__PI_CHROME_STATE__; + if (!state) { + state = { + nextElementUid: 1, + elements: {}, + console: [], + network: [], + nextRequestId: 1, + instrumentationInstalled: false, + }; + window.__PI_CHROME_STATE__ = state; + } + function pushConsole(level, args) { + state.console.push({ + id: state.console.length + 1, + level: level, + timestamp: Date.now(), + url: location.href, + args: Array.from(args).map(function(arg) { + try { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack }; + return JSON.parse(JSON.stringify(arg)); + } catch (e) { + return String(arg); + } + }), + }); + if (state.console.length > 500) state.console.splice(0, state.console.length - 500); + } + for (var i = 0; i < 5; i++) { + var levels = ["debug", "log", "info", "warn", "error"]; + var level = levels[i]; + var original = console[level]; + if (typeof original !== "function" || original.__piChromeWrapped) continue; + var wrapped = function(lvl, orig) { + return function() { + pushConsole(lvl, arguments); + return orig.apply(this, arguments); + }; + }(level, original); + wrapped.__piChromeWrapped = true; + console[level] = wrapped; + } + window.addEventListener("error", function(event) { + pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno]); + }); + window.addEventListener("unhandledrejection", function(event) { + pushConsole("unhandledrejection", [event.reason]); + }); + var trimBody = function(text) { + return typeof text === "string" && text.length > 200000 ? text.slice(0, 200000) + "\n[truncated " + (text.length - 200000) + " chars]" : text; + }; + var record = function(entry) { + state.network.push(entry); + if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000); + return entry; + }; + if (window.fetch && !window.fetch.__piChromeWrapped) { + var originalFetch = window.fetch.bind(window); + var wrappedFetch = async function() { + var args = []; + for (var k = 0; k < arguments.length; k++) args.push(arguments[k]); + var id = "req-" + state.nextRequestId++; + var startedAt = Date.now(); + var input = args[0]; + var init = args[1] || {}; + var url = typeof input === "string" ? input : (input ? input.url : ""); + var method = (init.method || (input ? input.method : null) || "GET").toUpperCase(); + var entry = record({ id: id, type: "fetch", method: method, url: String(url || ""), startedAt: startedAt, pageUrl: location.href, status: "pending" }); + try { + var response = await originalFetch.apply(window, args); + entry.status = response.status; + entry.statusText = response.statusText; + entry.ok = response.ok; + entry.responseUrl = response.url; + entry.durationMs = Date.now() - startedAt; + entry.responseHeaders = Array.from(response.headers.entries()); + response.clone().text().then(function(text) { + entry.responseBody = trimBody(text); + entry.responseBodyTruncated = typeof text === "string" && text.length > 200000; + }).catch(function(error) { entry.responseBodyError = error ? error.message : String(error); }); + return response; + } catch (error) { + entry.error = error ? error.message : String(error); + entry.durationMs = Date.now() - startedAt; + throw error; + } + }; + wrappedFetch.__piChromeWrapped = true; + window.fetch = wrappedFetch; + } + if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) { + var originalOpen = XMLHttpRequest.prototype.open; + var originalSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function(method, url) { + this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") }; + return originalOpen.apply(this, arguments); + }; + XMLHttpRequest.prototype.open.__piChromeWrapped = true; + XMLHttpRequest.prototype.send = function(body) { + var id = "req-" + state.nextRequestId++; + var startedAt = Date.now(); + var info = this.__piChromeRequest || {}; + var entry = record({ id: id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt: startedAt, pageUrl: location.href, status: "pending" }); + this.addEventListener("loadend", function() { + entry.status = this.status; + entry.statusText = this.statusText; + entry.responseUrl = this.responseURL; + entry.durationMs = Date.now() - startedAt; + try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch (e) {} + try { + if (typeof this.responseText === "string") { + entry.responseBody = trimBody(this.responseText); + entry.responseBodyTruncated = this.responseText.length > 200000; + } + } catch (error) { entry.responseBodyError = error ? error.message : String(error); } + }); + this.addEventListener("error", function() { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; }); + return originalSend.apply(this, arguments); + }; + } + state.instrumentationInstalled = true; +} + +function probePage() { + // Sanity probe used by /chrome-doctor. Returns evidence that MAIN-world execution works. + return { + arithmetic: 1 + 1, + location: location.href, + title: document.title, + documentReady: document.readyState, + userAgent: navigator.userAgent.slice(0, 200), + webdriver: !!navigator.webdriver, + }; +} + +function captureFullPageTiles() { + // Returns the *plan* for tile capture; the actual chrome.tabs.captureVisibleTab calls happen + // in the SW. We just report the scroll positions and metrics. + const html = document.documentElement; + const body = document.body; + const width = Math.max(html.scrollWidth, body ? body.scrollWidth : 0, innerWidth); + const height = Math.max(html.scrollHeight, body ? body.scrollHeight : 0, innerHeight); + const viewportHeight = innerHeight; + const dpr = window.devicePixelRatio || 1; + const originalScrollY = scrollY; + const tiles = []; + let y = 0; + while (y < height) { + tiles.push({ y, scrollY: y }); + y += viewportHeight; + } + return { width, height, viewportHeight, dpr, originalScrollY, tiles }; +} + +function scrollToY(y) { + window.scrollTo({ top: y, left: 0, behavior: "instant" }); + return { scrollY }; +} + +function resolvePoint(selector, uid, x, y) { + const element = elementBySelectorOrUid(selector, uid); + if (element) { + element.scrollIntoView({ block: "center", inline: "center", behavior: "instant" }); + const rect = element.getBoundingClientRect(); + return { element, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, rect }; + } + if (typeof x !== "number" || typeof y !== "number") throw new Error("Provide selector, uid, or x/y"); + return { element: document.elementFromPoint(x, y), x, y, rect: undefined }; +} + +async function clickPage(selector, uid, x, y) { + installPiChromeInstrumentation(); + const before = pageHash(); + const point = resolvePoint(selector, uid, x, y); + if (!point.element) throw new Error("No element at click point"); + const clickPoint = humanClickPoint(point); + point.x = clickPoint.x; + point.y = clickPoint.y; + point.element = document.elementFromPoint(point.x, point.y) || point.element; + const visible = isElementVisible(point.element); + const occluded = occluderAt(point.x, point.y, point.element); + let defaultPrevented = await humanMoveTo(point.x, point.y); + const state = getPiChromeState(); + const prevX = state.pointer?.x; + const prevY = state.pointer?.y; + defaultPrevented = dispatchPointerLikeEvent(point.element, "pointerdown", point.x, point.y, prevX, prevY, { pressure: 0.5 }) || defaultPrevented; + defaultPrevented = dispatchPointerLikeEvent(point.element, "mousedown", point.x, point.y, prevX, prevY) || defaultPrevented; + if (typeof point.element.focus === "function" && /^(A|BUTTON|INPUT|TEXTAREA|SELECT|SUMMARY)$/.test(point.element.tagName)) { + try { point.element.focus({ preventScroll: true }); } catch { try { point.element.focus(); } catch {} } + } + await sleepPage(rand(45, 140)); + defaultPrevented = dispatchPointerLikeEvent(point.element, "pointerup", point.x, point.y, prevX, prevY) || defaultPrevented; + defaultPrevented = dispatchPointerLikeEvent(point.element, "mouseup", point.x, point.y, prevX, prevY) || defaultPrevented; + defaultPrevented = dispatchPointerLikeEvent(point.element, "click", point.x, point.y, prevX, prevY) || defaultPrevented; + state.pointer = { x: point.x, y: point.y, t: performance.now() }; + // Heuristic: if the clicked thing looks like a media play affordance and the page has paused + // audio/video, the DOM-event click may not unlock autoplay. Surface a warning. + let autoplayHint; + const labelRaw = (point.element.getAttribute("aria-label") || point.element.textContent || "").trim(); + const label = labelRaw.toLowerCase(); + if (/^(play|start|begin|next|continue|unmute)/.test(label)) { + const idleMedia = Array.from(document.querySelectorAll("audio,video")).some((m) => m.paused); + if (idleMedia) autoplayHint = "This element looks like a media affordance and the page has paused media. DOM-event clicks do not satisfy user-activation gates; audio/video may not start."; + } + const pageMutated = pageHash() !== before; + // Smart-auto retry hint: only set when DOM-event path produced no observable change AND the + // element looks gated, OR the page just emitted a user-activation rejection. The dispatcher + // uses this to decide whether to retry with Chrome input. + let suggestChromeInput = false; + let suggestReason; + if (!pageMutated) { + if (autoplayHint) { suggestChromeInput = true; suggestReason = "play/media affordance + idle media"; } + else if (/copy(\s|$)|paste|share|download|fullscreen|sign in with|continue with|allow|enable/i.test(label)) { + suggestChromeInput = true; suggestReason = `label '${labelRaw.slice(0, 40)}' looks gated`; + } else { + // Inspect recent console errors for activation-gate rejections. + const recent = (state.console || []).slice(-8); + const hit = recent.find((e) => /NotAllowedError|Document is not focused|requires transient activation|gesture is required/.test( + (e.args || []).map((a) => typeof a === "string" ? a : (a && a.message) || JSON.stringify(a)).join(" ") + )); + if (hit) { suggestChromeInput = true; suggestReason = "recent console error indicates user-activation gate"; } + } + } + return { + x: point.x, + y: point.y, + selector, + uid, + tag: point.element.tagName, + label: labelRaw.slice(0, 80) || undefined, + input: "dom", + defaultPrevented, + elementVisible: visible, + occludedBy: occluded || undefined, + pageMutated, + autoplayHint, + suggestChromeInput: suggestChromeInput || undefined, + suggestReason, + }; +} + +async function hoverPage(selector, uid, x, y) { + installPiChromeInstrumentation(); + const point = resolvePoint(selector, uid, x, y); + if (!point.element) throw new Error("No element to hover"); + await humanMoveTo(point.x, point.y); + const state = getPiChromeState(); + const prevX = state.pointer?.x, prevY = state.pointer?.y; + let defaultPrevented = false; + for (const type of ["pointerover", "mouseover", "pointerenter", "mouseenter"]) { + defaultPrevented = dispatchPointerLikeEvent(point.element, type, point.x, point.y, prevX, prevY) || defaultPrevented; + } + // Small dwell so hover-intent handlers fire. + await sleepPage(rand(80, 220)); + return { x: point.x, y: point.y, selector, uid, tag: point.element.tagName, defaultPrevented, input: "dom" }; +} + +async function dragPage(fromUid, fromSelector, fromX, fromY, toUid, toSelector, toX, toY, steps) { + installPiChromeInstrumentation(); + const before = pageHash(); + const from = resolvePoint(fromSelector, fromUid, fromX, fromY); + const to = resolvePoint(toSelector, toUid, toX, toY); + if (!from.element) throw new Error("Drag source element not found"); + if (!to.element) throw new Error("Drag target element not found"); + // Move to source. + await humanMoveTo(from.x, from.y); + const state = getPiChromeState(); + let prevX = state.pointer?.x, prevY = state.pointer?.y; + // Build a shared DataTransfer so HTML5 drag-and-drop handlers can populate / read it. + const dt = new DataTransfer(); + const dragInit = (type, target, x, y) => { + const ev = new DragEvent(type, { + bubbles: true, cancelable: true, composed: true, + clientX: x, clientY: y, + screenX: x + (window.screenX || 0), screenY: y + (window.screenY || 0), + button: 0, buttons: 1, view: window, + dataTransfer: dt, + }); + target.dispatchEvent(ev); + return ev; + }; + dispatchPointerLikeEvent(from.element, "pointerover", from.x, from.y, prevX, prevY); + dispatchPointerLikeEvent(from.element, "pointerdown", from.x, from.y, prevX, prevY, { pressure: 0.5 }); + dispatchPointerLikeEvent(from.element, "mousedown", from.x, from.y, prevX, prevY); + await sleepPage(rand(40, 110)); + dragInit("dragstart", from.element, from.x, from.y); + dragInit("drag", from.element, from.x, from.y); + let lastOver = from.element; + const n = steps || 18; + for (let i = 1; i <= n; i++) { + const t = i / n; + const ease = t * t * (3 - 2 * t); + const wobble = Math.sin(t * Math.PI) * 6; + const x = from.x + (to.x - from.x) * ease + rand(-wobble, wobble); + const y = from.y + (to.y - from.y) * ease + rand(-wobble, wobble); + const overEl = document.elementFromPoint(x, y) || to.element; + dispatchPointerLikeEvent(overEl, "pointermove", x, y, prevX, prevY); + dispatchPointerLikeEvent(overEl, "mousemove", x, y, prevX, prevY); + if (overEl !== lastOver) { + dragInit("dragleave", lastOver, x, y); + dragInit("dragenter", overEl, x, y); + lastOver = overEl; + } + dragInit("dragover", overEl, x, y); + dragInit("drag", from.element, x, y); + prevX = x; prevY = y; + await sleepPage(rand(8, 26)); + } + dispatchPointerLikeEvent(to.element, "pointerover", to.x, to.y, prevX, prevY); + dispatchPointerLikeEvent(to.element, "mouseover", to.x, to.y, prevX, prevY); + dragInit("drop", to.element, to.x, to.y); + dragInit("dragend", from.element, to.x, to.y); + dispatchPointerLikeEvent(to.element, "pointerup", to.x, to.y, prevX, prevY); + dispatchPointerLikeEvent(to.element, "mouseup", to.x, to.y, prevX, prevY); + state.pointer = { x: to.x, y: to.y, t: performance.now() }; + return { + from: { x: from.x, y: from.y }, + to: { x: to.x, y: to.y }, + steps: n, + pageMutated: pageHash() !== before, + note: "DOM-event drag with HTML5 DragEvent + shared DataTransfer.", + }; +} + +async function scrollPage(selector, uid, deltaY, deltaX, steps) { + installPiChromeInstrumentation(); + const before = pageHash(); + let target; + if (selector || uid) { + target = elementBySelectorOrUid(selector, uid); + } else { + target = document.scrollingElement || document.documentElement || document.body; + } + if (!target) throw new Error("No scroll target"); + const rect = target.getBoundingClientRect ? target.getBoundingClientRect() : { left: 0, top: 0, width: innerWidth, height: innerHeight }; + const cx = Math.max(0, Math.min(innerWidth - 1, rect.left + Math.min(rect.width, innerWidth) / 2)); + const cy = Math.max(0, Math.min(innerHeight - 1, rect.top + Math.min(rect.height, innerHeight) / 2)); + const n = Math.max(3, Math.min(40, steps || Math.max(3, Math.ceil(Math.abs(deltaY || 0) / 100)))); + // Front-loaded wheel deltas, momentum-style. + const totalY = deltaY || 0; + const totalX = deltaX || 0; + const weights = []; + for (let i = 1; i <= n; i++) weights.push(1 / i); + const sumW = weights.reduce((a, b) => a + b, 0); + let movedY = 0, movedX = 0; + for (let i = 0; i < n; i++) { + const dy = totalY * (weights[i] / sumW); + const dx = totalX * (weights[i] / sumW); + const ev = new WheelEvent("wheel", { + bubbles: true, cancelable: true, composed: true, view: window, + clientX: cx, clientY: cy, + deltaX: dx, deltaY: dy, deltaMode: 0, + }); + target.dispatchEvent(ev); + if (!ev.defaultPrevented) { + // Apply scroll ourselves; mirrors what the browser would do. + if (target === document.scrollingElement || target === document.documentElement || target === document.body) { + window.scrollBy({ left: dx, top: dy, behavior: "instant" }); + } else { + target.scrollTop += dy; + target.scrollLeft += dx; + } + } + movedY += dy; movedX += dx; + await sleepPage(rand(12, 28)); + } + return { + deltaX: movedX, deltaY: movedY, steps: n, + scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, + pageMutated: pageHash() !== before, + input: "dom", + }; +} + +function uploadFiles(selector, uid, files) { + installPiChromeInstrumentation(); + const element = elementBySelectorOrUid(selector, uid); + if (!element || element.tagName !== "INPUT" || element.type !== "file") { + throw new Error("Target must be "); + } + const dt = new DataTransfer(); + for (const f of files) { + const bytes = Uint8Array.from(atob(f.base64 || ""), (c) => c.charCodeAt(0)); + dt.items.add(new File([bytes], f.name, { type: f.type || "application/octet-stream" })); + } + element.files = dt.files; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + return { uploaded: files.map((f) => ({ name: f.name, type: f.type, size: (f.base64 || "").length })) }; +} + +function dispatchInputEvents(element, data, inputType = "insertText") { + element.dispatchEvent(new InputEvent("beforeinput", { bubbles: true, cancelable: true, inputType, data })); + element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType, data })); + element.dispatchEvent(new Event("change", { bubbles: true })); +} + +function setNativeValue(element, value) { + const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; + const descriptor = Object.getOwnPropertyDescriptor(prototype, "value"); + if (descriptor?.set) descriptor.set.call(element, value); + else element.value = value; +} + +function printableKeyCode(ch) { + return ch.length === 1 ? usKeyLayoutForChar(ch).keyCode : 0; +} + +function dispatchKeyEvent(element, type, key, mods = {}) { + const SPECIAL = { Enter: 13, Tab: 9, Backspace: 8, Delete: 46, Escape: 27, + ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, " ": 32, Shift: 16, Control: 17, Alt: 18, Meta: 91 }; + const code = key.length === 1 ? usKeyLayoutForChar(key).code : (key === " " ? "Space" : key); + const keyCode = key.length === 1 ? printableKeyCode(key) : (SPECIAL[key] ?? 0); + const ev = new KeyboardEvent(type, { + key, + code, + keyCode, + which: keyCode, + charCode: type === "keypress" && key.length === 1 ? key.charCodeAt(0) : 0, + shiftKey: !!mods.shiftKey, + ctrlKey: !!mods.ctrlKey, + altKey: !!mods.altKey, + metaKey: !!mods.metaKey, + bubbles: true, + cancelable: true, + composed: true, + view: window, + }); + element.dispatchEvent(ev); + return ev; +} + +async function typeCharacter(element, ch) { + const needShift = ch.length === 1 && (/^[A-Z]$/.test(ch) || "~!@#$%^&*()_+{}|:\"<>?".includes(ch)); + if (needShift) { + dispatchKeyEvent(element, "keydown", "Shift", { shiftKey: true }); + await sleepPage(rand(8, 24)); + } + const mods = { shiftKey: needShift }; + const down = dispatchKeyEvent(element, "keydown", ch, mods); + if (down.defaultPrevented) { + if (needShift) dispatchKeyEvent(element, "keyup", "Shift", { shiftKey: false }); + return { defaultPrevented: true }; + } + if (ch.length === 1) dispatchKeyEvent(element, "keypress", ch, mods); + + if (element.isContentEditable) { + // execCommand("insertText") fires its own beforeinput + input. Don't double-dispatch. + document.execCommand("insertText", false, ch); + } else if ("value" in element) { + const start = element.selectionStart ?? element.value.length; + const end = element.selectionEnd ?? element.value.length; + const next = element.value.slice(0, start) + ch + element.value.slice(end); + const before = new InputEvent("beforeinput", { bubbles: true, cancelable: true, inputType: "insertText", data: ch }); + element.dispatchEvent(before); + if (!before.defaultPrevented) { + setNativeValue(element, next); + try { element.selectionStart = element.selectionEnd = start + ch.length; } catch {} + element.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: ch })); + } + } else { + throw new Error("Focused element is not text-editable"); + } + + await sleepPage(rand(25, 95)); + dispatchKeyEvent(element, "keyup", ch, mods); + if (needShift) { + await sleepPage(rand(5, 18)); + dispatchKeyEvent(element, "keyup", "Shift", { shiftKey: false }); + } + await sleepPage(rand(35, 140)); + return { defaultPrevented: false }; +} + +async function typeIntoPage(selector, uid, text, pressEnter) { + installPiChromeInstrumentation(); + const before = pageHash(); + let element = elementBySelectorOrUid(selector, uid) || document.activeElement; + if (!element) throw new Error(selector || uid ? `No element for ${selector || uid}` : "No active element"); + const initialValue = "value" in element ? element.value : (element.isContentEditable ? element.textContent : null); + element.focus(); + if (!(element.isContentEditable || "value" in element)) throw new Error("Focused element is not text-editable"); + for (const ch of Array.from(text)) await typeCharacter(element, ch); + if (pressEnter) await pressKeyInPage("Enter"); + const finalValue = "value" in element ? element.value : element.textContent; + const valueMatches = "value" in element ? element.value.includes(text) : (element.textContent || "").includes(text); + const pageMutated = pageHash() !== before; + // Smart-auto retry hint when typing didn't land at all (e.g., editor blocks DOM-event input). + let suggestChromeInput = false, suggestReason; + if (text.length > 0 && initialValue === finalValue) { + suggestChromeInput = true; + suggestReason = "value did not change — editor likely rejects DOM-event input"; + } + return { + selector, uid, length: text.length, pressEnter, + input: "dom", + valueMatches, + pageMutated, + suggestChromeInput: suggestChromeInput || undefined, + suggestReason, + }; +} + +async function fillPage(selector, uid, text, submit) { + installPiChromeInstrumentation(); + const before = pageHash(); + let element = elementBySelectorOrUid(selector, uid) || document.activeElement; + if (!element) throw new Error(selector || uid ? `No element for ${selector || uid}` : "No active element"); + element.focus(); + if (element.isContentEditable) { + element.textContent = ""; + document.execCommand("insertText", false, text); + } else if ("value" in element) { + setNativeValue(element, text); + const length = String(text).length; + try { element.selectionStart = element.selectionEnd = length; } catch {} + dispatchInputEvents(element, text, "insertReplacementText"); + } else { + throw new Error("Focused element is not text-editable"); + } + if (submit) await pressKeyInPage("Enter"); + return { + selector, uid, length: String(text).length, submit, + input: "dom", + valueMatches: "value" in element ? element.value === String(text) : undefined, + pageMutated: pageHash() !== before, + }; +} + +async function pressKeyInPage(key) { + const normalized = normalizeKey(key); + const target = document.activeElement || document.body; + const before = pageHash(); + const down = dispatchKeyEvent(target, "keydown", normalized); + if (normalized.length === 1) dispatchKeyEvent(target, "keypress", normalized); + // Character insertion for printable keys when focus is in an editable. + if (normalized.length === 1 && !down.defaultPrevented && (target.isContentEditable || ("value" in target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")))) { + if (target.isContentEditable) { + const bi = new InputEvent("beforeinput", { bubbles: true, cancelable: true, inputType: "insertText", data: normalized }); + target.dispatchEvent(bi); + if (!bi.defaultPrevented) { + document.execCommand("insertText", false, normalized); + target.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: normalized })); + } + } else { + const start = target.selectionStart ?? target.value.length; + const end = target.selectionEnd ?? target.value.length; + const bi = new InputEvent("beforeinput", { bubbles: true, cancelable: true, inputType: "insertText", data: normalized }); + target.dispatchEvent(bi); + if (!bi.defaultPrevented) { + setNativeValue(target, target.value.slice(0, start) + normalized + target.value.slice(end)); + try { target.selectionStart = target.selectionEnd = start + 1; } catch {} + target.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: normalized })); + } + } + } else if (normalized === "Backspace" && "value" in target) { + const start = target.selectionStart ?? target.value.length; + const end = target.selectionEnd ?? target.value.length; + if (start > 0 || end > start) { + const from = start === end ? start - 1 : start; + const bi = new InputEvent("beforeinput", { bubbles: true, cancelable: true, inputType: "deleteContentBackward" }); + target.dispatchEvent(bi); + if (!bi.defaultPrevented) { + setNativeValue(target, target.value.slice(0, from) + target.value.slice(end)); + try { target.selectionStart = target.selectionEnd = from; } catch {} + target.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "deleteContentBackward" })); + } + } + } + await sleepPage(rand(25, 95)); + const up = dispatchKeyEvent(target, "keyup", normalized); + if (normalized === "Enter") { + const form = target.closest?.("form"); + if (form) form.requestSubmit?.(); + } + return { + key: normalized, + input: "dom", + defaultPrevented: down.defaultPrevented || up.defaultPrevented, + pageMutated: pageHash() !== before, + }; +} + +function listConsoleMessages(clear) { + installPiChromeInstrumentation(); + const state = getPiChromeState(); + const messages = state.console.slice(); + if (clear) state.console = []; + return { messages, count: messages.length }; +} + +function listNetworkRequests(includePreservedRequests, clear) { + installPiChromeInstrumentation(); + const state = getPiChromeState(); + const currentUrl = location.href; + const requests = state.network + .filter((request) => includePreservedRequests || request.pageUrl === currentUrl) + .map(({ responseBody, ...summary }) => ({ ...summary, hasResponseBody: responseBody !== undefined })); + if (clear) state.network = []; + return { requests, count: requests.length, note: "Captures fetch/XHR after instrumentation is installed. Browser-initiated document/static asset requests are not captured." }; +} + +function getNetworkRequest(requestId) { + installPiChromeInstrumentation(); + const request = getPiChromeState().network.find((entry) => entry.id === requestId); + if (!request) throw new Error(`No network request with id ${requestId}`); + return request; +} + +function normalizeKey(key) { + const table = { + enter: "Enter", + escape: "Escape", + tab: "Tab", + backspace: "Backspace", + delete: "Delete", + arrowup: "ArrowUp", + arrowdown: "ArrowDown", + arrowleft: "ArrowLeft", + arrowright: "ArrowRight", + }; + return table[String(key).toLowerCase()] || key; +} diff --git a/pi-chrome/extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js new file mode 100644 index 0000000..ceee8ce --- /dev/null +++ b/pi-chrome/extensions/chrome-profile-bridge/browser-extension/snapshot_injected.js @@ -0,0 +1,677 @@ +// Static MAIN-world snapshot implementation injected by the MV3 service worker. +// Keep this file free of eval/new Function so `chrome_snapshot` works on strict-CSP pages. +(() => { + function getPiChromeState() { + const state = window.__PI_CHROME_STATE__ || { + nextElementUid: 1, + elements: {}, + console: [], + network: [], + nextRequestId: 1, + instrumentationInstalled: false, + lastSnapshotDigest: null, + }; + window.__PI_CHROME_STATE__ = state; + return state; + } + + function rememberElement(element) { + const state = getPiChromeState(); + if (!element.__piChromeUid) element.__piChromeUid = "el-" + state.nextElementUid++; + state.elements[element.__piChromeUid] = element; + return element.__piChromeUid; + } + + function isElementVisible(element) { + if (!element || !element.getBoundingClientRect) return false; + const style = getComputedStyle(element); + if (style.visibility === "hidden" || style.display === "none") return false; + const rect = element.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return false; + if (rect.bottom < 0 || rect.right < 0) return false; + if (rect.top > innerHeight || rect.left > innerWidth) return false; + return true; + } + + function occluderAt(x, y, expected) { + const top = document.elementFromPoint(x, y); + if (!top || top === expected) return null; + if (expected && expected.contains(top)) return null; + if (top.contains(expected)) return null; + return { + tag: top.tagName.toLowerCase(), + id: top.id || undefined, + className: typeof top.className === "string" ? top.className : undefined, + }; + } + + function textOf(element, max) { + return (element?.innerText || element?.textContent || "").replace(/\s+/g, " ").trim().slice(0, max || 500); + } + + function accessibleLabel(element) { + if (!element) return ""; + const labelledBy = element.getAttribute("aria-labelledby"); + if (labelledBy) { + const text = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.innerText || "").join(" ").trim(); + if (text) return text; + } + const id = element.id; + if (id) { + try { + const label = document.querySelector(`label[for="${cssEscape(id)}"]`); + if (label?.innerText) return label.innerText; + } catch {} + } + const wrappingLabel = element.closest?.("label"); + return ( + element.getAttribute("aria-label") || + element.getAttribute("title") || + element.getAttribute("placeholder") || + wrappingLabel?.innerText || + element.innerText || + element.textContent || + "" + ).trim().replace(/\s+/g, " ").slice(0, 180); + } + + function cssEscape(value) { + return (window.CSS && CSS.escape) ? CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&"); + } + + function roleOf(element) { + const explicit = element.getAttribute("role"); + if (explicit) return explicit.toLowerCase(); + const tag = element.tagName.toLowerCase(); + const type = (element.getAttribute("type") || "").toLowerCase(); + if (tag === "a" && element.href) return "link"; + if (tag === "button" || type === "button" || type === "submit" || type === "reset") return "button"; + if (tag === "textarea") return "textbox"; + if (tag === "select") return "combobox"; + if (tag === "input") { + if (["checkbox", "radio", "range", "search", "email", "password", "tel", "url", "number"].includes(type)) return type === "checkbox" || type === "radio" || type === "range" ? type : "textbox"; + return "textbox"; + } + if (element.isContentEditable) return "textbox"; + if (tag.match(/^h[1-6]$/)) return "heading"; + return tag; + } + + function isSensitiveField(element) { + if (!element) return false; + const tag = element.tagName?.toLowerCase?.() || ""; + if (!/^(input|textarea|select)$/.test(tag) && !element.isContentEditable) return false; + const type = (element.getAttribute("type") || "").toLowerCase(); + if (["password"].includes(type)) return true; + const haystack = [ + type, + element.getAttribute("name"), + element.id, + element.getAttribute("autocomplete"), + element.getAttribute("aria-label"), + element.getAttribute("placeholder"), + element.getAttribute("data-testid"), + ].filter(Boolean).join(" ").toLowerCase(); + return /password|passwd|\bpwd\b|secret|token|bearer|api[-_ ]?key|access[-_ ]?key|auth[-_ ]?code|one[-_ ]?time|otp|2fa|mfa|verification[-_ ]?code|recovery[-_ ]?code|credit[-_ ]?card|card[-_ ]?number|cc-number|cc-csc|cvc|cvv|security[-_ ]?code|ssn|social[-_ ]?security/.test(haystack); + } + + function installPiChromeInstrumentation() { + const state = getPiChromeState(); + if (state.instrumentationInstalled) return; + state.instrumentationInstalled = true; + const pushConsole = (level, args) => { + state.console.push({ + id: state.console.length + 1, + level, + timestamp: Date.now(), + url: location.href, + args: Array.from(args).map((arg) => { + try { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack }; + return JSON.parse(JSON.stringify(arg)); + } catch { + return String(arg); + } + }), + }); + if (state.console.length > 500) state.console.splice(0, state.console.length - 500); + }; + for (const level of ["debug", "log", "info", "warn", "error"]) { + const original = console[level]; + if (typeof original !== "function" || original.__piChromeWrapped) continue; + const wrapped = function(...args) { + pushConsole(level, args); + return original.apply(this, args); + }; + wrapped.__piChromeWrapped = true; + console[level] = wrapped; + } + window.addEventListener("error", (event) => pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno])); + window.addEventListener("unhandledrejection", (event) => pushConsole("unhandledrejection", [event.reason])); + + const record = (entry) => { + state.network.push(entry); + if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000); + return entry; + }; + if (window.fetch && !window.fetch.__piChromeWrapped) { + const originalFetch = window.fetch.bind(window); + const wrappedFetch = async (...args) => { + const id = "req-" + state.nextRequestId++; + const startedAt = Date.now(); + const input = args[0]; + const init = args[1] || {}; + const url = typeof input === "string" ? input : input?.url; + const method = (init.method || input?.method || "GET").toUpperCase(); + const entry = record({ id, type: "fetch", method, url: String(url || ""), startedAt, pageUrl: location.href, status: "pending" }); + try { + const response = await originalFetch(...args); + entry.status = response.status; + entry.statusText = response.statusText; + entry.ok = response.ok; + entry.responseUrl = response.url; + entry.durationMs = Date.now() - startedAt; + entry.responseHeaders = Array.from(response.headers.entries()); + entry.responseBodyOmitted = "response body capture is disabled by default"; + return response; + } catch (error) { + entry.error = error?.message || String(error); + entry.durationMs = Date.now() - startedAt; + throw error; + } + }; + wrappedFetch.__piChromeWrapped = true; + window.fetch = wrappedFetch; + } + if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) { + const originalOpen = XMLHttpRequest.prototype.open; + const originalSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function(method, url, ...rest) { + this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") }; + return originalOpen.call(this, method, url, ...rest); + }; + XMLHttpRequest.prototype.open.__piChromeWrapped = true; + XMLHttpRequest.prototype.send = function(body) { + const id = "req-" + state.nextRequestId++; + const startedAt = Date.now(); + const info = this.__piChromeRequest || {}; + const entry = record({ id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt, pageUrl: location.href, status: "pending" }); + this.addEventListener("loadend", () => { + entry.status = this.status; + entry.statusText = this.statusText; + entry.responseUrl = this.responseURL; + entry.durationMs = Date.now() - startedAt; + try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch {} + entry.responseBodyOmitted = "response body capture is disabled by default"; + }); + this.addEventListener("error", () => { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; }); + return originalSend.call(this, body); + }; + } + } + + function hashString(text) { + let h = 0; + for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0; + return h; + } + + function selectorFor(element) { + const unique = (selector) => { + try { return document.querySelectorAll(selector).length === 1; } catch { return false; } + }; + if (element.id && unique("#" + cssEscape(element.id))) return "#" + cssEscape(element.id); + const attr = ["aria-label", "name", "placeholder", "data-testid", "role"].find((name) => element.getAttribute(name)); + if (attr) { + const candidate = element.tagName.toLowerCase() + "[" + attr + "=" + JSON.stringify(element.getAttribute(attr)) + "]"; + if (unique(candidate)) return candidate; + } + const parts = []; + let current = element; + while (current && current.nodeType === Node.ELEMENT_NODE && parts.length < 5) { + let part = current.tagName.toLowerCase(); + if (current.classList.length > 0) part += "." + Array.from(current.classList).slice(0, 2).map(cssEscape).join("."); + const siblings = Array.from(current.parentElement?.children ?? []).filter((sibling) => sibling.tagName === current.tagName); + if (siblings.length > 1) part += ":nth-of-type(" + (siblings.indexOf(current) + 1) + ")"; + parts.unshift(part); + const candidate = parts.join(" > "); + if (unique(candidate)) return candidate; + current = current.parentElement; + } + return parts.join(" > "); + } + + function directHeadingText(element) { + const labelledBy = element.getAttribute?.("aria-labelledby"); + if (labelledBy) { + const text = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.innerText || "").join(" ").replace(/\s+/g, " ").trim(); + if (text) return text.slice(0, 180); + } + const aria = element.getAttribute?.("aria-label"); + if (aria) return aria.trim().slice(0, 180); + const heading = Array.from(element.querySelectorAll?.("h1,h2,h3,h4,[role='heading']") || []).find(isElementVisible); + if (heading) return textOf(heading, 180); + return ""; + } + + function meaningfulContainerFor(element) { + let current = element.parentElement; + let fallback = current; + let depth = 0; + while (current && current !== document.body && depth++ < 8) { + if (!isElementVisible(current)) { current = current.parentElement; continue; } + const tag = current.tagName.toLowerCase(); + const role = (current.getAttribute("role") || "").toLowerCase(); + const cls = typeof current.className === "string" ? current.className : ""; + const id = current.id || ""; + const named = Boolean(current.getAttribute("aria-label") || current.getAttribute("aria-labelledby") || directHeadingText(current)); + const semantic = /^(form|dialog|section|article|nav|header|main|aside|footer|li|tr|td|fieldset)$/.test(tag) || + /^(dialog|alertdialog|region|group|listitem|row|cell|tabpanel|menu|toolbar|navigation|main|banner|contentinfo|complementary)$/.test(role); + const classHint = /card|panel|pane|modal|dialog|section|content|container|toolbar|menu|list|item|row|cell|header|footer|sidebar|drawer|popover|dropdown/i.test(`${id} ${cls}`); + const rect = current.getBoundingClientRect(); + const childActions = current.querySelectorAll?.('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])').length || 0; + if ((semantic || classHint || named) && rect.width > 20 && rect.height > 20 && childActions <= 80) return current; + if (!fallback && rect.width > 20 && rect.height > 20) fallback = current; + current = current.parentElement; + } + return fallback || document.body; + } + + function contextForElement(element) { + const container = meaningfulContainerFor(element); + if (!container || container === document.body || container === element) return undefined; + return { + uid: rememberElement(container), + tag: container.tagName.toLowerCase(), + role: roleOf(container), + label: directHeadingText(container) || accessibleLabel(container) || textOf(container, 140), + rect: rectSummary(container), + }; + } + + function summarizeElement(element, index) { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + const occluded = occluderAt(cx, cy, element); + const role = roleOf(element); + const disabled = Boolean(element.disabled || element.getAttribute("aria-disabled") === "true"); + const rawValue = "value" in element && typeof element.value === "string" ? element.value : undefined; + const sensitive = isSensitiveField(element); + const value = rawValue && !sensitive ? rawValue.slice(0, 120) : undefined; + const checked = "checked" in element ? Boolean(element.checked) : undefined; + return { + index, + uid: rememberElement(element), + tag: element.tagName.toLowerCase(), + role, + selector: selectorFor(element), + label: accessibleLabel(element), + href: element.href || undefined, + type: element.getAttribute("type") || undefined, + value: value || undefined, + hasValue: rawValue ? rawValue.length > 0 : undefined, + valueLength: rawValue && sensitive ? rawValue.length : undefined, + valueRedacted: sensitive && rawValue ? true : undefined, + checked, + disabled, + inert: Boolean(element.closest?.("[inert]")), + pointerEvents: style.pointerEvents, + occluded: occluded || undefined, + context: contextForElement(element), + rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }, + }; + } + + function isInViewport(element) { + const rect = element.getBoundingClientRect(); + return rect.bottom >= 0 && rect.right >= 0 && rect.top <= innerHeight && rect.left <= innerWidth; + } + + function formSummaries() { + const fields = Array.from(document.querySelectorAll('input, textarea, select, [contenteditable="true"]')) + .filter(isElementVisible) + .slice(0, 80) + .map((element, index) => ({ + ...summarizeElement(element, index), + required: Boolean(element.required || element.getAttribute("aria-required") === "true"), + invalid: Boolean(element.matches?.(":invalid") || element.getAttribute("aria-invalid") === "true"), + autocomplete: element.getAttribute("autocomplete") || undefined, + })); + const submits = Array.from(document.querySelectorAll('button, input[type="submit"], [role="button"]')) + .filter(isElementVisible) + .filter((element) => /submit|save|continue|next|send|sign in|log in|create|update|done/i.test(accessibleLabel(element) + " " + (element.getAttribute("type") || ""))) + .slice(0, 30) + .map((element, index) => summarizeElement(element, index)); + return { fields, submits }; + } + + function pageMap() { + const landmarkSelectors = [ + ["header", 'header, [role="banner"]'], + ["nav", 'nav, [role="navigation"]'], + ["main", 'main, [role="main"]'], + ["aside", 'aside, [role="complementary"]'], + ["footer", 'footer, [role="contentinfo"]'], + ["dialog", 'dialog, [role="dialog"], [aria-modal="true"]'], + ["form", "form"], + ]; + const regions = []; + for (const [kind, selector] of landmarkSelectors) { + for (const element of Array.from(document.querySelectorAll(selector)).filter(isElementVisible).slice(0, 12)) { + const headings = Array.from(element.querySelectorAll("h1,h2,h3,[role='heading']")).filter(isElementVisible).slice(0, 6).map((h) => textOf(h, 120)); + const actions = Array.from(element.querySelectorAll('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])')).filter(isElementVisible).slice(0, 8).map((a) => { + const summary = summarizeElement(a, 0); + return { uid: summary.uid, role: summary.role, label: summary.label || summary.selector, disabled: summary.disabled || undefined }; + }); + regions.push({ kind, uid: rememberElement(element), label: accessibleLabel(element) || headings[0] || textOf(element, 100), headings, actions }); + } + } + const headings = Array.from(document.querySelectorAll("h1,h2,h3,[role='heading']")).filter(isElementVisible).slice(0, 30).map((element) => ({ + uid: rememberElement(element), + level: Number(element.tagName?.slice(1)) || Number(element.getAttribute("aria-level")) || undefined, + text: textOf(element, 180), + })); + return { regions, headings }; + } + + function layoutSections(elements, forms) { + const byUid = new Map(); + const addToSection = (summary, kind) => { + const source = getPiChromeState().elements[summary.uid]; + const container = source ? meaningfulContainerFor(source) : null; + if (!container || container === document.body) return; + const uid = rememberElement(container); + let section = byUid.get(uid); + if (!section) { + const rect = rectSummary(container); + section = { + uid, + tag: container.tagName.toLowerCase(), + role: roleOf(container), + label: directHeadingText(container) || accessibleLabel(container) || textOf(container, 160), + text: textOf(container, 260), + rect, + actions: [], + fields: [], + }; + byUid.set(uid, section); + } + const item = { uid: summary.uid, role: summary.role, label: summary.label || summary.selector, disabled: summary.disabled || undefined }; + if (kind === "field") section.fields.push(item); + else section.actions.push(item); + }; + for (const el of (elements || []).slice(0, 80)) addToSection(el, ["textbox", "checkbox", "radio", "combobox"].includes(el.role) ? "field" : "action"); + for (const field of (forms?.fields || []).slice(0, 80)) addToSection(field, "field"); + const sections = Array.from(byUid.values()) + .filter((section) => section.actions.length || section.fields.length) + .sort((a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x) + .slice(0, 18); + for (const section of sections) { + section.actions = section.actions.slice(0, 10); + section.fields = section.fields.slice(0, 10); + } + return sections; + } + + function tokenScore(haystack, query) { + if (!query) return 0; + const hay = String(haystack || "").toLowerCase(); + const tokens = String(query).toLowerCase().split(/\W+/).filter(Boolean); + if (!tokens.length) return 0; + let score = 0; + for (const token of tokens) { + if (hay.includes(token)) score += token.length <= 2 ? 1 : 3; + } + if (hay.includes(String(query).toLowerCase())) score += 8; + return score; + } + + function queryMatches(query, elements, map) { + if (!query) return []; + const candidates = []; + for (const element of elements) { + const hay = [element.role, element.label, element.selector, element.type, element.href].filter(Boolean).join(" "); + const score = tokenScore(hay, query); + if (score > 0) candidates.push({ score, kind: "element", ...element }); + } + const textNodes = []; + for (const block of Array.from(document.querySelectorAll("h1,h2,h3,h4,p,li,td,th,label,summary,[role='alert']")).filter(isElementVisible).slice(0, 300)) { + const text = textOf(block, 300); + const score = tokenScore(text, query); + if (score > 0) textNodes.push({ score, kind: "text", uid: rememberElement(block), tag: block.tagName.toLowerCase(), role: roleOf(block), text, rect: rectSummary(block) }); + } + for (const region of map.regions || []) { + const score = tokenScore([region.kind, region.label, ...(region.headings || [])].join(" "), query); + if (score > 0) candidates.push({ score, kind: "region", ...region }); + } + return candidates.concat(textNodes).sort((a, b) => b.score - a.score).slice(0, 20); + } + + function rectSummary(element) { + const rect = element.getBoundingClientRect(); + return { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) }; + } + + function activeElementSummary() { + const el = document.activeElement; + if (!el || el === document.body || el === document.documentElement) return null; + return summarizeElement(el, 0); + } + + function modalSummary() { + const selectors = 'dialog[open], [role="dialog"], [aria-modal="true"], [role="alertdialog"]'; + const modal = Array.from(document.querySelectorAll(selectors)).find(isElementVisible); + if (!modal) return null; + return { uid: rememberElement(modal), tag: modal.tagName.toLowerCase(), role: roleOf(modal), label: accessibleLabel(modal) || textOf(modal, 180), rect: rectSummary(modal) }; + } + + function digestFor(snapshot) { + return { + url: snapshot.url, + title: snapshot.title, + textHash: hashString(snapshot.text || ""), + focusedUid: snapshot.focused?.uid || null, + modalUid: snapshot.modal?.uid || null, + labels: (snapshot.elements || []).slice(0, 50).map((el) => ({ uid: el.uid, role: el.role, label: el.label, disabled: el.disabled, value: el.value, checked: el.checked })), + }; + } + + function diffSnapshot(previous, current) { + if (!previous) return { firstSnapshot: true }; + const changes = []; + if (previous.url !== current.url) changes.push({ kind: "url", before: previous.url, after: current.url }); + if (previous.title !== current.title) changes.push({ kind: "title", before: previous.title, after: current.title }); + if (previous.textHash !== current.textHash) changes.push({ kind: "textChanged" }); + if (previous.focusedUid !== current.focusedUid) changes.push({ kind: "focus", before: previous.focusedUid, after: current.focusedUid }); + if (previous.modalUid !== current.modalUid) changes.push({ kind: "modal", before: previous.modalUid, after: current.modalUid }); + const prevByUid = new Map((previous.labels || []).map((x) => [x.uid, x])); + const curByUid = new Map((current.labels || []).map((x) => [x.uid, x])); + const added = []; + const removed = []; + const updated = []; + for (const cur of current.labels || []) { + const prev = prevByUid.get(cur.uid); + if (!prev) added.push(cur); + else if (prev.label !== cur.label || prev.disabled !== cur.disabled || prev.value !== cur.value || prev.checked !== cur.checked) updated.push({ uid: cur.uid, before: prev, after: cur }); + } + for (const prev of previous.labels || []) { + if (!curByUid.has(prev.uid)) removed.push(prev); + } + return { changes, added: added.slice(0, 12), removed: removed.slice(0, 12), updated: updated.slice(0, 12) }; + } + + function visibleTextSnippets(maxChars) { + const snippets = []; + const blocks = Array.from(document.querySelectorAll("h1,h2,h3,h4,p,li,td,th,label,summary,[role='alert']")).filter(isElementVisible); + let used = 0; + for (const block of blocks) { + if (!isInViewport(block) && snippets.length > 12) continue; + const text = textOf(block, 500); + if (!text || snippets.some((s) => s.text === text)) continue; + const next = { uid: rememberElement(block), tag: block.tagName.toLowerCase(), text, rect: rectSummary(block) }; + snippets.push(next); + used += text.length; + if (used >= maxChars || snippets.length >= 40) break; + } + return snippets; + } + + function snapshotPage(maxElements, containingText, roleFilter, nearUid, mode, query, maxTextChars) { + installPiChromeInstrumentation(); + mode = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"].includes(mode) ? mode : "auto"; + const fullTextLimit = Number(maxTextChars || (mode === "full" ? 30000 : mode === "text" ? 18000 : 6000)); + let candidates = Array.from(document.querySelectorAll('a, button, input, textarea, select, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])')); + if (containingText) { + const needle = String(containingText).toLowerCase(); + candidates = candidates.filter((element) => accessibleLabel(element).toLowerCase().includes(needle)); + } + if (roleFilter) { + const wanted = String(roleFilter).toLowerCase(); + candidates = candidates.filter((element) => roleOf(element) === wanted || element.tagName.toLowerCase() === wanted); + } + let near; + if (nearUid) near = getPiChromeState().elements[nearUid]; + if (near) { + const nearRect = near.getBoundingClientRect(); + const cx = nearRect.left + nearRect.width / 2; + const cy = nearRect.top + nearRect.height / 2; + candidates.sort((a, b) => { + const ra = a.getBoundingClientRect(); + const rb = b.getBoundingClientRect(); + const da = Math.hypot(ra.left + ra.width / 2 - cx, ra.top + ra.height / 2 - cy); + const db = Math.hypot(rb.left + rb.width / 2 - cx, rb.top + rb.height / 2 - cy); + return da - db; + }); + } else { + candidates.sort((a, b) => { + const ar = a.getBoundingClientRect(); + const br = b.getBoundingClientRect(); + const avis = isInViewport(a) ? 0 : 1; + const bvis = isInViewport(b) ? 0 : 1; + return avis - bvis || ar.top - br.top || ar.left - br.left; + }); + } + const visibleCandidates = candidates.filter(isElementVisible); + const elements = visibleCandidates.slice(0, maxElements).map((element, index) => summarizeElement(element, index)); + const queryElements = query + ? visibleCandidates.slice(0, Math.max(maxElements, 500)).map((element, index) => summarizeElement(element, index)) + : elements; + const map = pageMap(); + const forms = formSummaries(); + const layout = layoutSections(elements, forms); + const focused = activeElementSummary(); + const modal = modalSummary(); + const bodyText = document.body ? document.body.innerText.replace(/\s+\n/g, "\n").trim() : ""; + const text = bodyText.slice(0, fullTextLimit); + const snapshot = { + title: document.title, + url: location.href, + mode, + query: query || undefined, + viewport: { width: innerWidth, height: innerHeight, scrollX, scrollY }, + summary: { + visibleText: textOf(document.body, 500), + visibleInteractiveCount: elements.filter((el) => el.rect.y >= 0 && el.rect.y <= innerHeight).length, + totalInteractiveSampled: elements.length, + totalInteractiveVisible: visibleCandidates.length, + focused: focused ? { uid: focused.uid, role: focused.role, label: focused.label } : undefined, + modal: modal ? { uid: modal.uid, label: modal.label } : undefined, + hints: [], + }, + focused: focused || undefined, + modal: modal || undefined, + text, + textTruncated: bodyText.length > text.length, + textSnippets: visibleTextSnippets(mode === "text" ? 12000 : 3000), + elements, + forms, + layout, + pageMap: map, + matches: queryMatches(query, queryElements, map), + filter: { containingText: containingText || undefined, roleFilter: roleFilter || undefined, nearUid: nearUid || undefined }, + }; + if (snapshot.modal) snapshot.summary.hints.push("A modal/dialog is visible; interact with it before the underlying page."); + const disabledImportant = elements.find((el) => el.disabled && /submit|save|merge|continue|next|send|approve|login|sign in/i.test(el.label || "")); + if (disabledImportant) snapshot.summary.hints.push(`${disabledImportant.uid} '${disabledImportant.label}' is disabled.`); + const occluded = elements.find((el) => el.occluded); + if (occluded) snapshot.summary.hints.push(`${occluded.uid} '${occluded.label || occluded.role}' appears occluded by ${occluded.occluded.tag}.`); + + const state = getPiChromeState(); + const currentDigest = digestFor(snapshot); + snapshot.diff = diffSnapshot(state.lastSnapshotDigest, currentDigest); + state.lastSnapshotDigest = currentDigest; + + if (mode === "interactive") { + delete snapshot.text; + delete snapshot.textSnippets; + delete snapshot.pageMap; + } else if (mode === "forms") { + delete snapshot.text; + delete snapshot.textSnippets; + snapshot.elements = elements.filter((el) => ["textbox", "checkbox", "radio", "combobox", "button"].includes(el.role)); + } else if (mode === "pageMap") { + delete snapshot.text; + delete snapshot.textSnippets; + snapshot.elements = elements.slice(0, 20); + } else if (mode === "changes") { + delete snapshot.text; + delete snapshot.textSnippets; + delete snapshot.elements; + delete snapshot.forms; + delete snapshot.layout; + delete snapshot.pageMap; + } else if (mode === "text") { + snapshot.elements = elements.slice(0, 20); + } else if (mode !== "full") { + snapshot.elements = elements.slice(0, Math.min(maxElements, 40)); + snapshot.text = text.slice(0, Math.min(text.length, 6000)); + } + return snapshot; + } + + function inspectTarget(uid, selector, shouldScrollIntoView) { + installPiChromeInstrumentation(); + const state = getPiChromeState(); + let element = null; + if (uid) element = state.elements[uid]; + if (!element && selector) element = document.querySelector(selector); + if (!element || !element.isConnected) throw new Error(uid ? `No live element for uid: ${uid}. Take a fresh chrome_snapshot.` : `No element matches selector: ${selector}`); + if (shouldScrollIntoView) element.scrollIntoView?.({ block: "center", inline: "center", behavior: "instant" }); + const summary = summarizeElement(element, 0); + const ancestors = []; + let current = element.parentElement; + while (current && current !== document.body && ancestors.length < 6) { + ancestors.push({ uid: rememberElement(current), tag: current.tagName.toLowerCase(), role: roleOf(current), label: accessibleLabel(current) || textOf(current, 100), selector: selectorFor(current) }); + current = current.parentElement; + } + const container = element.closest?.('form, dialog, [role="dialog"], [aria-modal="true"], section, article, main, aside') || element.parentElement || document.body; + const nearbyText = Array.from(container.querySelectorAll("h1,h2,h3,h4,p,li,label,[role='alert']")) + .filter(isElementVisible) + .slice(0, 24) + .map((node) => ({ uid: rememberElement(node), tag: node.tagName.toLowerCase(), text: textOf(node, 240), rect: rectSummary(node) })) + .filter((entry) => entry.text); + const nearbyActions = Array.from(container.querySelectorAll('a, button, input, textarea, select, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])')) + .filter(isElementVisible) + .slice(0, 30) + .map((node, index) => summarizeElement(node, index)); + const form = element.closest?.("form"); + const formContext = form ? { + uid: rememberElement(form), + label: accessibleLabel(form) || textOf(form, 160), + fields: Array.from(form.querySelectorAll('input, textarea, select, [contenteditable="true"]')).filter(isElementVisible).slice(0, 30).map((node, index) => summarizeElement(node, index)), + actions: Array.from(form.querySelectorAll('button, input[type="submit"], [role="button"]')).filter(isElementVisible).slice(0, 12).map((node, index) => summarizeElement(node, index)), + } : undefined; + const rect = element.getBoundingClientRect(); + const center = { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2) }; + const clickSuggestion = summary.disabled || summary.inert || summary.pointerEvents === "none" + ? undefined + : { uid: summary.uid, x: center.x, y: center.y }; + return { target: summary, ancestors, nearbyText, nearbyActions, formContext, clickSuggestion }; + } + + globalThis.__piChromeSnapshotPage = snapshotPage; + globalThis.__piChromeInspectTarget = inspectTarget; +})(); diff --git a/pi-chrome/extensions/chrome-profile-bridge/index.ts b/pi-chrome/extensions/chrome-profile-bridge/index.ts new file mode 100644 index 0000000..0b5687c --- /dev/null +++ b/pi-chrome/extensions/chrome-profile-bridge/index.ts @@ -0,0 +1,1915 @@ +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import { dirname, join, resolve } from "node:path"; +import { + SessionSnapshotStore, + type SnapshotCaptureMetadata, + type SnapshotTarget, +} from "../../src/session-snapshot-store.ts"; + +/** + * Existing-profile Chrome bridge for pi. + * + * This is intentionally not a remote-debugging-port integration. Chrome blocks default-profile + * remote debugging in many normal launches, so pi-chrome uses a companion extension from the + * browser-extension folder bundled next to this Pi extension. + * + * The companion extension runs inside the user's real Chrome profile and polls this local + * pi extension for commands. That gives pi access to the user's existing tabs/authenticated + * profile, subject to the browser extension permissions the user grants. + */ + +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +type ToolTextResult = { + content: Array<{ type: "text"; text: string }>; + details: Record; +}; + +type BridgeCommand = { + id: string; + action: string; + params: Record; +}; + +type PendingCommand = { + command: BridgeCommand; + resolve: (value: unknown) => void; + reject: (error: Error) => void; + timer: NodeJS.Timeout; + deliveredAt?: number; +}; + +type BridgeResult = { + id: string; + ok: boolean; + result?: unknown; + error?: string; +}; + +const PI_CHROME_PKG_PATH = resolve(__dirname, "..", "..", "package.json"); +function readPiChromeVersion(): string { + try { + const pkg = JSON.parse(readFileSync(PI_CHROME_PKG_PATH, "utf8")) as { version?: string }; + if (pkg.version) return pkg.version; + } catch {} + return "0.0.0-dev"; +} +const PI_CHROME_VERSION = readPiChromeVersion(); +const PI_CHROME_GLOBAL_KEY = "__piChromeProfileBridgeLoaded__"; +// Authorization is kept on globalThis (separate from the singleton flag, which is cleared on +// reload) so a /reload — which tears down and re-evaluates the module — does not silently drop +// an active /chrome authorize grant. +const PI_CHROME_AUTH_KEY = "__piChromeProfileBridgeAuth__"; +const DEFAULT_HOST = process.env.PI_CHROME_BRIDGE_HOST ?? "127.0.0.1"; +const DEFAULT_PORT = Number(process.env.PI_CHROME_BRIDGE_PORT ?? "17318"); +const DEFAULT_TIMEOUT_MS = 30_000; +const MAX_TEXT_CHARS = 30_000; +const MAX_ELEMENTS = 80; + +function truncateText(text: string, maxChars = MAX_TEXT_CHARS): string { + if (text.length <= maxChars) return text; + return `${text.slice(0, maxChars)}\n\n[truncated ${text.length - maxChars} characters]`; +} + +function safeJson(value: unknown): string { + return JSON.stringify(value, null, 2); +} + +const snapshotModeValues = ["auto", "interactive", "forms", "pageMap", "text", "changes", "full"] as const; + +function compactLine(value: unknown, max = 140): string { + const text = String(value ?? "").replace(/\s+/g, " ").trim(); + return text.length > max ? `${text.slice(0, max - 1)}…` : text; +} + +function rectText(rect: any): string { + if (!rect) return "?"; + return `${rect.x},${rect.y} ${rect.width}x${rect.height}`; +} + +function snapshotFromIncludedResult(raw: unknown): unknown { + return raw && typeof raw === "object" ? (raw as { snapshot?: unknown }).snapshot : undefined; +} + +export function formatSnapshotCapture(metadata: SnapshotCaptureMetadata): string { + const kib = Math.max(1, Math.ceil(metadata.bytes / 1024)); + return [ + `Chrome snapshot saved to ${metadata.path} (${kib} KiB, ${metadata.elementCount} elements${metadata.matchCount ? `, ${metadata.matchCount} matches` : ""}).`, + "Analyze it immediately with ctx_execute_file; FILE_CONTENT is JSON and the page data is under .snapshot.", + "This capture is replaced by the next snapshot for the same target and becomes stale after page interaction.", + ].join("\n"); +} + +export function formatChromeFindMatches(snapshot: any): string { + const lines = [`# Chrome find: ${snapshot?.query ? `\"${compactLine(snapshot.query, 100)}\"` : "matches"}`]; + const matches = Array.isArray(snapshot?.matches) ? snapshot.matches : []; + if (!matches.length) lines.push("No matching elements, regions, or text."); + for (const match of matches.slice(0, 20)) { + if (match.kind === "text") lines.push(`- ${match.uid} text ${compactLine(match.text)} @ ${rectText(match.rect)}`); + else if (match.kind === "region") lines.push(`- ${match.uid} region ${compactLine(match.label)} headings=${(match.headings || []).map((heading: string) => compactLine(heading, 50)).join(" | ")}`); + else lines.push(`- ${match.uid} ${match.role || match.tag || "element"}${match.disabled ? " disabled" : ""} ${compactLine(match.label || match.selector)} @ ${rectText(match.rect)}`); + } + return lines.join("\n"); +} + +function formatChromeInspect(inspect: any): string { + if (!inspect || typeof inspect !== "object") return safeJson(inspect); + const t = inspect.target || {}; + const lines: string[] = []; + lines.push(`# Chrome inspect ${t.uid || ""}`.trim()); + lines.push(`${t.role || t.tag || "element"}${t.disabled ? " disabled" : ""}${t.occluded ? ` occluded-by-${t.occluded.tag}` : ""} ${compactLine(t.label || t.selector)}`); + if (t.selector) lines.push(`selector: ${t.selector}`); + if (t.rect) lines.push(`rect: ${rectText(t.rect)}`); + if (inspect.clickSuggestion) lines.push(`suggested click: chrome_click({ uid: "${inspect.clickSuggestion.uid}" }) or x=${inspect.clickSuggestion.x}, y=${inspect.clickSuggestion.y}`); + if (Array.isArray(inspect.nearbyText) && inspect.nearbyText.length) { + lines.push("\n## Nearby text"); + for (const item of inspect.nearbyText.slice(0, 12)) lines.push(`- ${item.uid} ${compactLine(item.text, 180)}`); + } + if (inspect.formContext) { + lines.push("\n## Form context"); + for (const field of (inspect.formContext.fields || []).slice(0, 20)) lines.push(`- ${field.uid} ${field.role || field.tag}${field.disabled ? " disabled" : ""} ${compactLine(field.label || field.selector)}${field.value ? ` value=${compactLine(field.value, 60)}` : field.valueRedacted ? " value=[redacted]" : ""}`); + for (const action of (inspect.formContext.actions || []).slice(0, 10)) lines.push(`- ${action.uid} action${action.disabled ? " disabled" : ""} ${compactLine(action.label || action.selector)}`); + } + if (Array.isArray(inspect.nearbyActions) && inspect.nearbyActions.length) { + lines.push("\n## Nearby actions"); + for (const action of inspect.nearbyActions.slice(0, 18)) lines.push(`- ${action.uid} ${action.role || action.tag}${action.disabled ? " disabled" : ""} ${compactLine(action.label || action.selector)} @ ${rectText(action.rect)}`); + } + if (Array.isArray(inspect.ancestors) && inspect.ancestors.length) { + lines.push("\n## Ancestors"); + for (const a of inspect.ancestors.slice(0, 6)) lines.push(`- ${a.uid} ${a.role || a.tag} ${compactLine(a.label || a.selector, 120)}`); + } + return truncateText(lines.join("\n")); +} + +function extensionRoot(): string { + // Resolve relative to this extension file, not ctx.cwd. ctx.cwd can temporarily be + // an attachment/clipboard path when Pi is handling pasted images. + if (typeof __dirname === "string") return __dirname; + return process.cwd(); +} + +function workspaceCwd(ctx: ExtensionContext): string { + for (const candidate of [ctx.cwd, process.cwd()]) { + if (!candidate) continue; + try { + if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate; + } catch { + // try next candidate + } + } + return process.cwd(); +} + +function browserExtensionPath(): string { + return join(extensionRoot(), "browser-extension"); +} + +function hostnameOf(url: string | undefined): string { + if (!url) return ""; + try { return new URL(url).hostname; } catch { return ""; } +} + +// Description of a click/type/fill result's significant fields so the agent doesn't have to +// guess whether the action actually changed the page. +function summarizeActionResult(result: unknown): string | undefined { + if (!result || typeof result !== "object") return undefined; + const r = result as Record; + const parts: string[] = []; + // NOTE: pageMutated is a coarse heuristic (a hash over body text + input values + node count). + // Many real effects — class/aria/data-state toggles, JS-held state, canvas, async updates — + // don't move it, so a false value is NOT proof the action did nothing. Surface it only as a + // soft hint, and never present it as a failure on its own. + if (r.pageMutated === false) parts.push("no coarse DOM change detected (may still have taken effect — verify with includeSnapshot)"); + if (r.defaultPrevented === true) parts.push("defaultPrevented=true"); + if (r.elementVisible === false) parts.push("element NOT visible"); + if (r.occludedBy) { + const o = r.occludedBy as { tag?: string; id?: string }; + parts.push(`occluded by <${o.tag ?? "?"}${o.id ? "#" + o.id : ""}>`); + } + if (r.valueMatches === false) parts.push("input value did not stick"); + if (r.autoplayHint) parts.push("autoplay-gated affordance"); + return parts.length ? parts.join("; ") : undefined; +} + +function readRequestBody(request: IncomingMessage): Promise { + return new Promise((resolveBody, rejectBody) => { + const chunks: Buffer[] = []; + request.on("data", (chunk) => chunks.push(Buffer.from(chunk))); + request.on("end", () => resolveBody(Buffer.concat(chunks).toString("utf8"))); + request.on("error", rejectBody); + }); +} + +function corsHeadersFor(request: IncomingMessage): Record { + const origin = String(request.headers.origin ?? ""); + if (!origin.startsWith("chrome-extension://")) return {}; + return { + "access-control-allow-origin": origin, + "access-control-allow-methods": "GET,POST,OPTIONS", + "access-control-allow-headers": "content-type", + "access-control-expose-headers": "x-pi-chrome-version", + "vary": "origin", + }; +} + +function isBrowserOriginAllowed(request: IncomingMessage): boolean { + const origin = String(request.headers.origin ?? ""); + if (origin) return origin.startsWith("chrome-extension://"); + const secFetchSite = String(request.headers["sec-fetch-site"] ?? ""); + return !secFetchSite || secFetchSite === "none" || secFetchSite === "same-origin"; +} + +function isLocalProcessRequest(request: IncomingMessage): boolean { + return !request.headers.origin && !request.headers["sec-fetch-site"]; +} + +function sendJson(response: ServerResponse, status: number, body: unknown, extraHeaders?: Record): void { + response.writeHead(status, { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + ...(extraHeaders ?? {}), + }); + response.end(JSON.stringify(body)); +} + +class ChromeProfileBridge { + private server: Server | undefined; + private pending = new Map(); + private queue: BridgeCommand[] = []; + private waiters: Array<(command: BridgeCommand | undefined) => void> = []; + private lastSeenAt: number | undefined; + private clientName: string | undefined; + private mode: "server" | "client" | undefined; + + constructor( + private readonly host: string, + private readonly port: number, + ) {} + + get url(): string { + return `http://${this.host}:${this.port}`; + } + + get connected(): boolean { + // MV3 service workers can pause between polls/alarms. Treat a recent poll as + // connected without sending a probe command; real chrome_* tool calls are + // the authoritative end-to-end health check. + return this.lastSeenAt !== undefined && Date.now() - this.lastSeenAt < 5 * 60_000; + } + + status(): Record { + return { + url: this.url, + mode: this.mode ?? "starting", + connected: this.connected, + lastSeenAt: this.lastSeenAt, + clientName: this.clientName, + queuedCommands: this.queue.length, + pendingCommands: this.pending.size, + }; + } + + async start(): Promise { + if (this.server || this.mode === "client") return; + await this.bindServerOrClient(); + } + + // Try to own the bridge port. On success we are the server; on EADDRINUSE another Pi + // session owns it and we run as a client that forwards commands to that owner. + private async bindServerOrClient(): Promise { + const server = createServer((request, response) => { + void this.handle(request, response).catch((error) => { + sendJson(response, 500, { error: (error as Error).message }); + }); + }); + try { + await new Promise((resolveStart, rejectStart) => { + server.once("error", rejectStart); + server.listen(this.port, this.host, () => { + server.off("error", rejectStart); + resolveStart(); + }); + }); + this.server = server; + this.mode = "server"; + } catch (error) { + server.close(); + if ((error as NodeJS.ErrnoException).code !== "EADDRINUSE") throw error; + // Another Pi session already owns the bridge port. Use it as the shared + // machine-local broker so multiple Pi sessions can control Chrome at once. + this.mode = "client"; + } + } + + // Client-mode self-heal: when the owning Pi session disappears, fetches to its port fail + // with `fetch failed` / ECONNREFUSED forever. Try to grab the now-free port and become the + // server ourselves so chrome_* tools recover without a manual restart. + private async tryPromoteToServer(): Promise { + if (this.mode !== "client") return this.mode === "server"; + this.mode = undefined; + await this.bindServerOrClient(); + return this.mode === "server"; + } + + stop(): void { + if (this.mode === "client") { + this.mode = undefined; + return; + } + for (const pending of this.pending.values()) { + clearTimeout(pending.timer); + pending.reject(new Error("Chrome profile bridge stopped")); + } + this.pending.clear(); + this.queue = []; + for (const waiter of this.waiters) waiter(undefined); + this.waiters = []; + this.server?.close(); + this.server = undefined; + this.mode = undefined; + } + + send(action: string, params: Record, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise { + if (this.mode === "client") return this.sendViaOwner(action, params, timeoutMs, signal); + return this.sendLocal(action, params, timeoutMs, signal); + } + + private sendLocal(action: string, params: Record, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise { + const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + const command = { id, action, params }; + return new Promise((resolveCommand, rejectCommand) => { + if (signal?.aborted) { + rejectCommand(new Error("Chrome command aborted")); + return; + } + const cleanupAbort = () => { + if (signal) signal.removeEventListener("abort", onAbort); + }; + const onAbort = () => { + clearTimeout(timer); + this.pending.delete(id); + this.queue = this.queue.filter((queued) => queued.id !== id); + cleanupAbort(); + rejectCommand(new Error("Chrome command aborted")); + }; + const timer = setTimeout(() => { + const entry = this.pending.get(id); + this.pending.delete(id); + this.queue = this.queue.filter((queued) => queued.id !== id); + cleanupAbort(); + rejectCommand(new Error(this.timeoutMessage(entry, timeoutMs))); + }, timeoutMs); + this.pending.set(id, { + command, + resolve: (value) => { cleanupAbort(); resolveCommand(value); }, + reject: (err) => { cleanupAbort(); rejectCommand(err); }, + timer, + }); + if (signal) signal.addEventListener("abort", onAbort, { once: true }); + this.enqueue(command); + }); + } + + // Classify why a local command timed out so the agent isn't left guessing. The three + // distinct failure modes are: extension never polled (not installed / not running), + // extension polled but never picked up this command, and extension picked up the command + // but never posted a result back (long-running action or a failed /result post). + private timeoutMessage(entry: PendingCommand | undefined, timeoutMs: number): string { + const pollAgeMs = this.lastSeenAt === undefined ? undefined : Date.now() - this.lastSeenAt; + if (entry?.deliveredAt) { + return `Timed out after ${timeoutMs}ms: the Chrome extension received the command but never returned a result. The action may be long-running, or the result post failed. Run /chrome doctor; if it persists, reload 'Pi Chrome Connector' at chrome://extensions.`; + } + if (pollAgeMs === undefined || pollAgeMs > 60_000) { + return `Timed out after ${timeoutMs}ms: the Chrome extension is not polling (last seen ${pollAgeMs === undefined ? "never" : Math.round(pollAgeMs / 1000) + "s ago"}). Run /chrome onboard, then load the bundled browser-extension folder in your normal Chrome profile and keep that Chrome window open.`; + } + return `Timed out after ${timeoutMs}ms: the Chrome extension is polling (last seen ${Math.round(pollAgeMs / 1000)}s ago) but did not pick up this command in time. Retry; if it persists, reload 'Pi Chrome Connector' at chrome://extensions.`; + } + + private async sendViaOwner(action: string, params: Record, timeoutMs: number, signal?: AbortSignal): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs + 2_000); + const forwardAbort = () => controller.abort(); + if (signal) { + if (signal.aborted) controller.abort(); + else signal.addEventListener("abort", forwardAbort, { once: true }); + } + try { + const response = await fetch(`${this.url}/command`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ action, params, timeoutMs }), + signal: controller.signal, + }); + const payload = (await response.json().catch(() => ({}))) as { ok?: boolean; result?: unknown; error?: string }; + if (response.status === 404) { + throw new Error( + "A running Pi session owns the Chrome bridge but is using an older pi-chrome without multi-session support. Restart that Pi session after `pi update`, then retry.", + ); + } + if (!response.ok || !payload.ok) throw new Error(payload.error ?? `Chrome bridge owner HTTP ${response.status}`); + return payload.result; + } catch (error) { + if ((error as Error).name === "AbortError") { + if (signal?.aborted) throw new Error("Chrome command aborted"); + throw new Error(`Timed out waiting for shared Chrome bridge owner after ${timeoutMs}ms`); + } + // `fetch failed` / ECONNREFUSED means the Pi session that owned the bridge port is gone. + // Try to take over the port ourselves and re-run the command locally instead of staying + // stuck as a client pointed at a dead owner. + if (this.isOwnerUnreachable(error)) { + const promoted = await this.tryPromoteToServer().catch(() => false); + if (promoted) return this.sendLocal(action, params, timeoutMs, signal); + throw new Error( + "The Pi session that owned the Chrome bridge is unreachable and this session could not take over the bridge port. Restart this Pi session, or run /chrome doctor.", + ); + } + throw error; + } finally { + clearTimeout(timer); + if (signal) signal.removeEventListener("abort", forwardAbort); + } + } + + private isOwnerUnreachable(error: unknown): boolean { + const message = (error as Error)?.message ?? ""; + const code = (error as NodeJS.ErrnoException)?.code ?? ""; + const cause = (error as { cause?: NodeJS.ErrnoException })?.cause; + const causeCode = cause?.code ?? ""; + return ( + /fetch failed|ECONNREFUSED|ECONNRESET|other side closed|socket hang up/i.test(message) || + code === "ECONNREFUSED" || + causeCode === "ECONNREFUSED" || + causeCode === "ECONNRESET" + ); + } + + private enqueue(command: BridgeCommand): void { + const waiter = this.waiters.shift(); + if (waiter) waiter(command); + else this.queue.push(command); + } + + private async handle(request: IncomingMessage, response: ServerResponse): Promise { + const url = new URL(request.url ?? "/", this.url); + const corsHeaders = corsHeadersFor(request); + if (request.method === "OPTIONS") { + if (!isBrowserOriginAllowed(request)) { + sendJson(response, 403, { ok: false, error: "browser origin not allowed" }); + return; + } + sendJson(response, 200, { ok: true }, corsHeaders); + return; + } + if (request.method === "GET" && url.pathname === "/status") { + sendJson(response, 200, this.status()); + return; + } + if (request.method === "POST" && url.pathname === "/command") { + if (!isLocalProcessRequest(request)) { + sendJson(response, 403, { ok: false, error: "Chrome commands are accepted only from local Pi processes" }); + return; + } + const body = JSON.parse(await readRequestBody(request)) as { + action?: string; + params?: Record; + timeoutMs?: number; + }; + if (!body.action) { + sendJson(response, 400, { ok: false, error: "Missing command action" }); + return; + } + try { + const result = await this.sendLocal(body.action, body.params ?? {}, body.timeoutMs ?? DEFAULT_TIMEOUT_MS); + sendJson(response, 200, { ok: true, result }); + } catch (error) { + sendJson(response, 504, { ok: false, error: (error as Error).message }); + } + return; + } + if (request.method === "GET" && url.pathname === "/next") { + if (!isBrowserOriginAllowed(request)) { + sendJson(response, 403, { ok: false, error: "browser origin not allowed" }); + return; + } + this.lastSeenAt = Date.now(); + this.clientName = url.searchParams.get("name") ?? undefined; + let aborted = false; + let activeWaiter: ((command: BridgeCommand | undefined) => void) | undefined; + request.once("close", () => { + aborted = true; + if (activeWaiter) this.waiters = this.waiters.filter((entry) => entry !== activeWaiter); + }); + let command = this.queue.shift(); + if (!command) { + command = await this.waitForCommand(25_000, (waiter) => { + activeWaiter = waiter; + }); + } + if (aborted) { + // Long-poll connection died before we could deliver. Requeue any command we pulled + // so the next live /next picks it up instead of dropping it on the floor. + if (command) this.queue.unshift(command); + return; + } + // Mark the command as delivered so a later timeout can distinguish "extension never + // picked it up" from "extension is running it / failed to post a result". + if (command) { + const entry = this.pending.get(command.id); + if (entry) entry.deliveredAt = Date.now(); + } + // Re-read version on every /next so bumping package.json takes effect without pi restart. + const currentVersion = readPiChromeVersion(); + sendJson( + response, + 200, + command + ? { type: "command", command, expectedExtensionVersion: currentVersion } + : { type: "none", expectedExtensionVersion: currentVersion }, + { ...corsHeaders, "x-pi-chrome-version": currentVersion }, + ); + return; + } + if (request.method === "POST" && url.pathname === "/result") { + if (!isBrowserOriginAllowed(request)) { + sendJson(response, 403, { ok: false, error: "browser origin not allowed" }); + return; + } + this.lastSeenAt = Date.now(); + const result = JSON.parse(await readRequestBody(request)) as BridgeResult; + const pending = this.pending.get(result.id); + if (!pending) { + sendJson(response, 404, { ok: false, error: "unknown command id" }, corsHeaders); + return; + } + clearTimeout(pending.timer); + this.pending.delete(result.id); + if (result.ok) pending.resolve(result.result); + else pending.reject(new Error(result.error ?? "Chrome extension command failed")); + sendJson(response, 200, { ok: true }, corsHeaders); + return; + } + sendJson(response, 404, { error: "not found" }); + } + + private waitForCommand( + timeoutMs: number, + registerWaiter?: (waiter: (command: BridgeCommand | undefined) => void) => void, + ): Promise { + return new Promise((resolveWait) => { + let settled = false; + const waiter = (command: BridgeCommand | undefined) => { + if (settled) return; + settled = true; + clearTimeout(timer); + this.waiters = this.waiters.filter((entry) => entry !== waiter); + resolveWait(command); + }; + const timer = setTimeout(() => waiter(undefined), timeoutMs); + this.waiters.push(waiter); + registerWaiter?.(waiter); + }); + } +} + +const tabActionValues = ["list", "new", "activate", "close", "group", "ungroup", "version"] as const; +const imageFormatValues = ["png", "jpeg"] as const; +const waitForValues = ["selector", "expression"] as const; +export const CHROME_TOOL_NAMES = [ + "chrome_launch", + "chrome_tab", + "chrome_snapshot", + "chrome_find", + "chrome_inspect", + "chrome_navigate", + "chrome_evaluate", + "chrome_click", + "chrome_type", + "chrome_fill", + "chrome_key", + "chrome_wait_for", + "chrome_list_console_messages", + "chrome_list_network_requests", + "chrome_get_network_request", + "chrome_screenshot", + "chrome_hover", + "chrome_drag", + "chrome_tap", + "chrome_scroll", + "chrome_upload_file", +] as const; +const CHROME_TOOL_NAME_SET = new Set(CHROME_TOOL_NAMES); + +function StringEnum(values: T) { + return Type.Union(values.map((value) => Type.Literal(value)) as [ReturnType, ...ReturnType[]]); +} + +export default function (pi: ExtensionAPI): void { + const instanceToken = Symbol("pi-chrome-instance"); + const currentRoot = extensionRoot(); + const globalState = globalThis as typeof globalThis & { + [PI_CHROME_GLOBAL_KEY]?: { version: string; root: string; token?: symbol }; + [PI_CHROME_AUTH_KEY]?: { until: number | "indefinite" }; + }; + const alreadyLoaded = globalState[PI_CHROME_GLOBAL_KEY]; + if (alreadyLoaded?.token || (alreadyLoaded && alreadyLoaded.root !== currentRoot)) { + console.warn( + `pi-chrome already loaded from ${alreadyLoaded.root} (v${alreadyLoaded.version}); skipping duplicate from ${currentRoot}.`, + ); + return; + } + // pi-chrome <=0.15.19 set the singleton flag but did not clear it on reload. + // If the stale flag points at this same extension root, replace it instead of + // skipping the freshly reloaded extension. + globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: currentRoot, token: instanceToken }; + + const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT); + let backgroundDefault = true; + let chromeAuthorizedUntil: number | "indefinite" | undefined; + // Restore an authorization that survived a /reload. Drop it if it already expired. + const persistedAuth = globalState[PI_CHROME_AUTH_KEY]; + if (persistedAuth) { + if (persistedAuth.until === "indefinite" || persistedAuth.until > Date.now()) { + chromeAuthorizedUntil = persistedAuth.until; + } else { + delete globalState[PI_CHROME_AUTH_KEY]; + } + } + const persistAuth = (): void => { + if (chromeAuthorizedUntil === undefined) delete globalState[PI_CHROME_AUTH_KEY]; + else globalState[PI_CHROME_AUTH_KEY] = { until: chromeAuthorizedUntil }; + }; + let chromeToolsRegistered = false; + let chromeToolsUsable = false; + let authExpiryTimer: NodeJS.Timeout | undefined; + let countdownInterval: NodeJS.Timeout | undefined; + // Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy. + let sessionCtx: ExtensionContext | undefined; + let snapshotStore: SessionSnapshotStore | undefined; + + const clearAuthExpiryTimer = (): void => { + if (!authExpiryTimer) return; + clearTimeout(authExpiryTimer); + authExpiryTimer = undefined; + }; + + const clearCountdownInterval = (): void => { + if (!countdownInterval) return; + clearInterval(countdownInterval); + countdownInterval = undefined; + }; + + const chromeToolsActive = (tools = pi.getActiveTools()): boolean => tools.some((name) => CHROME_TOOL_NAME_SET.has(name)); + + const activateChromeTools = (): boolean => { + registerChromeTools(pi); + const before = pi.getActiveTools(); + const next = [...new Set([...before, ...CHROME_TOOL_NAMES])]; + pi.setActiveTools(next); + return !chromeToolsActive(before) && chromeToolsActive(next); + }; + + const deactivateChromeTools = (): boolean => { + const before = pi.getActiveTools(); + pi.setActiveTools(before.filter((name) => !CHROME_TOOL_NAME_SET.has(name))); + return chromeToolsActive(before); + }; + + const logChromeToolChange = ( + action: "authorized" | "reauthorized" | "revoked" | "expired", + options: { label?: string; authorizedUntil?: number | "indefinite" } = {}, + ): void => { + const content = action === "authorized" + ? `Chrome tools enabled by /chrome authorize${options.label ? ` (${options.label})` : ""}.` + : action === "reauthorized" + ? `Chrome tool authorization updated by /chrome authorize${options.label ? ` (${options.label})` : ""}.` + : action === "expired" + ? "Chrome tools disabled because /chrome authorize grant expired." + : "Chrome tools disabled by /chrome revoke."; + pi.sendMessage({ + customType: "pi-chrome-tool-change", + content, + display: true, + details: { + action, + tools: [...CHROME_TOOL_NAMES], + authorizedUntil: options.authorizedUntil, + at: Date.now(), + }, + }, { triggerTurn: false }); + }; + + // Close THIS session's dedicated automation window/tab. Fire-and-forget and best-effort: it + // must never block /quit, /reload, revoke, or session end, and the service-worker side only + // ever closes targets this session created itself (never user tabs/windows, never another + // session's target). Errors (bridge down, target already closed) are intentionally swallowed. + const cleanupAutomationTargetBestEffort = (): void => { + const sessionKey = sessionKeyFor(sessionCtx); + void bridge.send("automation.cleanup", sessionKey !== undefined ? { sessionKey } : {}, 3_000).catch(() => undefined); + }; + + const lockChromeControl = (logAction?: "revoked" | "expired"): void => { + clearAuthExpiryTimer(); + clearCountdownInterval(); + const wasUsable = chromeToolsUsable; + deactivateChromeTools(); + chromeToolsUsable = false; + if (logAction && wasUsable) logChromeToolChange(logAction, { authorizedUntil: undefined }); + chromeAuthorizedUntil = undefined; + persistAuth(); + // Revoking control ends pi-chrome's automation for this session; tidy up the target we own. + cleanupAutomationTargetBestEffort(); + void snapshotStore?.clear().catch(() => undefined); + }; + + const authSummary = (): string => { + if (chromeAuthorizedUntil === "indefinite") return "authorized indefinitely"; + if (typeof chromeAuthorizedUntil === "number") { + const remainingMs = chromeAuthorizedUntil - Date.now(); + if (remainingMs > 0) return `authorized for ~${Math.ceil(remainingMs / 60_000)}m`; + } + return "locked"; + }; + + const chromeControlAuthorized = (): boolean => { + if (chromeAuthorizedUntil === "indefinite") return true; + if (typeof chromeAuthorizedUntil === "number" && chromeAuthorizedUntil > Date.now()) return true; + if (chromeAuthorizedUntil !== undefined) lockChromeControl("expired"); + return false; + }; + + const requireChromeControlAuthorized = (): void => { + if (!chromeControlAuthorized()) { + throw new Error("Chrome control locked. Ask the user to run /chrome authorize before using chrome_* tools."); + } + }; + + // Tab-group title for this Pi session: prefer the user-set display name, else the session id. + const sessionGroupTitle = (ctx: ExtensionContext): string => { + const sm = ctx.sessionManager; + const name = sm.getSessionName?.(); + const id = sm.getSessionId?.(); + return `Pi Session: ${name || id || "unknown"}`; + }; + + const authCountdownLabel = (): string => { + if (chromeAuthorizedUntil === "indefinite") return " (indefinite)"; + if (typeof chromeAuthorizedUntil === "number") { + const remainingMs = chromeAuthorizedUntil - Date.now(); + if (remainingMs > 0) { + const mins = Math.ceil(remainingMs / 60_000); + return mins >= 1 ? ` (${mins}m)` : " (<1m)"; + } + } + return ""; + }; + + // Stable per-session key the service worker uses to scope its dedicated automation tab/window + // to *this* session (one extension brokers all sessions). The session id is stable across + // /reload, so the automation target is reused rather than orphaned. Returns undefined only + // before session_start, in which case the worker uses its default bucket. + const sessionKeyFor = (ctx: ExtensionContext | undefined): string | undefined => { + const id = ctx?.sessionManager?.getSessionId?.(); + return typeof id === "string" && id ? `session:${id}` : undefined; + }; + + const updateChromeStatus = (ctx: ExtensionContext): void => { + if (chromeControlAuthorized()) { + ctx.ui.setStatus("chrome", ctx.ui.theme.fg("success", "●") + " Chrome Bridge" + authCountdownLabel()); + } else { + ctx.ui.setStatus("chrome", undefined); + } + }; + + // Ticks every 60 s while a timed authorization is active to keep the countdown current. + const startCountdownTicker = (ctx: ExtensionContext): void => { + clearCountdownInterval(); + if (chromeAuthorizedUntil === "indefinite" || typeof chromeAuthorizedUntil !== "number") return; + countdownInterval = setInterval(() => { + if (!chromeControlAuthorized()) { + clearCountdownInterval(); + return; + } + updateChromeStatus(ctx); + }, 60_000); + }; + + const scheduleAuthExpiry = (ctx: ExtensionContext, until: number | "indefinite"): void => { + clearAuthExpiryTimer(); + startCountdownTicker(ctx); + if (until === "indefinite") return; + authExpiryTimer = setTimeout(() => { + if (chromeAuthorizedUntil !== until) return; + try { + lockChromeControl("expired"); + ctx.ui.notify("Chrome control authorization expired. Run /chrome authorize to allow chrome_* tools again.", "info"); + updateChromeStatus(ctx); + } catch (error) { + console.warn(`Failed to expire pi-chrome authorization cleanly: ${(error as Error).message}`); + } + }, Math.max(0, until - Date.now())); + }; + + const authorizedBridgeSend = (action: string, params: Record, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise => { + requireChromeControlAuthorized(); + // Scope the service worker's dedicated automation tab/window to this session. Forwarded on + // every action so tab resolution, navigation, and cleanup all agree on which target is ours. + const sessionKey = sessionKeyFor(sessionCtx); + let wireParams: Record = sessionKey !== undefined && params.sessionKey === undefined + ? { ...params, sessionKey } + : params; + const sessionTitle = sessionCtx !== undefined ? sessionGroupTitle(sessionCtx) : undefined; + // Any tab Pi opens through tab.new/tab.group must use THIS session's group, even if a caller + // passes group:false or a custom groupTitle. This central guard covers chrome_tab plus internal + // callers such as chrome_launch(url). + if ((action === "tab.new" || action === "tab.group") && sessionTitle !== undefined) { + wireParams = { ...wireParams, groupTitle: sessionTitle }; + } + // Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the + // auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull + // the resolved tab into the session group on the service-worker side. We skip tab.* actions: + // tab.new/group are forced above, and activate/close/ungroup/list must not group tabs. + const shouldJoinGroup = action.startsWith("page.") && sessionTitle !== undefined && params.sessionGroupTitle === undefined; + if (shouldJoinGroup) { + wireParams = { ...wireParams, sessionGroupTitle: sessionTitle, joinSessionGroup: true }; + } + return bridge.send(action, wireParams, timeoutMs, signal); + }; + + // Translate the public `background` parameter (default on = silent/background) into the + // service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback. + const withBackground = >(params: T): T => { + const typed = params as { background?: boolean; foreground?: boolean }; + const explicit = + typed.background !== undefined + ? typed.background + : typed.foreground !== undefined + ? !typed.foreground + : undefined; + const background = explicit ?? backgroundDefault; + return { ...params, foreground: !background } as T; + }; + + const snapshotTargetFrom = (params: Record): SnapshotTarget => ({ + targetId: typeof params.targetId === "string" ? params.targetId : undefined, + urlIncludes: typeof params.urlIncludes === "string" ? params.urlIncludes : undefined, + titleIncludes: typeof params.titleIncludes === "string" ? params.titleIncludes : undefined, + }); + + const saveSnapshotCapture = async (snapshot: unknown, params: Record): Promise => { + if (!snapshotStore) throw new Error("Chrome snapshot storage is unavailable before session_start."); + const stored = await snapshotStore.save(snapshot, snapshotTargetFrom(params)); + return stored.metadata; + }; + + const invalidateSnapshotCapture = async (params?: Record): Promise => { + if (!snapshotStore) return; + if (!params) await snapshotStore.clear(); + else await snapshotStore.invalidate(snapshotTargetFrom(params)); + }; + + const formatActionWithCapture = async (raw: unknown, params: Record, text: string): Promise => { + const snapshot = snapshotFromIncludedResult(raw); + const result = params.includeSnapshot && raw && typeof raw === "object" + ? (raw as { result?: unknown }).result + : raw; + if (!snapshot) { + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text }], details: { result: result as Json } }; + } + const capture = await saveSnapshotCapture(snapshot, params); + return { + content: [{ type: "text", text: `${text}\n\n${formatSnapshotCapture(capture)}` }], + details: { result: result as Json, capture }, + }; + }; + + pi.on("session_start", async (_event, ctx) => { + sessionCtx = ctx; + const sessionId = ctx.sessionManager.getSessionId?.() || "unknown-session"; + snapshotStore = new SessionSnapshotStore(workspaceCwd(ctx), sessionId); + await snapshotStore.clear(); + await bridge.start(); + // Reestablish in-memory state after a /reload restored chromeAuthorizedUntil from globalThis. + if (chromeControlAuthorized()) { + activateChromeTools(); + chromeToolsUsable = true; + if (typeof chromeAuthorizedUntil === "number") scheduleAuthExpiry(ctx, chromeAuthorizedUntil); + else if (chromeAuthorizedUntil === "indefinite") startCountdownTicker(ctx); + } else { + deactivateChromeTools(); + chromeToolsUsable = false; + } + updateChromeStatus(ctx); + }); + + pi.on("session_shutdown", async (event) => { + clearAuthExpiryTimer(); + clearCountdownInterval(); + // Snapshot files are intentionally ephemeral. A reload must take a fresh observation too, + // because the browser page may have changed while the extension was reloading. + await snapshotStore?.clear().catch(() => undefined); + snapshotStore = undefined; + // Keep the owned automation target across /reload, but clean it up on real session end. + if (event?.reason !== "reload") cleanupAutomationTargetBestEffort(); + bridge.stop(); + if (globalState[PI_CHROME_GLOBAL_KEY]?.token === instanceToken) { + delete globalState[PI_CHROME_GLOBAL_KEY]; + } + }); + + pi.on("before_agent_start", (event) => { + if (!chromeToolsRegistered || !chromeControlAuthorized()) { + return { systemPrompt: event.systemPrompt }; + } + const primer = ` + +Chrome control is available through the chrome_* tools via a companion Chrome extension installed in the user's normal Chrome profile. Tools target the existing signed-in profile: no remote-debug port, no throwaway profile. + +Tab/window isolation (important): +- pi-chrome owns a dedicated automation window/tab. When a chrome_* tool runs with no explicit target, it acts on that pi-chrome-owned target — it never reuses or overwrites the user's currently active tab. The dedicated target is created on first use and reused afterward. +- To act on a specific *existing* tab (e.g. one the user asks you to use), pass targetId/urlIncludes/titleIncludes. Without one of those, assume you are working in pi-chrome's own automation target. +- pi-chrome's automation target may be closed automatically when Chrome control is revoked; user tabs/windows are never closed by pi-chrome. + +Capability model (important): +- Interactive controls (click/type/fill/key/hover/drag/scroll/tap) use Chrome's real input layer via chrome.debugger / CDP. Events satisfy normal user-activation gates. +- Input bypasses page CSP because it is injected at browser input layer, not page JavaScript. Chrome may show the “Pi Chrome Connector started debugging this browser” banner while attached. +- \`chrome_evaluate\` and \`chrome_snapshot\` run in MAIN world via **CDP \`Runtime.evaluate\`**, which is not subject to the page's Content-Security-Policy. They work even on strict-CSP pages (e.g. github.com, many bank/SaaS apps) that block \`'unsafe-eval'\`. \`chrome_navigate initScript\` likewise injects at document_start via CDP and bypasses CSP. \`chrome_screenshot\`, \`chrome_tab\`, and Chrome input also work under any CSP. +- \`chrome_snapshot\` and snapshots requested with \`includeSnapshot=true\` are written to a private session-local JSON capture; raw page data is never returned inline. The tool result gives the path for immediate \`ctx_execute_file\` analysis. + +Usage rules: +1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying. +2. Call \`chrome_snapshot\`, then immediately call \`ctx_execute_file\` on the returned path and print only the uids/state needed for the next action. Never use read/cat on the full capture. +3. Prefer \`uid\` over \`selector\`. After any page interaction, treat the old capture as stale. Use \`includeSnapshot=true\` when a fresh post-action capture is needed. +4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm. +5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now). +6. By default chrome_* tools run in the background without focusing Chrome; pass \`background=false\` or run /chrome background off when the user wants to watch Chrome work. +7. If you hit a native file-picker or privileged browser prompt gate, tell the user; generic clicks/typing/CSP gates are handled by Chrome input. +8. Run /chrome doctor when in doubt about connectivity or capabilities. +`; + return { systemPrompt: event.systemPrompt + primer }; + }); + + // Shared handlers, dispatched by the unified /chrome command below. + const doctorHandler = async (ctx: ExtensionContext) => { + ctx.ui.notify("Checking pi-chrome…", "info"); + const lines: string[] = [`pi-chrome v${PI_CHROME_VERSION}`]; + const status = bridge.status(); + const roleLabel = status.mode === "client" ? "sharing another pi session's connection" : "running the Chrome connection for this machine"; + lines.push(`• This pi session is ${roleLabel}.`); + let extensionAlive = false; + let versionMismatch = false; + try { + const started = Date.now(); + const version = (await bridge.send("tab.version", {}, 35_000)) as { + extensionId?: string; + extensionVersion?: string; + bridgeUrl?: string; + }; + const latencyMs = Date.now() - started; + extensionAlive = true; + if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) { + versionMismatch = true; + lines.push( + `✗ The Chrome companion extension is on an old version (${version.extensionVersion}); this pi-chrome is ${PI_CHROME_VERSION}.`, + ` Every Chrome action will run with the old code until you reload the extension.`, + ` Fix: open chrome://extensions and click the refresh icon on 'Pi Chrome Connector'.`, + ` (After this one-time fix, future updates reload automatically.)`, + ); + } else { + lines.push(`✓ Chrome is connected (companion extension v${version.extensionVersion ?? "?"}, responded in ${latencyMs}ms).`); + } + } catch (error) { + const message = (error as Error).message; + lines.push(`✗ Chrome isn't responding: ${message}`); + if (message.includes("older pi-chrome without multi-session")) { + lines.push(" Fix: quit and restart the pi session that first opened the Chrome connection (it was on an older pi-chrome)."); + } else { + lines.push(" Fix: run /chrome onboard to install the Chrome companion extension, then keep that Chrome window open."); + } + } + + if (extensionAlive && !versionMismatch) { + // Sanity-check that pi-chrome can actually run code in the active tab. + try { + const value = await bridge.send("page.evaluate", { expression: "1+1", awaitPromise: true, foreground: false }, 10_000); + if (value === 2) lines.push(`✓ pi-chrome can run code in the active Chrome tab.`); + else lines.push(`⚠ pi-chrome ran code in the active tab but got an unexpected result (${JSON.stringify(value)}). The current tab may be locked-down (a Chrome internal page or a strict site).`); + } catch (error) { + lines.push(`✗ pi-chrome can't run code in the active tab: ${(error as Error).message}`); + } + + // Surface obvious site-side automation flags so the user knows why a site might block pi. + try { + const probe = (await bridge.send("page.probe", { foreground: false }, 10_000)) as Record; + if (probe && probe.arithmetic === 2) lines.push(`✓ The active tab is ${hostnameOf(String(probe.location))} and accepts pi-chrome's commands.`); + if (probe && probe.webdriver) lines.push(`⚠ Your Chrome is reporting itself as automated to websites. Some sites use this signal to block sign-ins or bot checks.`); + } catch (error) { + lines.push(`⚠ Couldn't inspect the active tab: ${(error as Error).message}`); + } + } else if (versionMismatch) { + lines.push(`… Skipped the remaining checks until you reload the Chrome extension.`); + } + + ctx.ui.notify(lines.join("\n"), "info"); + }; + + // Run-in-background (Chrome focus) handler. No args = toggle. Explicit on/off/status. + const BACKGROUND_DESC: Record = { + on: "pi-chrome runs in the background; Chrome won't pop up or steal focus.", + off: "Chrome pops to the front and switches tabs so you can watch what pi-chrome is doing.", + }; + + const backgroundHandler = async (ctx: ExtensionContext, args: string) => { + const arg = (args || "").trim().toLowerCase(); + const currentLabel = backgroundDefault ? "on" : "off"; + + if (arg === "status") { + ctx.ui.notify(`Run in background is ${currentLabel}. ${BACKGROUND_DESC[currentLabel]}`, "info"); + return; + } + + if (arg === "on" || arg === "true" || arg === "1") backgroundDefault = true; + else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false; + else if (arg === "toggle" || arg === "") backgroundDefault = !backgroundDefault; + else { + ctx.ui.notify(`Unknown background setting '${arg}'. Pick one of: on | off | toggle | status.`, "warning"); + return; + } + + const nextLabel = backgroundDefault ? "on" : "off"; + ctx.ui.notify(`Run in background → ${nextLabel}. ${BACKGROUND_DESC[nextLabel]}`, "info"); + }; + + const authorizeFor = async (ctx: ExtensionContext, label: string, until: number | "indefinite") => { + const ok = await ctx.ui.confirm( + "Authorize pi-chrome control?", + `This Pi session will be allowed to inspect and control your existing Chrome profile for ${label}.\n\nChrome actions use your signed-in browser state and real input. Only approve if you trust the current agent/task.`, + ); + if (!ok) { + ctx.ui.notify("Chrome control remains locked.", "info"); + return; + } + const wasUsable = chromeToolsUsable; + chromeAuthorizedUntil = until; + persistAuth(); + activateChromeTools(); + chromeToolsUsable = true; + logChromeToolChange(wasUsable ? "reauthorized" : "authorized", { label, authorizedUntil: until }); + scheduleAuthExpiry(ctx, until); + ctx.ui.notify(`Chrome control authorized for ${label}.`, "info"); + updateChromeStatus(ctx); + }; + + const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => { + const normalized = arg.trim().toLowerCase() || "15m"; + if (normalized === "indefinite" || normalized === "forever") return { label: "indefinitely", until: "indefinite" }; + const minutes = normalized.endsWith("m") ? Number(normalized.slice(0, -1)) : Number(normalized); + if (!Number.isFinite(minutes) || minutes <= 0) return undefined; + return { label: `${minutes} minutes`, until: Date.now() + minutes * 60_000 }; + }; + + const authorizeHandler = async (ctx: ExtensionContext, args: string) => { + const grant = parseAuthorizeArg(args); + if (!grant) { + ctx.ui.notify("Unknown authorize duration. Use minutes (15m, 30m, 45) or indefinite.", "warning"); + return; + } + return authorizeFor(ctx, grant.label, grant.until); + }; + + const revokeHandler = (ctx: ExtensionContext) => { + lockChromeControl("revoked"); + ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info"); + updateChromeStatus(ctx); + }; + + const onboardHandler = async (ctx: ExtensionContext) => { + const extensionPath = browserExtensionPath(); + const proceed = await ctx.ui.confirm( + "Install the pi-chrome Chrome extension?", + `This opens Chrome's extensions page and reveals the folder pi-chrome needs you to load.\n\nWhen the windows open, in Chrome:\n 1. Turn on 'Developer mode' (top-right toggle).\n 2. Click 'Load unpacked' and choose the folder that just opened in Finder, or paste this path:\n ${extensionPath}\n\nPress Enter to continue, or Esc to cancel.`, + ); + if (!proceed) { + ctx.ui.notify("Cancelled. You can run /chrome onboard again whenever you're ready.", "info"); + return; + } + if (process.platform === "darwin") { + await pi.exec("open", ["-a", "Google Chrome", "chrome://extensions"], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined); + await pi.exec("open", ["-R", extensionPath], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined); + await pi.exec("sh", ["-lc", `printf %s ${JSON.stringify(extensionPath)} | pbcopy`], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined); + } + ctx.ui.notify( + "Chrome and Finder should be open. The extension folder path is on your clipboard. After you click 'Load unpacked' and pick it, run /chrome doctor to confirm everything is connected.", + "info", + ); + }; + + // One-line snapshot of pi-chrome's current state. Used as a header in the bare-/chrome + // picker and as the body of /chrome status. + const statusSummary = async (): Promise => { + const parts: string[] = []; + try { + const version = (await bridge.send("tab.version", {}, 5_000)) as { extensionVersion?: string }; + if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) { + parts.push(`⚠ Chrome extension v${version.extensionVersion} (pi-chrome v${PI_CHROME_VERSION}, reload extension)`); + } else { + parts.push(`✓ Chrome connected`); + } + } catch { + parts.push(`✗ Chrome not responding`); + } + parts.push(`auth: ${authSummary()}`); + parts.push(`background: ${backgroundDefault ? "on" : "off"}`); + return parts.join(" · "); + }; + + const statusHandler = async (ctx: ExtensionContext) => { + ctx.ui.notify("Checking Chrome connection…", "info"); + ctx.ui.notify(await statusSummary(), "info"); + }; + + const openAuthorizeMenu = async (ctx: ExtensionContext): Promise => { + while (true) { + const choice = await ctx.ui.select("Authorize Chrome control", [ + "15 minutes", + "30 minutes", + "Indefinite", + "Custom minutes", + ]); + if (!choice) return; + switch (choice) { + case "15 minutes": return authorizeHandler(ctx, "15m"); + case "30 minutes": return authorizeHandler(ctx, "30m"); + case "Indefinite": return authorizeHandler(ctx, "indefinite"); + case "Custom minutes": { + const value = await ctx.ui.input("Authorize for how many minutes?", "45"); + if (!value) continue; + return authorizeHandler(ctx, value); + } + } + } + }; + + const openBackgroundMenu = async (ctx: ExtensionContext): Promise => { + const choice = await ctx.ui.select("Background / watch mode", [ + "Use Chrome in background", + "Use Chrome in foreground", + ]); + if (!choice) return; + switch (choice) { + case "Use Chrome in background": return backgroundHandler(ctx, "on"); + case "Use Chrome in foreground": return backgroundHandler(ctx, "off"); + } + }; + + const openCommandMenu = async (ctx: ExtensionContext): Promise => { + while (true) { + ctx.ui.notify("Checking Chrome connection…", "info"); + const choice = await ctx.ui.select(`pi-chrome\n${await statusSummary()}`, [ + "Authorize Chrome control…", + "Lock Chrome control", + "Doctor / troubleshoot", + "Background / watch mode…", + "Install / onboard extension", + ]); + if (!choice) return; + switch (choice) { + case "Authorize Chrome control…": await openAuthorizeMenu(ctx); continue; + case "Lock Chrome control": return revokeHandler(ctx); + case "Doctor / troubleshoot": return doctorHandler(ctx); + case "Background / watch mode…": await openBackgroundMenu(ctx); continue; + case "Install / onboard extension": return onboardHandler(ctx); + } + } + }; + + pi.registerCommand("chrome", { + description: + "All pi-chrome controls in one place.\n /chrome authorize [15m|30m||indefinite] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.", + getArgumentCompletions: (prefix) => { + const raw = prefix; + const trimmedRight = raw.replace(/\s+$/, ""); + const tokens = trimmedRight ? trimmedRight.split(/\s+/) : []; + const endsWithSpace = raw.length > 0 && raw !== trimmedRight; + // Path = completed tokens; partial = the token currently being typed (or "" if cursor sits right after a space). + const partial = endsWithSpace ? "" : (tokens.pop() ?? ""); + const path = tokens.map((t) => t.toLowerCase()); + const partialLower = partial.toLowerCase(); + + // Build candidate set with FULL argument-text values so pi-tui's apply-completion + // (which replaces the entire argument) lands correctly even for nested paths. + type Item = { fullValue: string; label: string; description: string }; + let candidates: Item[] = []; + if (path.length === 0) { + candidates = [ + { fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." }, + { fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." }, + { fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." }, + { fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." }, + { fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." }, + { fullValue: "background", label: "background", description: "Run pi-chrome in the background without focusing Chrome?" }, + ]; + } else if (path[0] === "authorize" && path.length === 1) { + candidates = [ + { fullValue: "authorize 15m", label: "15m", description: "Authorize Chrome control for 15 minutes." }, + { fullValue: "authorize 30m", label: "30m", description: "Authorize Chrome control for 30 minutes." }, + { fullValue: "authorize indefinite", label: "indefinite", description: "Authorize Chrome control until revoked or Pi exits." }, + ]; + } else if (path[0] === "background" && path.length === 1) { + candidates = [ + { fullValue: "background on", label: "on", description: "Run in background. Chrome stays in the background. Your editor keeps focus. (default)" }, + { fullValue: "background off", label: "off", description: "Bring Chrome to the front so you can watch." }, + { fullValue: "background toggle", label: "toggle", description: "Flip whichever way it's currently set." }, + { fullValue: "background status", label: "status", description: "Show the current setting." }, + ]; + } + if (candidates.length === 0) return null; + const filtered = candidates.filter((c) => c.label.toLowerCase().startsWith(partialLower)); + if (filtered.length === 0) return null; + return filtered.map((c) => ({ value: c.fullValue, label: c.label, description: c.description })); + }, + handler: async (args, ctx) => { + const tokens = (args || "").trim().split(/\s+/).filter(Boolean); + if (tokens.length === 0) { + await openCommandMenu(ctx); + return; + } + const [head, ...rest] = tokens; + const subArgs = rest.join(" "); + switch (head) { + case "authorize": return authorizeHandler(ctx, subArgs); + case "revoke": return revokeHandler(ctx); + case "status": return statusHandler(ctx); + case "doctor": return doctorHandler(ctx); + case "onboard": return onboardHandler(ctx); + case "background": + return backgroundHandler(ctx, subArgs); + case "settings": { + // Legacy nested form: /chrome settings background ... + const [setting, ...settingArgs] = rest; + if (setting === "background") return backgroundHandler(ctx, settingArgs.join(" ")); + ctx.ui.notify(`'/chrome settings' was removed. Use /chrome background directly.`, "warning"); + return; + } + default: + ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`, "warning"); + } + }, + }); + + function registerChromeTools(pi: ExtensionAPI): void { + if (chromeToolsRegistered) return; + chromeToolsRegistered = true; + + pi.registerTool({ + name: "chrome_launch", + label: "Chrome Bridge Setup", + description: + "Start/check the local bridge used by the companion Chrome extension. This does not launch a separate Chrome profile; install the unpacked Chrome extension in your existing Chrome profile to connect.", + promptSnippet: "Show instructions for connecting Pi to the user's existing Chrome profile via the companion extension.", + parameters: Type.Object({ + port: Type.Optional(Type.Number({ description: "Ignored. The bundled Chrome extension polls 127.0.0.1:17318." })), + url: Type.Optional(Type.String({ description: "Optional URL to open in the existing Chrome profile after the extension is connected." })), + userDataDir: Type.Optional(Type.String({ description: "Ignored. This bridge intentionally uses the user's existing Chrome profile through the companion extension." })), + useDefaultProfile: Type.Optional(Type.Boolean({ description: "Ignored; existing-profile access comes from the companion Chrome extension." })), + headless: Type.Optional(Type.Boolean({ description: "Ignored." })), + }), + async execute(_id, params, signal, _onUpdate, _ctx): Promise { + if (params.url && bridge.connected) { + const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS, signal); + return { content: [{ type: "text", text: `Chrome bridge connected; opened ${params.url}` }], details: { status: bridge.status(), result } }; + } + return { + content: [ + { + type: "text", + text: + `Chrome profile bridge is listening at ${bridge.url}.\n\n` + + `To connect your existing Chrome profile:\n` + + `1. Open chrome://extensions in the Chrome profile you normally use.\n` + + `2. Enable Developer mode.\n` + + `3. Click “Load unpacked”.\n` + + `4. Select: ${browserExtensionPath()}\n\n` + + `Status: ${bridge.connected ? "connected" : "waiting for extension"}.`, + }, + ], + details: { status: bridge.status(), extensionPath: browserExtensionPath() }, + }; + }, + }); + + pi.registerTool({ + name: "chrome_tab", + label: "Chrome Tab", + description: "List, create, activate, close, group, ungroup, or inspect tabs in the user's existing Chrome profile via the companion extension. New/grouped tabs always use this session's Pi tab group. activate/close/group/ungroup require a target (targetId/urlIncludes/titleIncludes); with no target they act on this session's pi-chrome automation tab if one exists, and otherwise error rather than touching the user's active tab.", + promptSnippet: "List/open/activate/close/group existing Chrome tabs through the companion extension.", + parameters: Type.Object({ + action: StringEnum(tabActionValues), + url: Type.Optional(Type.String({ description: "URL for action=new." })), + targetId: Type.Optional(Type.String({ description: "Chrome tab id for activate/close/group/ungroup." })), + urlIncludes: Type.Optional(Type.String({ description: "Match the target tab by URL substring for activate/close/group/ungroup." })), + titleIncludes: Type.Optional(Type.String({ description: "Match the target tab by title substring for activate/close/group/ungroup." })), + group: Type.Optional(Type.Boolean({ description: "Deprecated; ignored. Pi-created tabs always join this session's own tab group." })), + groupTitle: Type.Optional(Type.String({ description: "Deprecated for action=new/group; ignored so one Pi session uses one tab group ('Pi Session: ')." })), + groupColor: Type.Optional(Type.String({ description: "Tab group color for action=group/new: grey, blue, red, yellow, green, pink, purple, cyan, or orange. Defaults to blue." })), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal, _onUpdate, ctx): Promise { + const forwarded = { ...params } as typeof params & { groupTitle?: string }; + // Force every Pi-opened/explicitly-grouped tab into this session's own group, + // named after the session display name (falling back to the session id). There is + // intentionally no opt-out: one Pi session should create/use one tab group. + if (params.action === "new" || params.action === "group") { + forwarded.groupTitle = sessionGroupTitle(ctx); + } + const result = await authorizedBridgeSend(`tab.${params.action}`, forwarded, DEFAULT_TIMEOUT_MS, signal); + if (params.action === "list") { + const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number; group?: { title?: string } | null }>; + const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.group?.title ? `[${tab.group.title}] ` : ""}${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs."; + return { content: [{ type: "text", text }], details: { tabs } }; + } + if (params.action !== "version") await invalidateSnapshotCapture(); + return { content: [{ type: "text", text: safeJson(result) }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_snapshot", + label: "Chrome Snapshot", + description: + "Capture the current Chrome page into a private, session-local JSON file instead of placing the raw DOM snapshot in model context. The result returns a path for immediate ctx_execute_file analysis. Each target keeps only its latest capture; page interaction makes it stale. Use mode/query/nearUid to control what Chrome captures. Runs in the background by default; pass background=false so the user can watch.", + promptSnippet: "Save the current Chrome page to a session-local JSON capture, then analyze that path with ctx_execute_file for only the required uids and state.", + parameters: Type.Object({ + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS })), + mode: Type.Optional(StringEnum(snapshotModeValues)), + query: Type.Optional(Type.String({ description: "Find/rank elements, regions, and text matching this phrase, e.g. 'merge button', 'email error', 'approve PR'." })), + maxTextChars: Type.Optional(Type.Number({ description: "Max body text chars included in the underlying snapshot. Defaults are smaller for concise modes." })), + containingText: Type.Optional(Type.String({ description: "Only return elements whose label/text contains this string (case-insensitive). Useful when the page has many controls." })), + roleFilter: Type.Optional(Type.String({ description: "Only return elements matching this ARIA role or tag name (case-insensitive). e.g. 'button', 'link', 'textbox'." })), + nearUid: Type.Optional(Type.String({ description: "Sort elements by proximity to this snapshot uid. Useful for finding controls near a known anchor." })), + background: Type.Optional( + Type.Boolean({ description: "If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const snapshot = await authorizedBridgeSend( + "page.snapshot", + withBackground({ ...params, maxElements: params.maxElements ?? MAX_ELEMENTS }), + DEFAULT_TIMEOUT_MS, + signal, + ); + const capture = await saveSnapshotCapture(snapshot, params); + return { content: [{ type: "text", text: formatSnapshotCapture(capture) }], details: { capture } }; + }, + }); + + pi.registerTool({ + name: "chrome_find", + label: "Chrome Find", + description: + "Find elements, regions, or text on the live Chrome page. Returns only compact ranked matches with stable uids and coordinates; the full structured snapshot is saved to the same private session-local file used by chrome_snapshot for optional ctx_execute_file analysis.", + promptSnippet: "Find live Chrome controls/text/regions and return compact stable-uid matches while keeping the full snapshot out of model context.", + parameters: Type.Object({ + query: Type.String({ description: "What to find, e.g. 'merge button', 'email error', 'approve PR', 'search box'." }), + mode: Type.Optional(StringEnum(snapshotModeValues)), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const snapshot = await authorizedBridgeSend( + "page.snapshot", + withBackground({ ...params, mode: params.mode || "auto", maxElements: params.maxElements ?? MAX_ELEMENTS }), + DEFAULT_TIMEOUT_MS, + signal, + ); + const capture = await saveSnapshotCapture(snapshot, params); + return { + content: [{ type: "text", text: `${formatChromeFindMatches(snapshot)}\n\n${formatSnapshotCapture(capture)}` }], + details: { capture }, + }; + }, + }); + + pi.registerTool({ + name: "chrome_inspect", + label: "Chrome Inspect Element", + description: + "Inspect one snapshot uid or selector deeply: nearby text, nearby actions, form context, ancestors, and suggested click target. Use after chrome_snapshot/chrome_find when you need context around one element.", + promptSnippet: "Inspect a Chrome snapshot uid deeply for nearby text, form context, and suggested actions.", + parameters: Type.Object({ + uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot/chrome_find." })), + selector: Type.Optional(Type.String({ description: "CSS selector if uid is unavailable." })), + scrollIntoView: Type.Optional(Type.Boolean({ description: "If true, scroll the target into view before inspecting. Default false to avoid changing page state." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + try { + const inspect = await authorizedBridgeSend("page.inspect", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + return { content: [{ type: "text", text: formatChromeInspect(inspect) }], details: { inspect } }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!/Unknown action: page\.inspect/i.test(message)) throw error; + // Compatibility fallback for a loaded Chrome extension service worker that has not + // been reloaded since chrome_inspect was added. It is less rich than page.inspect, + // but still gives useful nearby candidates instead of failing the workflow. + const snapshot = await authorizedBridgeSend( + "page.snapshot", + withBackground({ + ...params, + mode: "interactive", + maxElements: MAX_ELEMENTS, + nearUid: params.uid, + query: params.selector, + }), + DEFAULT_TIMEOUT_MS, + signal, + ); + const capture = await saveSnapshotCapture(snapshot, params); + const text = `chrome_inspect fallback: loaded Chrome extension does not yet support page.inspect; reload it at chrome://extensions for deep inspect.\n\n${formatSnapshotCapture(capture)}`; + return { content: [{ type: "text", text }], details: { capture, fallback: "page.snapshot" } }; + } + }, + }); + + pi.registerTool({ + name: "chrome_navigate", + label: "Chrome Navigate", + description: + "Navigate a Chrome tab to a URL via the companion extension. With no target, navigation goes to pi-chrome's own dedicated automation window/tab — it never replaces the user's active tab. Pass targetId/urlIncludes/titleIncludes only to act on a specific existing tab. Runs in the background by default; pass background=false to focus Chrome and activate the tab so the user can watch. Optionally waits for load completion.", + promptSnippet: "Navigate a Chrome tab in the user's existing profile.", + parameters: Type.Object({ + url: Type.String(), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + waitUntilLoad: Type.Optional(Type.Boolean({ default: true })), + timeoutMs: Type.Optional(Type.Number({ default: 15_000 })), + initScript: Type.Optional(Type.String({ description: "Optional JavaScript source to run in MAIN world at document_start of the next navigation. Useful for seeding localStorage, stubbing Date.now(), or defining navigator.webdriver=undefined. Requires the companion extension's webNavigation permission." })), + background: Type.Optional( + Type.Boolean({ description: "If true, navigate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_evaluate", + label: "Chrome Evaluate", + description: + "Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context and returns JSON-serializable values when possible. Runs in the background by default; pass background=false to focus Chrome and activate the tab.", + promptSnippet: "Evaluate JavaScript in the active Chrome tab through the companion extension.", + parameters: Type.Object({ + expression: Type.String(), + awaitPromise: Type.Optional(Type.Boolean({ default: true })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true, evaluate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + const text = value === undefined + ? "undefined" + : typeof value === "string" + ? value + : safeJson(value) ?? "undefined"; + return { content: [{ type: "text", text: truncateText(text) }], details: { value: value as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_click", + label: "Chrome Click", + description: + "Click a snapshot uid, CSS selector, or viewport coordinate using Chrome's real input layer. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-click snapshot to the session-local file and return its ctx_execute_file path, never the raw snapshot.", + promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.", + parameters: Type.Object({ + uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })), + selector: Type.Optional(Type.String({ description: "CSS selector to click. Prefer uid from chrome_snapshot when available." })), + x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })), + y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })), + domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })), + includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-click snapshot to the session-local capture file and return its path." })), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true, click silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json; + const summary = summarizeActionResult(result); + const target = params.uid ?? params.selector ?? `${params.x},${params.y}`; + const text = summary ? `Clicked ${target} — ${summary}` : `Clicked ${target}`; + return formatActionWithCapture(raw, params, text); + }, + }); + + pi.registerTool({ + name: "chrome_type", + label: "Chrome Type", + description: + "Focus an optional snapshot uid or CSS selector, then type text using Chrome's real keyboard input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-type snapshot and return its ctx_execute_file path.", + promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.", + parameters: Type.Object({ + text: Type.String(), + uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })), + selector: Type.Optional(Type.String({ description: "CSS selector to focus before typing." })), + includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-type snapshot to the session-local capture file and return its path." })), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })), + pressEnter: Type.Optional(Type.Boolean()), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true, type silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json; + const summary = summarizeActionResult(result); + const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : ""; + const base = `Typed ${params.text.length} character(s)${into}.`; + const text = summary ? `${base} (${summary})` : base; + return formatActionWithCapture(raw, params, text); + }, + }); + + pi.registerTool({ + name: "chrome_fill", + label: "Chrome Fill", + description: + "Set the full value of a text input, textarea, or contenteditable element using Chrome input. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-fill snapshot and return its ctx_execute_file path.", + promptSnippet: "Fill a Chrome form field by snapshot uid or selector, optionally returning a fresh snapshot.", + parameters: Type.Object({ + text: Type.String(), + uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot." })), + selector: Type.Optional(Type.String({ description: "CSS selector to fill if uid is omitted." })), + submit: Type.Optional(Type.Boolean({ description: "If true, press Enter after filling." })), + domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM value-setting if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })), + includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-fill snapshot to the session-local capture file and return its path." })), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true, fill silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json; + const summary = summarizeActionResult(result); + const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : ""; + const base = `Filled ${params.text.length} character(s)${into}.`; + const text = summary ? `${base} (${summary})` : base; + return formatActionWithCapture(raw, params, text); + }, + }); + + pi.registerTool({ + name: "chrome_key", + label: "Chrome Key", + description: + "Send a keyboard key to an existing Chrome tab. The previous capture becomes stale. Pass includeSnapshot=true to save a fresh post-key snapshot and return its ctx_execute_file path. Runs in the background by default; pass background=false so the user can watch.", + promptSnippet: "Press keys in Chrome through the companion extension.", + parameters: Type.Object({ + key: Type.String(), + modifiers: Type.Optional(Type.Object({ + shiftKey: Type.Optional(Type.Boolean()), + ctrlKey: Type.Optional(Type.Boolean()), + altKey: Type.Optional(Type.Boolean()), + metaKey: Type.Optional(Type.Boolean()), + }, { description: "Modifier keys to hold while pressing the key (chord)." })), + includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, save a fresh post-key snapshot to the session-local capture file and return its path." })), + maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true, send the key silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json; + const summary = summarizeActionResult(result); + const base = `Pressed ${params.key}.`; + const text = summary ? `${base} (${summary})` : base; + return formatActionWithCapture(raw, params, text); + }, + }); + + pi.registerTool({ + name: "chrome_wait_for", + label: "Chrome Wait For", + description: "Poll an existing Chrome tab until a selector exists or a JavaScript expression returns truthy.", + promptSnippet: "Wait for page state in Chrome before further automation.", + parameters: Type.Object({ + kind: StringEnum(waitForValues), + value: Type.String({ description: "CSS selector when kind=selector; JavaScript expression when kind=expression." }), + timeoutMs: Type.Optional(Type.Number({ default: 10_000 })), + intervalMs: Type.Optional(Type.Number({ default: 250 })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_list_console_messages", + label: "Chrome Console Messages", + description: + "List console messages captured in the page by the companion extension. Capture starts after any chrome_snapshot, chrome_evaluate, chrome_list_console_messages, or chrome_list_network_requests call installs page instrumentation.", + promptSnippet: "List captured console messages from the active Chrome page.", + parameters: Type.Object({ + clear: Type.Optional(Type.Boolean({ description: "Clear the captured console log after reading." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.console.list", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_list_network_requests", + label: "Chrome Network Requests", + description: + "List fetch/XMLHttpRequest activity captured in the page by the companion extension. Capture starts after instrumentation is installed by snapshot/evaluate/network/console tools; browser document/static asset requests are not captured. Use includePreservedRequests=true to keep requests from earlier same-tab navigations that were captured before navigation.", + promptSnippet: "List captured XHR/fetch requests from the active Chrome page before doing DOM-heavy debugging.", + parameters: Type.Object({ + includePreservedRequests: Type.Optional(Type.Boolean({ description: "Include captured requests from earlier locations in the same tab/session." })), + clear: Type.Optional(Type.Boolean({ description: "Clear the captured request log after reading." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.network.list", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_get_network_request", + label: "Chrome Network Request", + description: "Retrieve one captured fetch/XMLHttpRequest entry, including response body when available, by requestId from chrome_list_network_requests.", + promptSnippet: "Fetch captured request details and response body by requestId.", + parameters: Type.Object({ + requestId: Type.String({ description: "Request id returned by chrome_list_network_requests." }), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.network.get", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_screenshot", + label: "Chrome Screenshot", + description: + "Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk. Chrome's extension screenshot API requires the target tab to be the active tab in its window. Runs in the background by default (the tab is briefly activated within its window for the capture, then the previous active tab is restored); pass background=false to focus Chrome so the user can watch.", + promptSnippet: "Capture Chrome screenshots and save them under .pi/chrome-screenshots by default.", + parameters: Type.Object({ + path: Type.Optional(Type.String({ description: "Output path. Defaults to .pi/chrome-screenshots/.." })), + format: Type.Optional(StringEnum(imageFormatValues)), + quality: Type.Optional(Type.Number({ description: "JPEG quality 0-100." })), + fullPage: Type.Optional(Type.Boolean({ description: "Not supported by the extension bridge yet; viewport screenshots are captured." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional( + Type.Boolean({ description: "If true (the default), capture silently without focusing the Chrome window (the target tab is briefly activated within its window for the capture, then restored); pass false to focus Chrome." }), + ), + host: Type.Optional(Type.String()), + port: Type.Optional(Type.Number()), + }), + async execute(_id, params, signal, _onUpdate, ctx: ExtensionContext): Promise { + const format = params.format ?? "png"; + const cwd = workspaceCwd(ctx); + const defaultPath = join(cwd, ".pi", "chrome-screenshots", `${new Date().toISOString().replace(/[:.]/g, "-")}.${format}`); + const outputPath = params.path ? resolve(cwd, params.path) : defaultPath; + const result = (await authorizedBridgeSend("page.screenshot", withBackground(params), params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS, signal)) as { + dataUrl?: string; + tab?: unknown; + fullPage?: boolean; + dimensions?: { width: number; height: number; viewportHeight: number; dpr: number }; + tiles?: Array<{ y: number; dataUrl: string }>; + }; + await mkdir(dirname(outputPath), { recursive: true }); + if (result.fullPage && result.tiles && result.dimensions) { + // Stitch via PNG if format is png; otherwise we fall back to writing tile files and a + // manifest. We avoid pulling in an image library by writing each tile next to the main + // path with a -tileN suffix and a stitched.json manifest. + const { width, height, viewportHeight, dpr } = result.dimensions; + const manifest: Array<{ path: string; y: number }> = []; + for (let i = 0; i < result.tiles.length; i++) { + const tile = result.tiles[i]; + const tilePath = outputPath.replace(/(\.[^.]+)$/, `-tile${i}$1`); + const base64 = tile.dataUrl.replace(/^data:image\/(?:png|jpeg);base64,/, ""); + await writeFile(tilePath, Buffer.from(base64, "base64")); + manifest.push({ path: tilePath, y: tile.y }); + } + await writeFile(outputPath + ".json", JSON.stringify({ width, height, viewportHeight, dpr, tiles: manifest }, null, 2)); + return { + content: [{ type: "text", text: `Saved ${result.tiles.length} full-page tile(s) for ${width}×${height}px page. Manifest: ${outputPath}.json` }], + details: { manifest: outputPath + ".json", tiles: manifest, dimensions: result.dimensions, tab: result.tab } as unknown as Record, + }; + } + if (!result.dataUrl) throw new Error("Screenshot returned no dataUrl"); + const base64 = result.dataUrl.replace(/^data:image\/(?:png|jpeg);base64,/, ""); + await writeFile(outputPath, Buffer.from(base64, "base64")); + return { content: [{ type: "text", text: `Saved Chrome screenshot to ${outputPath}` }], details: { path: outputPath, format, tab: result.tab } }; + }, + }); + + pi.registerTool({ + name: "chrome_hover", + label: "Chrome Hover", + description: "Hover over an element by uid, selector, or x/y using Chrome pointer movement.", + promptSnippet: "Hover a Chrome element to trigger :hover / mouseover handlers.", + parameters: Type.Object({ + uid: Type.Optional(Type.String()), + selector: Type.Optional(Type.String()), + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_drag", + label: "Chrome Drag", + description: "Drag from one uid/selector/point to another using Chrome pointer input.", + promptSnippet: "Drag a Chrome element from one point to another.", + parameters: Type.Object({ + fromUid: Type.Optional(Type.String()), + fromSelector: Type.Optional(Type.String()), + fromX: Type.Optional(Type.Number()), + fromY: Type.Optional(Type.Number()), + toUid: Type.Optional(Type.String()), + toSelector: Type.Optional(Type.String()), + toX: Type.Optional(Type.Number()), + toY: Type.Optional(Type.Number()), + steps: Type.Optional(Type.Number({ default: 12 })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_tap", + label: "Chrome Tap (Touch)", + description: + "Dispatch a real touchstart/touchend tap through Chrome's input layer. Use for sites that gate on TouchEvent rather than MouseEvent (mobile-first PWAs, swipe carousels). Chrome may show its debugging banner while attached.", + promptSnippet: "Tap (real touch) a Chrome element by snapshot uid, selector, or coordinate.", + parameters: Type.Object({ + uid: Type.Optional(Type.String()), + selector: Type.Optional(Type.String()), + x: Type.Optional(Type.Number()), + y: Type.Optional(Type.Number()), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + const target = params.uid ?? params.selector ?? `${params.x},${params.y}`; + return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_scroll", + label: "Chrome Scroll", + description: "Scroll the page or a specific scrollable element by dispatching real wheel events with momentum-shaped deltas, then applying the scroll. Positive deltaY scrolls down. Pass uid/selector to scroll within a container, otherwise the document scrolls.", + promptSnippet: "Scroll a Chrome page or container via wheel events (not raw scrollTop).", + parameters: Type.Object({ + uid: Type.Optional(Type.String()), + selector: Type.Optional(Type.String()), + deltaY: Type.Optional(Type.Number({ description: "Pixels to scroll vertically. Positive = down." })), + deltaX: Type.Optional(Type.Number({ description: "Pixels to scroll horizontally. Positive = right." })), + steps: Type.Optional(Type.Number({ description: "Number of wheel events to dispatch. Defaults to ceil(|deltaY|/100)." })), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean()), + }), + async execute(_id, params, signal): Promise { + const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } }; + }, + }); + + pi.registerTool({ + name: "chrome_upload_file", + label: "Chrome Upload File", + description: "Attach local files to an element using Chrome DevTools file-input control. Does NOT open the native file picker; works with React/Vue/Angular controlled inputs.", + promptSnippet: "Attach local files to a Chrome without opening the native file picker.", + parameters: Type.Object({ + uid: Type.Optional(Type.String()), + selector: Type.Optional(Type.String()), + paths: Type.Array(Type.String(), { description: "Local absolute file paths to upload." }), + targetId: Type.Optional(Type.String()), + urlIncludes: Type.Optional(Type.String()), + titleIncludes: Type.Optional(Type.String()), + background: Type.Optional(Type.Boolean()), + }), + async execute(_id, params, signal, _onUpdate, ctx): Promise { + const cwd = workspaceCwd(ctx); + const paths = params.paths.map((p) => resolve(cwd, p)); + const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS, signal); + await invalidateSnapshotCapture(params); + return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } }; + }, + }); + } + +} diff --git a/pi-chrome/package-lock.json b/pi-chrome/package-lock.json new file mode 100644 index 0000000..e93cbdd --- /dev/null +++ b/pi-chrome/package-lock.json @@ -0,0 +1,38 @@ +{ + "name": "pi-chrome", + "version": "0.15.46", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pi-chrome", + "version": "0.15.46", + "license": "MIT", + "devDependencies": { + "jiti": "2.7.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*", + "typebox": "*" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-coding-agent": { + "optional": true + }, + "typebox": { + "optional": true + } + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + } + } +} diff --git a/pi-chrome/package.json b/pi-chrome/package.json new file mode 100644 index 0000000..014adc5 --- /dev/null +++ b/pi-chrome/package.json @@ -0,0 +1,86 @@ +{ + "name": "pi-chrome", + "version": "0.15.46", + "scripts": { + "test": "node test-suite/unit/csp-eval.test.mjs && node test-suite/unit/automation-target.test.mjs && jiti test/session-snapshot-store.test.ts", + "version": "node scripts/sync-manifest-version.js", + "prepublishOnly": "node scripts/sync-manifest-version.js" + }, + "description": "Let Pi use your existing signed-in Chrome profile after explicit authorization.", + "keywords": [ + "pi", + "pi-package", + "pi-extension", + "pi-agent", + "chrome", + "chrome-extension", + "browser", + "browser-automation", + "browser-control", + "web-automation", + "agent", + "ai-agent", + "llm-agent", + "llm-tools", + "agentic", + "playwright-alternative", + "puppeteer-alternative", + "selenium-alternative", + "cdp-alternative", + "authenticated-session", + "real-profile", + "web-debugging", + "web-scraping", + "screenshot", + "automation-tools", + "browser-use", + "stagehand-alternative" + ], + "license": "MIT", + "author": { + "name": "tianrendong", + "company": "Earendil Inc." + }, + "homepage": "https://github.com/tianrendong/pi-chrome#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/tianrendong/pi-chrome.git" + }, + "bugs": { + "url": "https://github.com/tianrendong/pi-chrome/issues" + }, + "type": "commonjs", + "files": [ + "extensions", + "src", + "test", + "docs", + "test-suite", + "README.md", + "CHANGELOG.md", + "CONTRIBUTING.md", + "SECURITY.md", + "LICENSE", + "UPSTREAM.md" + ], + "pi": { + "extensions": [ + "./extensions/chrome-profile-bridge/index.ts" + ] + }, + "devDependencies": { + "jiti": "2.7.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "*", + "typebox": "*" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-coding-agent": { + "optional": true + }, + "typebox": { + "optional": true + } + } +} diff --git a/pi-chrome/scripts/sync-manifest-version.js b/pi-chrome/scripts/sync-manifest-version.js new file mode 100644 index 0000000..b5515af --- /dev/null +++ b/pi-chrome/scripts/sync-manifest-version.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +// Sync browser-extension/manifest.json version from package.json. +// Run automatically by the `version` npm lifecycle hook (npm version ), +// and pre-publish, so the Chrome extension version never drifts from the npm +// package version. +const { readFileSync, writeFileSync } = require("node:fs"); +const { join } = require("node:path"); + +const pkgPath = join(__dirname, "..", "package.json"); +const manifestPath = join( + __dirname, + "..", + "extensions", + "chrome-profile-bridge", + "browser-extension", + "manifest.json", +); + +const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + +if (manifest.version === pkg.version) { + console.log(`manifest.json already at ${pkg.version}`); + process.exit(0); +} + +const prev = manifest.version; +manifest.version = pkg.version; +writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); +console.log(`manifest.json ${prev} -> ${pkg.version}`); + +// Stage so `npm version` includes manifest bump in the version commit. +try { + require("node:child_process").execSync(`git add "${manifestPath}"`, { stdio: "ignore" }); +} catch { + /* not in a git checkout - ok */ +} diff --git a/pi-chrome/src/session-snapshot-store.ts b/pi-chrome/src/session-snapshot-store.ts new file mode 100644 index 0000000..c50d009 --- /dev/null +++ b/pi-chrome/src/session-snapshot-store.ts @@ -0,0 +1,140 @@ +import { createHash, randomUUID } from "node:crypto"; +import { chmod, mkdir, rename, rm, writeFile } from "node:fs/promises"; +import { join, relative, resolve, sep } from "node:path"; + +type SnapshotObject = Record; + +export type SnapshotTarget = { + targetId?: string; + urlIncludes?: string; + titleIncludes?: string; +}; + +export type SnapshotCaptureMetadata = { + version: 1; + captureId: string; + capturedAt: string; + path: string; + bytes: number; + targetKey: string; + targetId?: string | number; + mode?: string; + elementCount: number; + matchCount: number; + staleAfterInteraction: true; +}; + +export type StoredSnapshotCapture = { + metadata: SnapshotCaptureMetadata; + absolutePath: string; +}; + +export type SnapshotCaptureFile = { + version: 1; + captureId: string; + capturedAt: string; + targetKey: string; + snapshot: unknown; +}; + +function hashSegment(value: string): string { + return createHash("sha256").update(value).digest("hex").slice(0, 20); +} + +function normalizeRelativePath(path: string): string { + return path.split(sep).join("/"); +} + +function snapshotObject(snapshot: unknown): SnapshotObject | undefined { + return snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) + ? snapshot as SnapshotObject + : undefined; +} + +export function snapshotTargetKey(target: SnapshotTarget = {}): string { + if (target.targetId) return `target:${target.targetId}`; + if (target.urlIncludes) return `url:${target.urlIncludes}`; + if (target.titleIncludes) return `title:${target.titleIncludes}`; + return "default"; +} + +export class SessionSnapshotStore { + readonly workspaceRoot: string; + readonly sessionRoot: string; + readonly sessionHash: string; + + constructor(workspaceRoot: string, sessionId: string) { + this.workspaceRoot = resolve(workspaceRoot); + this.sessionHash = hashSegment(sessionId || "unknown-session"); + this.sessionRoot = join(this.workspaceRoot, ".pi", "chrome-context", this.sessionHash); + } + + async save(snapshot: unknown, target: SnapshotTarget = {}): Promise { + const targetKey = snapshotTargetKey(target); + const targetDirectory = join(this.sessionRoot, hashSegment(targetKey)); + const absolutePath = join(targetDirectory, "snapshot.json"); + const temporaryPath = join(targetDirectory, `.snapshot.${process.pid}.${randomUUID()}.tmp`); + const captureId = randomUUID(); + const capturedAt = new Date().toISOString(); + const capture: SnapshotCaptureFile = { + version: 1, + captureId, + capturedAt, + targetKey, + snapshot, + }; + const serialized = `${JSON.stringify(capture, null, 2)}\n`; + + await mkdir(targetDirectory, { recursive: true, mode: 0o700 }); + await chmod(this.sessionRoot, 0o700).catch(() => undefined); + await chmod(targetDirectory, 0o700).catch(() => undefined); + try { + await writeFile(temporaryPath, serialized, { encoding: "utf8", mode: 0o600, flag: "wx" }); + await chmod(temporaryPath, 0o600).catch(() => undefined); + await rename(temporaryPath, absolutePath); + await chmod(absolutePath, 0o600).catch(() => undefined); + } catch (error) { + await rm(temporaryPath, { force: true }).catch(() => undefined); + throw error; + } + + const object = snapshotObject(snapshot); + const elements = Array.isArray(object?.elements) ? object.elements : []; + const matches = Array.isArray(object?.matches) ? object.matches : []; + const targetId = typeof object?.targetId === "string" || typeof object?.targetId === "number" + ? object.targetId + : undefined; + const mode = typeof object?.mode === "string" ? object.mode : undefined; + const path = normalizeRelativePath(relative(this.workspaceRoot, absolutePath)); + + return { + absolutePath, + metadata: { + version: 1, + captureId, + capturedAt, + path, + bytes: Buffer.byteLength(serialized), + targetKey, + targetId, + mode, + elementCount: elements.length, + matchCount: matches.length, + staleAfterInteraction: true, + }, + }; + } + + async invalidate(target?: SnapshotTarget): Promise { + if (!target) { + await this.clear(); + return; + } + const targetDirectory = join(this.sessionRoot, hashSegment(snapshotTargetKey(target))); + await rm(targetDirectory, { recursive: true, force: true }); + } + + async clear(): Promise { + await rm(this.sessionRoot, { recursive: true, force: true }); + } +} diff --git a/pi-chrome/test-suite/README.md b/pi-chrome/test-suite/README.md new file mode 100644 index 0000000..47e737f --- /dev/null +++ b/pi-chrome/test-suite/README.md @@ -0,0 +1,194 @@ +# pi-chrome browser-control benchmark + +Static benchmark pages for evaluating tools that let agents control Chrome. The suite has two layers: + +1. **Unit challenges** (`manifest.json`) — MiniWoB-style capability probes for + forms, scroll containers, contenteditable, files, frames, Shadow DOM, + network/console inspection, `isTrusted`, user activation, pointer paths, key + cadence, native controls, drag/drop, touch, paste, and scroll momentum. +2. **Long-horizon hermetic tasks** (`task-manifest.json`) — WebArena / + BrowserGym-inspired multi-step tasks with fresh run IDs and deterministic + programmatic graders. + +## Run + +```bash +cd test-suite +python3 -m http.server 8765 +# open http://127.0.0.1:8765/ in the Chrome window pi-chrome controls +``` + +Each challenge page exposes: + +- `window.__challenge` — id +- `window.__verdict` — `"PENDING" | "PASS" | "FAIL" | "SKIP" | "WARN"` +- `window.__reason` — array of reasons +- `window.__events` — raw event log for forensics + +`manifest.json` is the source of truth for unit-challenge metadata: category, +gate bucket, goal, expected result per mode, prerequisites, flake risk, manual +baseline status, and canonical tool recipe. `manifest.schema.json` documents the +manifest shape. Recipes express tool intent; runners may need to adapt +descriptive selectors (e.g. shadow/iframe notation), dynamic tab ids, and expand +path placeholders like `$PWD`. + +`task-manifest.json` is the source of truth for long-horizon tasks: BrowserGym-style +`taskId`, seed, viewport, goal object, difficulty tier, max steps, declared +action subsets, reset/setup URL, validate hook, optional cheat recipe, and +programmatic grader expression. `task-manifest.schema.json` documents this shape. +`browsergym-action-space.json` records BrowserGym-compatible action subsets. + +## Modes / expected outcomes + +The same page can have different expected results depending on tool capability: + +- `synthetic` — DOM-dispatched events / framework-aware setters. Fast and quiet. +- `trusted` — browser-trusted input, usually via `chrome.debugger`/CDP. Can show + Chrome's debugging banner. +- `manual` — human baseline in same browser/profile. + +Expected values in `manifest.json`: + +- `PASS` / `FAIL` — deterministic target for that mode. +- `CONDITIONAL` — depends on browser policy, OS, device capability, permissions, + or an unreleased tool primitive. Inspect `prerequisites`, `notes`, and + `flakeRisk`. + +Manual baselines are tracked separately with `manualBaseline`. `unverified` +means the manual expectation is a target, not a recorded contract. + +## Gate buckets + +Each unit challenge has a `gate` field: + +- `core` — required release blocker for normal trusted-mode pi-chrome shipping. +- `conditional` — blocks only when declared prerequisites/capabilities are present + (clipboard, touch, dialogs, native UI, etc.). +- `quality` — adversarial humanization/fingerprint signal. Track regressions, but + do not block general ship without an explicit product decision. + +## Recommended unit-challenge agent flow + +1. Navigate to dashboard: + `http://127.0.0.1:8765/`. +2. Pick mode (`synthetic`, `trusted`, or `manual`) and clear local verdicts. +3. For each manifest row: + - `chrome_navigate` to `http://127.0.0.1:8765/`. + - `chrome_snapshot` before acting; prefer snapshot `uid` over raw selector. + - Execute the listed `recipe`, adapting descriptive frame/shadow selectors to + whatever selectors/uids the tool exposes. + - Read: + ```js + JSON.stringify({ + v: window.__verdict, + r: window.__reason, + e: window.__events?.slice(-20) + }) + ``` +4. Return to dashboard and compare actual verdicts with expected values. +5. Copy JSON report from dashboard for PRs or regression notes. + +## Recommended long-horizon task flow + +1. Load `task-manifest.json`. +2. Replace `$RUN_ID` in `startUrl` with a fresh value. +3. Navigate to the start URL and read the visible task instruction. +4. Solve using normal browser tools only; avoid direct state mutation unless the + benchmark mode explicitly allows evaluate-based actions. +5. Click **Grade now** or evaluate the task grader expression: + ```js + JSON.stringify({ v: window.__taskVerdict, r: window.__taskReason }) + ``` +6. Record action count, observations used, tools used, verdict, and reason. + +## Design principles copied from browser-agent benchmarks + +- Prefer hermetic sites and deterministic graders over live sites and LLM judges. +- Report action API and observation format; these strongly affect scores. +- Use difficulty tiers: L1 atomic, L2 compositional, L3 cross-page/context-rich. +- Include tedious cross-page memory and exact-value transfer tasks; short unit + probes hide these failures. +- Keep synthetic-event-gated tests because extension bridges face failures that + CDP/Playwright-style benchmarks usually do not measure. + +## Challenge categories + +- `trusted-input` — browser-trusted click/key events. +- `pointer-humanization` — paths, coordinates, movement continuity/rate. +- `keyboard` / `focus-keyboard` — typing fidelity, modifiers, Tab flows. +- `activation-gates` — clipboard/fullscreen/user activation. +- `scroll` / `scroll-visibility` — wheel events, momentum, IntersectionObserver. +- `drag-drop` — HTML5 drag/drop + `DataTransfer`. +- `clipboard` — OS/browser paste path. +- `native-controls` — controls that should use browser UI/keyboard semantics. +- `frameworks` / `editing` — React-style value tracking, contenteditable. +- `dom-complexity` / `frames` — Shadow DOM and iframe targeting. +- `files` — file attachment to ``. +- `observability` — console/network capture tools. +- `csp` — strict Content Security Policy: screenshot/coordinate fallback (39) and the CDP eval/snapshot bypass that works under `script-src 'self'` without `unsafe-eval` (42). +- `lazy-loading` — dynamic DOM readiness and wait behavior. +- `fingerprint` — environment and stack fingerprint probes. +- `agent-safety` — hidden honeypots and safe target selection. + +## Current challenge inventory + +The dashboard renders this from `manifest.json`. In brief: + +1. trusted click +2. trusted keyboard +3. webdriver/runtime flags +4. mouse entropy before click +5. click timing +6. click coordinate variation +7. pointer event properties +8. keyboard cadence +9. beforeinput/input order +10. user activation gates +11. honeypot safety +12. fingerprint consistency +13. focus order +14. wheel scroll +15. drag/drop `DataTransfer` +16. contenteditable selection +17. paste clipboard +18. native select +19. hover dwell +20. React value tracker +21. keyboard modifiers +22. touch events +23. stack trace fingerprint +24. viewport click coordinates +25. pointer continuity +26. mousemove rate +27. scroll momentum +28. intersection visibility +29. Shadow DOM controls +30. iframe targeting +31. file upload +32. keyboard Tab navigation +33. network/console capture +34. dialog handling +35. target blank popup +36. modal focus trap +37. autocomplete combobox +38. SPA route change +39. strict CSP screenshot/coordinate fallback +40. dynamic wait/readiness +41. explicit tab lifecycle +42. strict CSP eval/snapshot via CDP (regression guard for the CSP bypass) + +## Design notes + +- A failure is useful only when compared to expected mode. Example: synthetic + `isTrusted` failing is expected and validates that the test detects quiet DOM + events. +- Some tests are capability-gated. Example: touch tests should be `SKIP`/manual + conditional on non-touch hardware. +- Fingerprint tests should warn before blocking. Real Chrome profiles can use + software WebGL in VMs, remote desktops, or policy-constrained environments. +- `notes/bypass-ideas.md` is historical guidance for older synthetic-only + versions. Prefer `manifest.json` for current expected outcomes. +- `notes/browsergym-compat.md` defines the reset/step/validate/observation/BID + contract for external BrowserGym-style agents. +- `notes/runner-spec.md`, `notes/scoring.md`, and `notes/profiles.md` define + runner output, scoring, retry policy, and environment metadata. diff --git a/pi-chrome/test-suite/_lib.js b/pi-chrome/test-suite/_lib.js new file mode 100644 index 0000000..f3845e5 --- /dev/null +++ b/pi-chrome/test-suite/_lib.js @@ -0,0 +1,130 @@ +// Tiny shared harness for challenge pages. +// Each page calls Challenge.init({id, instructions}) then Challenge.pass()/fail() +// based on its own listeners. +(function () { + const events = []; + const state = { + id: null, + verdict: "PENDING", + reason: [], + events, + details: [], + thresholds: {}, + }; + + function render() { + const el = document.getElementById("__verdict"); + if (!el) return; + el.textContent = state.verdict; + el.dataset.verdict = state.verdict; + el.style.background = + state.verdict === "PASS" ? "#1f7a1f" : + state.verdict === "FAIL" ? "#a11" : + state.verdict === "SKIP" ? "#76520b" : + state.verdict === "WARN" ? "#6b5d00" : "#444"; + el.style.color = "#fff"; + const r = document.getElementById("__reason"); + if (r) r.textContent = state.reason.join("\n"); + } + + function log(name, detail) { + events.push({ t: performance.now(), name, ...detail }); + if (events.length > 500) events.shift(); + } + + const Challenge = { + init({ id, instructions, thresholds = {} }) { + state.id = id; + state.thresholds = parseThresholds(thresholds); + document.title = `[${state.verdict}] ${id}`; + const root = document.body; + const bar = document.createElement("div"); + bar.style.cssText = + "position:sticky;top:0;background:#111;color:#eee;padding:8px 12px;font:13px monospace;border-bottom:1px solid #333;z-index:9999"; + bar.innerHTML = ` + ${id} + PENDING + ${instructions} +

+      `;
+      root.insertBefore(bar, root.firstChild);
+      window.__challenge = id;
+      window.__verdict = state.verdict;
+      window.__reason = state.reason;
+      window.__events = state.events;
+      render();
+    },
+    pass(...reasons) {
+      if (state.verdict === "FAIL") return; // sticky
+      state.verdict = "PASS";
+      state.reason.push(...reasons.map((r) => "✓ " + r));
+      window.__verdict = state.verdict;
+      document.title = `[PASS] ${state.id}`;
+      persist(); render();
+    },
+    fail(...reasons) {
+      state.verdict = "FAIL";
+      state.reason.push(...reasons.map((r) => "✗ " + r));
+      window.__verdict = state.verdict;
+      document.title = `[FAIL] ${state.id}`;
+      persist(); render();
+    },
+    skip(...reasons) {
+      if (state.verdict === "FAIL" || state.verdict === "PASS") return;
+      state.verdict = "SKIP";
+      state.reason.push(...reasons.map((r) => "↷ " + r));
+      window.__verdict = state.verdict;
+      document.title = `[SKIP] ${state.id}`;
+      persist(); render();
+    },
+    warn(...reasons) {
+      if (state.verdict === "FAIL" || state.verdict === "PASS") return;
+      state.verdict = "WARN";
+      state.reason.push(...reasons.map((r) => "! " + r));
+      window.__verdict = state.verdict;
+      document.title = `[WARN] ${state.id}`;
+      persist(); render();
+    },
+    partial({ name, pass, reason, data }) {
+      state.details.push({ name, pass: !!pass, reason: reason || "", data });
+      log("partial", { name, pass: !!pass, reason, data });
+      persist(); render();
+      return !!pass;
+    },
+    finishPartials() {
+      const failed = state.details.filter(d => !d.pass);
+      if (failed.length) Challenge.fail(...failed.map(d => `${d.name}: ${d.reason || "failed"}`));
+      else Challenge.pass(...state.details.map(d => `${d.name}: ok`));
+    },
+    getThreshold(name, fallback) {
+      return Object.prototype.hasOwnProperty.call(state.thresholds, name) ? state.thresholds[name] : fallback;
+    },
+    log,
+    state,
+  };
+
+  function persist() {
+    try {
+      localStorage.setItem(
+        "pi-chrome-suite:" + state.id,
+        JSON.stringify({ id: state.id, verdict: state.verdict, reason: state.reason, details: state.details, thresholds: state.thresholds, events: state.events.slice(-50), ts: Date.now() })
+      );
+    } catch {}
+  }
+
+  function parseThresholds(defaults) {
+    const out = { ...defaults };
+    try {
+      const qs = new URLSearchParams(location.search);
+      for (const [k, v] of qs) {
+        if (!k.startsWith("threshold.")) continue;
+        const key = k.slice("threshold.".length);
+        const num = Number(v);
+        out[key] = Number.isFinite(num) ? num : v;
+      }
+    } catch {}
+    return out;
+  }
+
+  window.Challenge = Challenge;
+})();
diff --git a/pi-chrome/test-suite/_style.css b/pi-chrome/test-suite/_style.css
new file mode 100644
index 0000000..f8af1b4
--- /dev/null
+++ b/pi-chrome/test-suite/_style.css
@@ -0,0 +1,31 @@
+body { font: 14px system-ui, sans-serif; margin: 0; background: #1a1a1a; color: #eee; }
+main { padding: 24px; max-width: 720px; }
+code { background:#222;padding:1px 4px;border-radius:3px }
+a { color: #6cf; }
+button, input, select { font: 14px system-ui, sans-serif; }
+button { padding: 7px 11px; border-radius: 6px; border: 1px solid #555; background: #262626; color: #eee; cursor:pointer; }
+button:hover { background:#333; }
+input, select { padding: 6px 8px; border-radius: 6px; border: 1px solid #555; background:#111; color:#eee; }
+table { border-collapse: collapse; width: 100%; font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; }
+th, td { border-bottom: 1px solid #333; padding: 8px; vertical-align: top; text-align:left; }
+th { position: sticky; top: 0; background:#151515; z-index:1; }
+tr.ok { background: rgba(31,122,31,.08); }
+tr.mismatch { background: rgba(170,17,17,.12); }
+tr.conditional { background: rgba(118,82,11,.12); }
+.panel { background:#202020; border:1px solid #333; border-radius:10px; padding:12px; margin:16px 0; }
+.controls { display:flex; flex-wrap:wrap; gap:10px; align-items:center; }
+.hint, .notes, .reason { color:#aaa; font-size:12px; margin-top:4px; }
+.summary { display:flex; flex-wrap:wrap; gap:8px; margin: 14px 0; }
+.pill { display:inline-block; border-radius:999px; padding:2px 8px; font-size:12px; font-weight:700; background:#444; color:#fff; }
+.pass { background:#1f7a1f; }
+.fail { background:#a11; }
+.skip, .conditional { background:#76520b; }
+.warn { background:#6b5d00; }
+.pending { background:#444; }
+.expected { background:#2b4b6b; }
+.risk { color:#ffd479; }
+.code, pre { background:#111; color:#eee; padding:12px; border-radius:6px; overflow:auto; }
+.copy-fallback { display:none; width:100%; min-height:180px; margin-top:10px; font:12px ui-monospace, SFMono-Regular, Menlo, monospace; background:#111; color:#eee; border:1px solid #555; border-radius:6px; padding:10px; }
+.copy-fallback[data-copied="true"] { border-color:#1f7a1f; }
+.copy-fallback[data-copied="false"] { border-color:#a11; }
+details summary { cursor:pointer; color:#9cf; }
diff --git a/pi-chrome/test-suite/baseline-dashboard.png b/pi-chrome/test-suite/baseline-dashboard.png
new file mode 100644
index 0000000000000000000000000000000000000000..c3c652413fdef66e7b3e848f560c95dcb2c0f9da
GIT binary patch
literal 439285
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

U`em*sry2f&>*!$FEw??}UlI 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 z zq{t>TcecbxCgdo&x!GP_Au)UJAMmqa>a&TwEj#u0+zmQSKebDJx0;%6U=CE3UD6X4 z<0rN?Kbz#=#@@gaa*&z%B2Z(oJVM_J7g!R$*Ci zT^BCh-QC^YNQ!i)q%_hY-AH$rba!`mhjc3)qS7tw^?uj>uYI&V@<`?Qh;AH(kA{;sj$^=7=$DwzzRDmf-#%{7_QJfWR^4$Jm3bQ(ig+} ziQ*)f&DXFUASfsZ+!iQHdNfexif$gd3~7GPYjCjhWYVxnV5Fw5h!A}PqS`XD4-8ZV zA`e-El{H9vAt;mvkNb1{WU}l+TU&6laeuNO_ z>Ef|Vh=I$mO?My?zPeKtS?HFy<-|4+y11K$lS0WbD<+x!mb_wz}=z~w=Md)6;=&LLmZRoXF{-nx7%%UIaDYMXKnQK)2;@ju* zyqDmw_>4&tCPm@o_rj1igQjy{J^aC^eX*IYC#XPE&H0nQ5 z;;IE){`j-)*+a&u9{s*HX+T_K-f4u0H<6Pd0;#MEzTPg_q};y4FJ9|5>zw?^#fuPI z8HXZpS0!JS3y4A}(ohK9+f5pWe7&6ktmO#k!f&VV7Q3p7%|ndfs|JuM(z~{&BK1+O zsjFTD8;@vm+1;#9@;js|^Z)Mav?mU0RgLrs_XkEl{5}{6`;wxDa5kfVu-rADKtQI# zK=?i7KrOV4;G@aBa>fr!dfGpAIw2iZY!l`OL(jTgR#yjkeZ~%KJdF0^;q3DqmZPKR z_sp7O;_R0{SKl_d%X~$=WfLBXJeOlL^Ll!<5?nlQ|Gv;FAJ|}{k^%aa3%hn*<|zb(C^hPC!xAI2OR})$)Z~orqOx$X~_F z7PTO9^K`$owgy!twiecLUFzz1zKE5W@)0t&a_GB8#9x<3algBh)&9}evDjyfaV3RK z!yc-*=DV6j%fFu*teD3qf;cO2R%ddQjbTu+pt`EOBX4_ zPus1S#?ew%;T4*V|7jh&il`+4<*%Q50?$tvF4MwYr69rN7MQTm3zmNDfhd|LJH7Gc zh>6Xr$>r4)ryrM18OcTlxzIrFBP?vL%Yq$n0q=)3bCgks`qdB~D9QhP?cyUm3@>PK z?pCHragmYWq14pU!doou``!v{3in4FenXjLg}Vv~2}u&LW5IrbpG7J4efSO*UQNTY z5il)?aZnR@)o&C%fgBJ_v!F~yTyT7Hl8O;%Ir0!RXvoOW1O3a(%hS^+vmD?oeo#|& z%diULHZkO*r#}YXFGNH{r%b2E>XNFj?d<{rh-gC=QOjvXcS~3snAx;bQ>OL0DFy-^R8T2OI-Q z<{u@q@im(m-j$Y?s%93@r8DdN2Py=C9Z6c)7dI0Vij`XlJ>UC3q)SMi?iAMvc_5)2 zsLj;LWH|Z+ZSmesPZp(x2*w2bm~i%Ojb?T|v-7&*76Gq!PZdL4JO> zVzJ8^4iDH)jtM9mB!p$%px7Y8i%IAdGMAJDuoUw92b6}{;&vtL>6C`;PLXpc9R&hm zKZYh&NltD;P`z%$x8NEJ^cJYbUQXHq(Y1@QLj@Mr^B2( z#g=GegJnp%DI4daUulQdbl7oLEHc4=wkQ|Wxq->XV$)s=98jlaUv7drav@+kX3N-j z^WZ`hUN%<+ZEW)APBXHC3K#8$(uS1&X$?p;cf-<0HZE=1vK4#c zy7j;zPYV>0?|7+9Qr;Y`gCQMl+7?GPUElhmtakb*3Q}dGBRJe$xLllq`6jJSE>awg zGnv0LqmoFv>3QC$55CLp^a=b#3iQw*zoJr#;@x^0$58_7(wH1Wip@jYCz*HNftpH( zpMn`JJdS6jnW*-5NXu5bC2V>+qVq2robeonA0c~E3_5V1&q|bf<^=2xeg+tFno>rmW5p==~!5gY;gjA`9wylp<%#E zDb^hu9!dktES!HuC^GHQnO`+CH7$#TI-xAf;o`=d9PajX_ZcX~I*3qoczNy3HC)CJW)5bcA?J|+T9oDV6?h^=-5mHU(d zN&h4Bn;_D!Mkm2McG<1%{i#$1@CK{?$TGto;q@>LWUN9qaU5!TUR-Hj0&0l>n4ZWF z@$YogLnnP36@-g1F02oVOZy{W!@8ahYxLr*WV>c=I11=|6NA!Imj3=w@AF$+S`7K` z-HKuyRA0Gaqq~sUCON4vglk*RhCFAuRh7*LCslmlPFN|S`HVoubv1f5(-mI9q#p%8gM{ux3+%Bj2o!}H`Asvp z{*$c14yd46q3P}U$6I}-FxUfw*!?guy>%>@B-@&sX&47gmEGci8iUzT4;(iCImBTA zVi1Z7!4IJ{=Px^S|2z_DFKnaZ5wGFzVkPeFc=Bou_6@b+n zWeGi839gjA3*Z|j=CKZ>5%0E;L^N4G1I|v3Qk+i?(0WAhLLPs*O$MMC86zu!m9h)~ zs7+GG7Z3>@HU+YsmX#2gHQMP=FH4*V7O3)QhiV))!CUA zrw;oC$52}B-U9SE>0?WmV01LIHzZjSiXAifL5L;stw47D)|d`9103*97!i_)Vfihi zS9U6N*>t3L?GALD5z7IBUigg&c#qv4O(m(JrA6%G{5{<{&9L#gSvY-;)~?;mR|tdz zE804^Wc+$btjqk}sJ9OHgVVhhIu$GVCZ*RlOk!j4DB}o^wDcRW)b_^i$Ap^)mx=AP$lza~{!a^7 z`%rExp5EnZiNkO(ez4LUNyCy6NRH3El2yd<#d2n-lSOkXIsl~~Kl+mJs%&naymsN| z1Z)s__>c1)iMv?1h84$mj$41`t7+$Ai_J$2Wy~4fJLE?<+TtReLd;JI*49#sd8$~? zkFYGbA*a;2oU;25!iUJ?N$EO~Mf`H+3?ZMZTN@C0VB>YzhAwxY;mkBhtu?`yMdcl- z@Ej;p<+?2=ooRv`X-WJR(iN}y0jw+w*a+7)f>C7fy6C$D z?%n^gF@6ZL8%?$>h;(w(?%?^g(n0L_>%*&A?VvnXvpsT}CDj@+0d_aDIpM!geFwjtPB=`NP3_W>~(dKihDVoFzAWNNM%oueGFm6v-UH%5uy{ zO`RZgvT#e*Ec*kmkr(&(Ka8TbfJ8q<0~I_%h8xxClPOtN$JT-QM>@hBTTnV3PlYf1eNW1x1AVEKU5A8!O8T z{y)^eN~L7E6pt8s|88sTJs)V&idCQ_-s^wU5jouTIy^i+084r%lD2?er`fJIieVpL zm?DIe4dr_TRm-mA`sm|JZA`7Oc+(kYx6iDe&py4o0W@6Pe0;mxoAgyW^)C>KBhji> znl1X$iQ`(7xc$AKuu0mVSEtd#?~^?fENx@*s?9B#&@p4MjP2JW#&BOMGOb30OcU`{ z$A%n#c(%JyYS7U$*8q*z}1v;)rzg@b>@7D>^d2>OPM`^cZ!#Gx?IF@EIsrZl)RF)051aa~F} zP>y*3v!Y^>EudHa^`Ck0L164^wN^(47{pBt@y2Io;ojqgOFU58L|k}*?W(q`0d zd^!Y^{Ej3~;BzF2h>kWpYwQOmE%74B5?lr2HGA&r7ip5fF*-jIBW!u&MUIF;Mv7{rmTvIg?mQ+q1Rv=Y#(#N96Cm zwR|LE;>IKel;qlfwYmV{^z?L2@1{4r`W9h6g%#KLqVHPE=9 zuoxT;Y}+Ap@JvuozrAjaDdB(SZ|0nDDQgjT2SFxwl>a(PafW^2{-u`+Ujz}yIw&9b2|`T0S{xy)IsK{FjMsfGw2 z4>ojs*(M4;GuKb^ac>#fc@m`58?LtvOd05y-cK}%Oa&1UQ%c?9+9}8(PgvK7;neWS zlEIiiuzqt(!`jM4L)Yhf-Qu>3^axoBXr#aQW$$|a^{N}aztcjWwmC2@4L;I5cnCV> z*HZyrdgb@>)Q%1Kh)ez5)rxxW1Q{pFVJJHzVt!FuFlI6Z#8Dwpxr&mA_<2|&<1ohm z5o@)iZ#y6S=|BC&Zr*J_m2)1U-Tgd<8hl4Db8#KLc_V$d2&yW2)3mz^qS=D#)C4>J z8}z~{IQwniZYj(&N9|yfkAXp08YeVr3gyS!#ysd1ao}a5+e9cim+Rf+Bm6`dDDL8H z9k37~s{f4MTeBgv7fN0ZHyj-E5IyVcjWIq_JH~{%lp{*c{MiQcM}f+xkE4r4RM49( zo}HGokrkp+7kJqI$8?La^yAI#Io`zV3|>s3YkI7B=? zaeG-r#+Jxeljpf;@%uF&OZ-oZ?GnKye7VZoBXI>1teIzBNh_9SpW zdyjB9e;I00=Q19<=Nk$F zV61vPv!EJs$FFiLZ=C;ab${hb?gD%+_x-~;AWfa5^_xN-#?acNfARH&u{^y_(cgqoU0vS>4gC&-MMG#pkFiOSnorG$3@BgKW_OCDT8K6-27_4OG}OMH}j)M~#(tp|Zo-oO^S~ zN5JTWWy5!V1-qjRhHUXpjmbQy?j7JvW%YS-4gW~Ry`(}ljn*-=o+ z(aax7YOO|;)%jT?9?q?Sm^Df2^7s#Erfr#csq&{}%Bu2fGmKl8RQCyN$J7_W%ug@D zGCLQJx%fyL_epH-dJtU%zN)9tT^Y&S!HfzMYfHG@Hzi@=O}S_U&?bLj&Ec(;prP#+ z!#^w!kIdlIHDk9zPvl8mWDaOQ?;<#U6Ys`r%7Wz>krDNm_5bK~<=W_V4ME_q$An6l zMuIo`Sw;7t6!CogW2KQ494X8lGlDmnEnLWzuZT-T^@s33xAe6Za%iX)tOirc$_u=0 z3C%(mQ-_k-Xp+1UUU$VJ#+A+0>Pn$GSxAgJ@9UHAhf0v6740+!FyV9S-pGGd0b>d? zkA2(&dT5eD^KON?sOrS00?+lTh9+BeYlNia?rFspvfUDx^G>|Ww(Aty9u`_^M`}8t z82)XaIK=DT*0G*=y+(bzCgUKeKrjmW87IDWa%qsu_A1OsquY6GHBR8RXX>G(owUJy zytgZHvaV5?B@hvmr1Ee<)`v;)DjV6&$be#fv8KX>!Y*N~_(~GBAK5s2LpTI8Nw|>R z;gd8O8LcU$f}e^oB1-f@(J^Ru-SyqzIE2w}#P4O9n5?w6ZeYY>*~Y z!~lm;C}wFIq~l#*uTvirVUNU%^nv}(lxdwj@cEX6tFz8@c6IGTEC#b9cJov2)vcQQ zg{}@#CB8Qs=C!DF`m7b^gk|jD!1hwb_$QW)fgu7H)Y+|wh@(UNhT&w{0CAa5v}lNk z1Ezt|^O9q4+b9Wf^+>RAZXChkp`oIH>;UqP6#ojP)i;;t~Fk0Bv7gJs5CGEISR54^&T0R{^NJM9`cIWAu7 z@|oLg7UA5qJx#!|=MAz-r_X)L*>t+Y7jt575lRqIEVa;Cq0JQ?hlz}cNPnRIqcB%8wyDtO*x#yV=9_CqG0!_itk<)ZKp($;U=0*x(^OVGi zI|VemSGUr>=@RnVlu{ZjmHU&~g!8<+mAm)_H)!PVP;Spa6cq@a?_3TL6n?VXGt48J zn%nT>lyjHF;%BemGw!ZE@%N`~-IR~}?z#oVhoftf59yCv>-E4f`31(dNG{?vmaX#f z0*523PDdq@;cf$lp)UlShn>^F&bve>a=Kw-`Cn~LnZ+8ZRq9HRW*8ar4e#0MdEeh` z0&4-OzmMsz;UlpBn9gB1zp*8u@Xb*4avRCn)C+3K>C&kcc7HxQji$qE$#_}IX=`T7 zp+_%b@u?&f6xtQ<9@oqkD3FQZ=hMdOi0V<)NXVYDg%=09{VFA1Q#w^gqF2S`2|s>^tPb|B*o@oUgMw*NP>GbStLJKDtWo{i!H+@ z&{rb2$?^Ap|Bk&*OG%;W>`zR!5}>>z^f!6)XF*JEWlP=~kxv7$tUhvjYNb8f6V2((6!mVK6js<)M=lG?T7X80M{XFi^tg0m)#!K%h^@2V#EFr{(Vj*wg zwByO;i6Cw^D1=w*g3GkCk&AldGxpnEOL3hi8D{8vdfH>b(L`qnj8p4~_szkdnT(|8{zu@GdcTETO^L@~9d1kFW)w78j>ie;# zq_JiWa~Z=L>ado0yi&qG$E~D9b?Y1#U+O>H?@XG@7z-@YhBO%GQqa*6F~i- zB|^+RfsiIr-~2bef)2!TF-TwK4*-J2%Hrapv_KFM6iCI@=+xyJU>QbAu<5xe5t|~= zyniafQ~0~%B4G0}lAs=%0s}KH5d!0L1VGF-BT=~0`N2^-gZegD&dwnA1im%JnL=5x zaB~PLTIE26?5M+6kmh^-_v}Ffov#y1CfW&z8{qG)Q7=Ja@O_Atm69?8IJAO-g8c7M zWVh)~)SCcorn8W_|CO2k!l*kRhYhonj?TX<(l;XLmpu?80X(3fHY&L$Rr3x=2K3E8iPZ}Wv|6Ly2^1rZ z7pg;jI5}t2U)wq?MpV3D=d1Q>y(2GHkFYCZ+xrZaXjn(VopgVjvNt0jtmlMi@^|fY zKTICAFuNzQCFW7ElK*EVj48uv+5n_oZha_z&S^Y9|9dWD#g&xeTqL%#Key|xXg5SFTTfPVS*FEZ)YO2|BgX?Un0(D73=fw%slvJNRS-S8PBTW(-AP+Tnt!tulV^ccnGHczK5^omO| z@Y&@(Il>HK!J#sF!jvz2;5d9h@RGo$ZfD54vgsW#@C{GjlkB{P#wUxi(M>x^(RI?| zYa3#ZQ`q5lM^l1)g@{|bMT?{kjA_A-;F3vU7kETM8eKDbD}n@F&8tWsQca8Y{_M-} zkZ0XU;yR=FG9KDcm>-#2{HJ6O(NcZTMTkhse zsx zPsDPX0E=!mZG_JnK+FLiopQ_K;mbV!?swbye8|@*ks2fl@YB)`nS#u?D3PIFHJB*~ zb`3z`kwYWqi+N~(FVQ|fS}zI0`UMl9rTEQ|C;B6<{c1ktli?-Uy;YX7qy6}2E96YU z=*J8C0ulU5ULr7=@J!kUgT8t|9h*JL*F9j;9x4!^VT{B@#?8MkPAO?nhfu)SDOFag zYPYkBqU^j#oOV*NvV&KpxUtWlCEvglZy3!*R7fJvPMzc+%scUwp^A(wI-hPQXQsaj z)zDp%#_|50bi074@+~nWnI&(}lcX5q@!}m?xl@68r;~s@iK+tSQ)WL!73*f0z4<_M z(0Fej9N)+@sVPI0vA%uYIkUpS%{r`|8~}1_Di7uf1VLLDtOqF z(c%UHqq)j%*X%;v(uKg2yE$`28y9Pi$OEd^&#aj61#q;_?Vn(-{RK!EaU&@%Cgye{ z{#eVRsh}a#CW@`N;A&sRSa=d>A@FouL^n2juJpF3O;8b#ISFVl+fr#!fEU^7PkJ$7 z;%Cm4CEV*;OZ$?u~1A#8imO72=XDhcOnDjLFTj?6NH}o!>iYmSjdPrPKYp z;02CJ52I|qVUwLfRpJ#OZh}$H6+=I{@pQej<@F5ncY8t+?uymD0Rq+0WYbd75HHJO z-LZ=1R;_kGr6F!K(|mX|@YSzeZM&y8HKf&n?pZWhxKP|u_7>we@=fZ_&R|!c38M5v z%`crXH2O}Ky^h8Ui9QCdyULQ{|N9Y1vy8L@wDRw+qkX=Z-Cy`^7QWtU`Mg>tMC|se z6Y6F{-hLkpwyJ?9Osz(xow^0Pw`rkncl&`ZX(uO+BotX#pzP9Z3MCnGrv?ca@o9$M zXm_Z#UvK&nJlM^J^-0l;5ijejYtiI|2+VEqD0 z3+N9uK^qMXaOWQa#AveE?!;gaxQ_}71BX2$oz&H_GS3SkfD!zVSX^&@VZqd1SAi(t z$lLUg+Z8xtz|$tqg>qRZ)y@a5lAy@U0hx6I5)_O9~y@(%jj>^f*=>mO3y~; zWW$nzuI8r36AqMp7De-a`2r?XuR~Ik^Z?t-DTQM;xF9^iM#L=lP zpd>u#7rMwkAj@DWL41fjQ{+lYP7WE7^^}aHhs0MtY4C*WV&7YDc1NNtRpo7?ba zFP5|w4Mi9Y%b5tCn}EUi#WD~s63u_E(^q*e5+)Ppr-0DP<#GcN&-ZQJUHQoD6Z_4S zkKC016EQRi7RF8Tc}IglaG2>EAp=jtm(=sus%~b>z;h%6{V&N_^3~WcT9i|L@8HNL zB4R)JB}u}}eHrl&nRkTyVA!+$TI6YA?(cp#A_hm1yM!5>V{SP*R`IzcJ+q7c+HiY# z9hO)?9RJOIpDFIe`?re`o})Aj<4EoN0vC^)>YxkJn-Rxt?`*Z#^g|`}m}!86Qx9yT zeGd!Lx@Y5S60Yes%Eq!|x$UPQdAxc^FOKPZlMq+y9jtJWTzs6N$+KUDZuXg6v>P2dP80m$s4sHFLZ~wOE#gzU^l9E0_ zOw_Z{h?xqTPsCxRm+9lR-=aO=%AJ1siJA)#`&0c~rb#Fe9V)@ehSewM@ z6hmC`4Q8CumG?NkD%~l6A-`Z+W_oFRXgsKn*|lT(DGif0qixJiP$JRAVH>R?j!FUU zW9s&Dx4F&b?Cq~0`LuiZ`#o`xGDwU{sKQM4xRCNCeo$sC)>!!_fXj$#U3C|m6*W5a zQ1Hh%7;!R5Cd|o@&Ot;0r&(D|g^Ut+7pyW=2<4CvM`5d&KH9CzwLgz};iPkbV?KliP7tQ9ADR%Z zdXD1QdVr(Eat4&){mg-qPzQu49(AIGuRza3OG3%``(J^Xjmgr&z& z1Ki5P{e9?BXH_v;oNG&3=ufQPB>sb3U zd$-3Yn9vsxHo({A=HkM|#T6WdPe8yBoo!0>CjRg1$0cxpf%}=)mhO8I*yTWR-vfCF zkZG2p(J=)IBqdu0c2j*pQ8>c#=Hw(&%06JAZ}kO``y((S8Bn10FSY@;GvG-94(TfZ z6qxdphgR=&enGyRdI6l8_GwJxp%Ca@L{SiY18J~~a8Pv$<01h3O`fBan+g5^Vt)`x zQMH2zj7fu`C}oq(lkF;5(Sgc205pr}fs|W!I0egYR|qVH_?pDl#KgRxVUYfSjnJ;n z&0}_XP#ki4e2V!0*23i+AXpb3hEK#ZJ`HS54IV*&uK+r$KkQD6qXzkO2E4uU*>Chy zI_U$TOD8}QcT3_czy6e<{Hs5|$Ek02u9mqh9nBUC>i!VX`g1-uJ5j#qr^f@+E_bGZ zl3(PP;655)Wb}SWO`5_Lj()p-KX4?jToa)9x#D>>nC@-!tC%_RP^m5deufP58~y3= zxLP5(-rX$Ar^;BO4~n?J>Nkx&i|g~$r}g*lZ!fnkla7`To|Ay#tRZCVw=y(7TSBKR zs5mrH45auysECLn4+kb`WNiR1#iBdW}_J3REWsFzY&G$TaBCjr$08&?5A zT`zHu3SKW6knVAQDLRZ!EDV$h#-BaY`*`d1FxX;d#h}}+3mneAWwYnc4_Z_9K6Ym~ zS%NSwI0OP0;Tik4EUa?Hnc+y?m4S@)3;mQ?-cKfff4M*688svly~duhvA^6Yu{dOQ z`aH3}65p_5KyQ!=Sbj_?fMJ(~?J`=uUhRfQX|?Uw$1$zB;fe+;<6+2W86}~-4wy>5 z6dFxvaP>cncp6#V`6gj46}3~mMj@e zvAA?j;E7jlql)^UJD%H8x+o{<4FZFw<)8 zjo!Ai2+n-n{>NRmo8J2r77AoRSM`P6S^LMGjP@-66QICV^Y|m#J&MfKI;bHxyeF(w z-y$f&8~=)j^0UD2-D({@L09HnqG_OL7LUV)+Ug!Vnfg8lb%BzMJk;{fJyg%Vd`2g` z_0l2Cfo;Th&M|rhmyR&mdd)j61-2+)DJux?~QWit>K@#slkYlUsuKv~$B0M^k z(EJ#9D4}T4KgKBVUqD06aq(EF?h3=@2Ehl>_&4kZ=$6m(ww<`V67igOXEHze|*u!25K}`@$}tq z0$$OCaIEtRHeKw#6LoI;tNcj#M`84@P41;3r)&4!N!1x$5kw0eV)F!i^fBe~r@eDY zU)Fvztbd9ZPk7H)kK2OWxpn$QOmh=;9tcN9KjYBdY`>>tmVfnhW);b>zb6Zek&u&& zhxUtQk>7cS4nBnixqA05U+gfRi+ZVS8BN^|I5DtfgNIW7_UrzJH^wL*He>+Ky!2@; z(*6!%=f{wvp^gCed|L3=_y^SY_sng(@|2w29^&+IDReKhtRx)QH_7RQT2SWAzwpvFkxe_(Iq^9$1WLak{U0i4{6p)pd-$y&?ZC@)1k?hJFKTV6bn* z`(AC|Vj8&heZYY+A%y4VAsK%`f*AzqW=buHGN>kj7Yyzzzjqg z$G?H~wh7(r^fXQtUr?wY6S=m?w{NGjIG3yC4YQG-jLbfNW+HgfLj-+N!oZi0@NhMd zND=h>jGs011w^+|F^80}-yNvbKbQ?bgLBTXGf)T$UpNx|nn6`SvvA-g3`m-Im6;6< zoH*y8ln)p5=AHl|TK?3ajqoLHAhhet4>7z>t$2~Yl*lA8<4C2<>0t)?>| zKV#5@5#jd3QvnjGkV!wpHwcjAPw`#&&Tz*ky9{;2SauUzERF9ozu@mZZ2-+G{Q}V2o9_ z^Us+=lqsn^Q1KnfR(Ma^z%S;tStlZ}V1I8+mwEJ^IC(7cpI~Y4)ZWNw%lgDbInO7s zafb%s(f}lp!I!h0?seHvrw)o#-XGmLgz4G2{sEr~5RJ&=NZhP0ng=8E^O$=z>=^(F6Xl;-i!hO%onKNgEiM!D6f6%XcQ(qPJAvdX4 zOW+$=I1h$P2R_D^FA>!5e-I0Z!ngEt`z43Baa39Y_F4)J{M)&_E&nKYpkO&FI990~ zUD>_Iq#1{M{BOY=$?=Dx_v4m}enHQsp34$&Z3^dfL#)V@o6PVzSop*0cptOhxg_i8 zzt!+&wh^s$7kZ?->c~yLRI{Ku>)$iVaHgVUT)WMz+t%6tJ4?R!7wVegXr5bb5VcAY z;v7rhn|kmx{M{t(>q4LqI-c%noc?_mH#6 zN>(PwE0k7AweX3Ew(lPi2rfs+ERJ?VEph><#6_$5Ejp~WRztxE8IJdEcL7$m1mi=r zjtoB6%2=;AWkp|dbQX50_Bnq7E~5KcVDj-qULyLRoL@h%8Nqp7;<#Cza|_cj@4hSMJ6Xf%o;N?E^!}j=W=04fxaOX z;jdID$4Jm6V>3MAcjQdTX-IYrFA6QCIIytn-|vqJGQ!QGV8EUiJDb%*ASVIPAo`6A z1}X3fmHVADpnc#t4HAp;?DU3J&&s#gKby{l4`QO05i=+ZrdQaJQLG(6kFgXZ6GkC| z`?}rb7LKmYJD=9+c5<*FF-<1{#s###FH+}fJOAMK`-^ZD5~Av>eqgB7O-IB%vEhtr z24|LuPlSDXyG0D$@%=%*0j#Dc9g7@vG|&H;E*SXq<^$fek5tr{{_2;d{zZ4Rx=^jM z`7D>`k$SO9I-co{>S><^w86hO>w|gkq`nD_^FA=Hd>4S*Fd|s!564AEzfIPfN}ZU_ z(bL!@`zZ*sz-lE|t4S-4o_g_0I>*Ts4vkLX565{iuHezBN-`uFEuX4q2r(M)oeOJL zPHA1E2umGJ&s!_>Im`4+7+^t4QdBE-W=y^B&O5+V`)$yNm7+HvL6VxAj)n%NS*q~| z86|K6!W4kfGLhiiJXLqC<3%oXnt+7j5+)*(%L3`$Hz%MNmwpZgBRNKdin))@AXpKC zHOja;^U&a6Z@?Sdlk0^PaD?M)`CXZ3K(c!C4I5_&073!-Rl|=-*rs{$k6qrWqNNiv zGqAT29T^0XDfMgE>KuuQiC~Nrb3KZ&p6DkP33v@p45q=TA!z@mS0ZXXPmz{xBLHpv z9(2u^=BTm&|9KA>pG)n+Ov*PwhZHI*s)hHxg{TB5i1_-M(OYsH>>m_Wx{;Rh08~C0 z4QU2e8G`k{n&NO&Y~2)HV#=Ys`LOeimgH2aZ^r;O0h$XE2oXc+&RPgATaM94h3b{x zIqyk#f4m1HEtf+-2(Fq~w`Zio?E>~gtL>_Cv)ortZOZ;U&>(!_iA6p&N#PSJeZ>>_ zV0ZAX_&^RKfY$qicplt#j+Ov;0pQ%?ul ze*#-NX+fqLH_gE=5br6X;J;s}H+4Rm#$D;x=ca3FomPCra<31nE2o}U7=kk*7enfrXYplo z`24^xr_3w2?o#qj4~VAlq9Ie>%^e2wc~F~?91diQj%{X-3Me%QU8&7Ft`R zT}fiIEdmc=c(~8&pSv+-)sL#0?D!V@(JlH6h?d~fIO0n`6f%dsbWDvCG!WawxK!V5 zc%45>)hD*PJHnN(vaP2Ep}K%bo-}%zX<^uRv7Mhai<$qaBFQS%^CL zFoiGWI>1~tXr@_AVzTl>r|jd!#D3u%&WrX*rsSx{_;Kcrq3ipwUS|pR`_-BO1FmSn z#VTZGXOM(n9g7|Qv_c)pFOBmhL*i)#i!GB%_4^~p&qjVA%@y^I-cEv;Fy?aMHIBvkiG{Ac)>G@d zM&98bfW&pT=1lbnTgl`y-Qg!gxz69ZT*EckkkuJHIlPf+PYlT(>HaG9oGrg8JKdXy zl+fqDHnPe=O9ytp9KP>=Tn-NYHmhmZ8ABCc~YQS%QES;{8w=cfHEf#zP`Rc z=K!4hSy%=sp}m=GnuL0KUuC#S4UzVZqvgacKb%KwEC z7bu4Msp0gluCAiu%aQ(Dsn`LY%JAS88xVD`x=;Dlx0|{xNb493aT-TxAnjbhbH=6;Xs-wALQO&T1&omIpCXjaS8qMH3c4nOdUMFe?nUt zv}+^?P;2kiOV@oXF`MEe5)z`cQZ7Yq6V*VZPyUbVkQZTWSNRxvujV~zjFKEbOupblh% z3fu+TY`_rmcG%e53zvltI3-6rsZ@V|gcPZsLNH}3<#=}mu@5~;YLLSCnlI26;dTO*Va1Y$YUrADuI9My%rfh0uqXUlq1jV zX2(!fp&Bf_bt2Zg?vT&b8#>Tn%plEbgL!|kcv-};bCDyoXvHuWfNLEjvHAL59>R=< zV3c4(Cy-8P@6|9>MTG^GVWn!JH*DgYoBN2W#%lSBeRGS4eG}3j65iY5zm%NhxniQt zbo3$rigkW*ElhuBX6C)zS$Wjzig7Amo?hBv6=kEk+Dl)Z1wa zi_Br>*^e|9^Y`(;er$-X79?-gdJ)aO3@eSlV_99|z4-ins`sOm;i3y;;wSQ2GA}=O zdH{>8EMk#s62nge$W+xyHc)~G<{eTPP+C4?kgD(d@_3wQHX0Mu58i)I_kZIVSkl|4fULId`&+t0LIf!hkZzDp=~TK)T3V1sKuM*$I|Zau zLb?PbM7mQXRZzfpa?U;Xp6~DVpA!Pl!+!Q&YpyxR_>6cCgLQehUG>9N7Kwb(qb zO|7?2no@SGwtj@2p9PYB@g6cnQq)(w%v%y;J=h<*U}6_KnSAZwoKva0RaG{d-SgU? zx7JPfsN|8c>d0;;->^STPD*;dJ$6@v=0)V~+qcmJU|@0&zWQ2}!1WO} z#zQ~vlBKSj(kkFG&#`;N#?7Xs*~}9C3kWy&TFW04Ka|KJLc1^2!0qnL%5d{OP5W)q z@kbs>+sRw1F}r8(dortic;`(`u(MOck7aCuUfq>IQGp|R=SBM0{)VVySJ$`9za(E5 zR`~T5^tpd8OO5Mv-x$$V>b5?16$4W$dT=aQ`&V zzzhj4g4}ycqFSHFUdr(|nVw^2T!}V4m$M&NnxgD*kvZe3KYb%P(XfeE6*>xy;P-e}65IqY=Ab#Q!T|YftO?A~OEg zc=`oxulh@gyliiM2W1#li zCA`ozFmC_&dMGGChFdh?Az^pEI#Vd?jV$Eab&jN!&f|JG#@%qd!$G?pe>)_w;lW3r z=Qd)v4Vg7VZC+C78s9ihKepEZNAMAfZ76(@eul^Acf6hW$<>|!3H2S@(1r^@+?pXO z8IZ*P#TozonbWD%RpdD>PxtOTDVk|be~YDf)nVFtUdC|u`KXEEEr$M~_pOpnlxnK- zWR;umpD7PXQ16c1%N<%hS(@cg<`KEOGj~6C2=QkFdk2E~9a(q*4P--JW@hSjAXOI> z7Sezg)7^Tw2z*T-BYeQY5&HVI4Ui#> z@OkDDL;O;}&yXJ$-93`joM%YaoLI~P4xm7CzN-rnUzggwnQslxmfoon>rG?*OP@c8 z+@f?`EUfhN9iOfN$o3|&8HwG39NS7c6cYsPe!~hXW1M^wTpaW?Aj#~H4@9Q^x@TK|SJxcP_x=DDx61RMu zzTGeoV@f?D*)h4uySqR*-H_c=ZT4yHoXC4trw4R54` zKw9k#x}|}_I*AsOgP-UVO)16eJCs?;;}QitFxY(6*sbRi#laMMNtMWY;w*gmVbU24Jw5?_fNKkyN8^I za$1tYz&a#nm695z^rZ%_!uW6_|Nm|zKa5bsQZ@(P(!xke-1SgG1p(V#P=8v!PE8fW zUAHX6K-49jX+H`O+lzbilo4Fs0SYznaxA6}4!C~+X|cJ!o}Kk3At9llfQ}oL>(;GX z!PmDa&7EC6Jv|v2e1?ov_5T8_zbwQhK=o(ddt#fA5w8)Q6rw=h*t6lgCr=NpXWX@Q3c4wh2c zpmqW>NT}qweWT|Iy<9AW#X%mGbUP=oO6IC9u#f>j2mp85)0+Q>YkmEcGE|S7oQr+5 z7ZP9oM1|D8&>E~j2<(KDXSy%j78(;NDP9fIQ!aS@Vl_Yw0D7LV4i8BJzZS2rzWowb zfR)`J&o)1t|NOcW-!FJY%8scQ9Z#IZd!)1^FOrx^V3V&dHJYf_djl);zklccp~11S z@d-X2gp!1k%+eCOWFgJ&mC0?#Tr|NcoAX~^KT_@R@>F?0aq#nVNbLCR{NZyHJTOOu z4dLf!i11i`dP&A4R9uFowP9x+Pbv)^JR9~bP&?9KN3C_~Ul;BAiOacQdf4Xq5{cJa^+~UH-B%o2X5`0tZMW7V zj{G#5)~JR_Xbz??1$_3LpB^d_od5Vh8LQJbrg2IOAEFX-BP`U%+zg7Jp6Vj!e<}63 zxV*gi>R9Lzuo4ZkC&*)_1F%rEKb5$zBKpT|`MsNK6nR zvYC-^QOodSKeoTH-nqv<1G0GCe$Bo_kCyMs#bMDS zPqc>M4A@Q>@t$_vGFjx70d(y*&0Og35X*5i60lNN{a=z3-evf3|X8B-AxaOUm%{ zcs9Y55&U_VyJYNqh2+@fp0U`4)5ciT1m**WwOj8D8C;uC$n$akz=xhoqtL=*XKO16 zaSB@Nfhbt3Yds!`z?--#s;tC2-WU6G!3{a8KzacD_{RD=#FBdgdU|DLWw;xD9S6{P zAz%I4tAs}%3aHMW9y%eR-{4&VD<6sGhvk=|qN33z5Gpq8!OUFNaQOY&a=Fy9hR8g-A>s4BhHyjJayG>M+5k6!GFPkByCWvM?X7witp1M#AK8 z5+_J(t|#18RnAIEfc>$Wt~4H1z>5khE3;Ej7=+<*k!qUO=;&x~Fa6!SD^Scqn+({g zLflp5<@=DMM(qShRM@LH+R6By>VTpjcny88NrTMi7ZxDWBD@PSQTS&!iQfIw3*Z1) zhS7UJmV*~@KI9d3kv~o~{@(RKaLO z+>3uJ00|8c8TF7((nxan5R$hrX9HSWg|D*^i2jCfzAR}e%KSeR&Y2;l30MjNMsmW% z4M=VlFH|`Cl}2ss|0eL-TnBe=iZPIQ{)er^4T&SW;$7{$3{l*4#X67%lH)=9nIpOn zrZB-o^b=Pk#Svp`^!W$DI=QKkKMh+rnHhYSw9Dr)sUaS`$uniX0%#aT%t+! zV4&IEFb@-VR;CxE{yQ--mCx^uH`wSvhhv7Bgy6@(dZ(3l7c{`Nd!7Q08b4 zyXFncQ1DqH7bCUQON)8aY=}d0eU&9D)02Ht*KD6<(@_ujaoC2GIrdmr;*ALyn2>qo! zPuQRH7#G9e+0IV;fooCt^kN<1hehf4-Zbutdc#9&X)AeFwl+IPQ)7dotb8;AtF9kD z^H3w+zu1mpk7Os3T%M@EMQ%s^(i; zy9G6>o8IvTZiS896P$-E@u;|Kb5A~Jk8j+R>ai_0djDHOS#9F% zh^Da25I1ugR4Eb}zEw6EGg+ZNZs(Z#W({#Vm)kldr&#qwDfq`f-PKkHBCzsE`c zb%hR|Bn7kr!_+qs5x{dUWy1=ylZRU+2eKTUK4a)LTa)jO%d~o&rEu!07hwE%6SkLn;=V#Job(U$<6;4g;RhE|$2>_1f_*JEuHp}ttcKOZp7l0bRzu8!B;>G8xTn(78F9n7gQ$Ili79UjdrNof~!EJBO}^JO$dq7SEU zi=BT5KE_^=MP+m{+g9bS{>c`KOOqO|&CQ#=Dk`mlT`OnU6q(2S%&z731p!i_Y*_bI zF@8VY{nq(;v+Q2MBf`jz6M*Nn`zykfNL>{#UXJyi`B~{ky-43IPeQe%MljX#g7Vnc ziMEiUVE?tvEmENtve_?P30ikVM8)oJZESweMH?`(3K!uf$5l<*aEX(Y;#xBsI@bVY zdC>iap7yz)QzeNB$sJ02+#bm=D&x-j}za!HPflXtv-BfbSdZ z=Wc~OfLw&>A_cVg1UQPUCe#7*UDzl2!3e|vDJmK;N_d-OG4Gj-mFGZl^P{f@d6V|k40U@6=yrJ zXS`9HeZWOkZmr!~0M}pG{p`o<0sgloMN8-N#1MaBZyu)?S9Dzo@R$Q}JAHqF_|L=d zRfG~>1765BWIjywt+5mGacbNeK@)b491^)XrgvPI8}5s?EPl3sha}|m{t;Mwr}`ju z=8_^iJ!=DwgtP<>vY!eou!kdPGE1EmBx@Zk6wKVRKI&vhlRhbNy&b+ zewR}>rb5;$(z2Ht#V{xZG7$m*;H%iMb{BTOWrW&U;6I1R8#SxVdDT8#LhY$W8U*M> z3hULQhosqGjOOSn=JSAB2g(ys-S>fYIyJzR{d(s?Pm1Zzpi9izmB{?YXQ2izDiIHc zcXyn!?sfqoN^Vn7ogih!r81J@ruz{&@>E@Ivq59z)I6*?23Z`b`GK<4KxpN^_&%G! zoOeat(z~0~;Lzko61o)2m3S)8F`_azbNjz1t?Qecu_oT#MvK#OG*elavfcMLA1!Tn zPY=pb1|C2HQWE%ic@ws)J0MXtQ^dP4Cue1S{fV93(CqU#NyZV#mEfdshDg5~ql)ip zY6#P=Ac+CMZ`Cz55-|%9XuH~Sprw?`$|UWHsuiO3eLJ78=0;L;*IJXW1-7jRHa8NUz5pJ`6GxSXJ)TR>^~z?Z7X{Y5993m3Oc9xodpl-O5h+i?ht_YH=X@WPF*> zv)?MWINe@U)JR3aIanRLdU)(v|09d3OE##m*gijgI#>H60h%ZUbM1t!ktO}39fj9r z<{=R>g+3Tq?amZK9ss}RRJ@H8PBcFAB2B$lpuwT6umI(8nKWHi<7Wx}#llQ6cM+UG zEt8hbCL5b|{Kl})y>A|tipL%BBSbrsOaYc3?hU{l`U!iXztlue^VFpM^wxlwpjhD1 zJHubEIxmy;7lYG9ADMq3ioO*8^{P4ifmprcq(+4CE3j)MKfl=B7DH>pwG{lQiQV6E z6j}aotuk^Yg9>reXi*NMJCmHPd~D;K^HVy147P={vRvkD7j05IE1D zV~_r6g2C1B6E~~TKDIH?^VGQ4t3-ukKmCj-4|67RfQm`|XUV&6jk&r_-9Dq`cuj@o{jYRwGxt zFZ0aP4hZ>p`U^IDjz2&H*YxsgZfk1;QXtbpM^Jh%M<3F_z<^Uq>T3!r;(K&-C}{5# zt;Swd-B6mRPD@F--g9qptUSLAJvf-Gk#&@Kn@V;OcdWV0wm7*TLYSGoub)~m_xg-k zEzGI7qQC;Ml*kYr71kSkTc{mq@ii(RSDeJ}t(TPdnzd5Uh6Hcka%cm~%CiyH$#}aq z@kvhiO+)rj_nUa%1{fIH#NM&rcvV^^9AEUA5P1&OwCxPhsr_zLX8Hb6YXCNi^b@ejXn41U%|4# zi-Y~{0+k>;EF!I>?2i0v579D!tCZV)?)k)Wq<8UMFURbqC=1k!=s2cK(IhNlt zn{RC(i*>RtJ5BPOYH+)7ysP4M`}wkL?T|fD$!v_5`~FZK2G?{-?10i(J?pR-p_pI4 zio#3T8>xuOBEGq$iDi$8(Ln4Jx{j7qf2=3;R|Dj)$2~$=pmBC)mh=8SVrZ0bePll~ z%euRY#=^;Y6Ac>&M|uJc4Xwtk&F?b2Ko|0s9zp>x8uF^jOk5W^M*I`~h=jz`GCE-v z4^=H8p%y5{H1xv4Ow%4pXrS6?B~o}6#4$bh?g+qCPVJN(ljV^7 zrx$SXMSvW?Dpo9nYmF;6Pp3rOc-=y(X>)IX(U9-^#myxY-sZ+*C%@5illV?{ z_er-4Zpg-h9>K6?`Hk^?@wj_2;E7|t`q3L|oO~6LD6inSy32;nj}guQunarTF=tJh zkyP!|A8GCR2E<3+70gXpRgopS?>!^P^=o@Sc1d^6{j83z`WUZdxf(d*(8ayBDxUi4 zMM{zOY0Z89*kMKVb~lOvJcmdaMg85Ry8evLFVCRm2318nvh8-#Js~0ePujM&W)<4c z3|V4aVAc;)M0aXUZJB<|M~FA67Zw&CG0O04mC|Sz_qd!#6f}OIQ*UZ&;?J(Ai2t4f z4=%PaTDxQw;S)ajtn^dKA-49VwS7*!)etMy&WaS;L$ftOO9hG*L@za`f^=gZbAg@- zAKxaIqw`<9C?D))-G)c@$#^P6Tpj*wIo@IUgOMJbH6cG>}`oL|#6NW@bb? z>mYzC?4%?8R!-pevHB|*XQ#_(s%xIRrp z2pa@76XnJ0PEJl@VzQ&sOpt(aD?&IuJRX+2$=D1QCrRAYFDg_INTa-(Jm34~(T6tZ z)Q`@+7lNl)>CEoHWnv;POVqOP<%F4kjjTS863(As1%%P*s}r-eS+cbvIP=fXjLTEA zIP+hpH&R}h90_aCc>LJ4+w5^gz*(IrV@;!0d>D9~Avy&7Ix_zy>;PQTXQ?leVdZhP`qAv<5;Wz?Mr0x(7)a9(uMb#vQyyh1{p4Z;;@(XC#hGq1p5)cx0ehj4&MNdbLoyyHMis|t}*T(k1PU3<*3Ty6X zI}bgh2BGOrx4yN$OqXw~=MpLdrQY(sqO#$Fg!K_5eGEM^t}rkwP>Oopjc`x)zJ~3M zsjZ{If@`5A(y>VCewWRbVua=QBQ|V=l48h zXrQ$^yJ(q7{$(H5GE`Jl-Z5Mp`dm8lJ8I}v>2E-KOa#?t79_+YSzPa{q%t;GjiIo*OH~NV+#O zGs8isU12~fAKVzCnG-6VQydO*L5@F@)6@0umRTiNU7GSg$f8r+)ziD>vWu=zeyXQz z5RsOenwxugbYwG{4d`LH0_c18ba+qf?(C4>z75m9$Cx)rF^=97&hc8){(fYd#7=!> zx`-6q_?);_n*Bk!1)btA@<1fuyymVVJ9-YaV0*Scb2>R$+Fx?z%Eoc;#N?`(c>>8t zl*cPrSEo&X$V(6nu{zDB-wl!I8tZD6vu%h&rT;ehnAg|&MU%fRNX+<`XG@rw_ySc? zS6^s;CzF$gEe~3OE8bQn_IZp_4Y5Q_x2wY$d-dAn1e6SrQ@vc|IPA6z!BZ0b$>dK* z6DjFiltJjYR?)IxknSlBJn5}7zrUGv*zw_g*Gg?DRa#NL-|uiFu*p7jk*r`_KgIZ8 z2M=!3f?>%5-Sv%)p2>6wUWb-*)Y;GzFf&!LP%|l*Wbb@A>H9>8gYyP(^9lmzeNl*A zYK%)wamU_>2`NpOKHZ@=J;tqcbZ&=1HI|lkhx@3}*}R|<|B}Vv*>zOdX$@n*FfDq9 zDwwv&V8eXe(PtjwoDG&oa_6*>xhY4vXAeI_yg?+cH(NpozCa00IdYcIx%>7BXx!Fb z*cGvvHQeKMcd>LBF5w`R^zCnZq|P`nEZq9AlJg$hE#?-3Zes8FTh@MKu_t0=Ud?jE z_^Pc5Ay)&hD}|rtC&pC8BaO_9;5_wzd%)xD>Ux&?f~non^FF=F2xjB`&_l&&tn}h?}oA>E@ejA>&up2k9} z3D1feIfHf+6E~&CFp$UZ7Ifa3w%y&6;c(Ce8oP3d}h5iDg?n6Fam_6UK0V zxhsssjRZlV3u?N=L@m&SK*mhe8gNaN({Cfe;khFK3~4a99O~~!$8B=oPc2T;rib9v zGurGcho!dnkaZ7#TL#Yfb@E3Hv78(nCR%>VZ4KhngpggV=1~|4p1-2NtUCfO>wu_m zzJQ=#`P9I`K-#Nd`{)oD^mk66;Vpo@7!CV2!{xearf_Ef$lqA?8u72nhx%q`$rAYK zqEkS)v)Yj^;JR#ju+Z{=hiD|Vq?eE$=Ik$u?6g+h!o3o;faL46KSTOV2tV$v?c{NW zU=5^%ur};hm8|s%Vm^x>Zq(^^GGcNM(3{`O29CkqE1G`F2UHq&L@4(f3%^k&pyKYh zpKE=Ncu)w@Y;ZaR13?}M7$HD$J`LyfSqPEws!*cBd zKSOqdu8)@Lw+S7>(}3CO)ZyKM-_H>*5Nd^!SYFEO>2*`q<=UE7GL}y|e-GfnNGe7h z<2hR@TUa7g)Qgn4VeAc0yG{S}_y?Jw+TJSGqRyYXE!On&eG#~{Bukf1Ub%)=zAk-Y z$`IQwX+-)RO-r9bzi#K*E{#tCw5=?xt(V?m81T-2lPU9K*$n2Eoo^q)a^^Q=UqDVE zArNY?Sz765%;%Bs%xW}PG}Gl}EwA^+oBQ=ZZDQ=5<0O%SzB+Lv%_ka^xQ=Wx?`H=% z_hFwSbXsktjX6fEo`Smebz+Qj(#dCP&UkqUCC%YmJp&yqcO29u^3=_)lg&oW0cp5r z#GpTP(!cgc&9VNt0Kwo&!HIFD5y%MsDFLomU@jN_K4#C>_wVt@`KigoVCqJoqO6SK zi^3R^nYjpn^AygVogG*^ta_1AQB|@;DIS-Wm5r_jt-+s>k&%LQA86Afq2a!K!4BR6 zJrahNP|28%8BTMrpF3UQ>rVWz7}NQSg88IB`mP*CqXC;-`LHYE^| zmBcUTHi0=#+h=KEv9qgSU_neo1n}|uAW7|ahw$mc&LaTmk&628GwCW&bKiO3fGlup z(%}r__??uZ3pjOP;qxQVmbn`D+VUGKu6?6SR-tom?NR4_B;Vi*oul(>(d|HmD*3|S zQ~2y84Jx5W2tV_y4?TJaDvf+42!hIr1#QX0+PS@dg>?N+VU&2~21XXquj)=s*TNCH z9-uE83c7z98}A{t+!VMch#9W98e>oqTw!!FGMD1qw6pV*L6K z*`goNh>=va*m~G(nSW3mUUeAdV*+R$h_F(ed3fF_(7;ECa+K_tkq^b+8u}?HV`Ah8WBwM=SNTZ*==$M@lBG6Fy6y-O zPY1o(fM0FUU2>965YD6wg<)xs{(Q zHvzRqnMroFKmWogc0TJC2brKlfUOK8i~}J|{UbxG>iuFZ$Ddz)-n*tPt8N}(b~RWk zeN<5~qf9ua`)Y!;^fmwCxUs;Og3C)3S4rH1CbfupZo9W>Zl6|w_1T$gKtYL`GM3Y3sSEGH(NJiZr3^^xg$1IZwtkPiylJE^c$G>%}INVBj)D`c(70z; z$=O0i2aWBIKz{aN|H{w?)_uV+T|}(h-)(|ZfvS`w<*3lnmKqJ!-G(?0^kd=GmL~d^ zssDC(uU8DlJSPZrA9Eal$ve!b(F5*E1q=I}y_0-}38)7JIk`r!@3v;S4}&68Q}tjA zf?>fWRCbC5j>b{c|Fg=D*jl_8h(^yg2w z$1b2<0=eGTN4|K`TiX#!x8m~(Ese(KW*aIJV;(_fQsV;&SUaP4IcjSfO7_r>F^cIi zsKj36_a$)jpeRt#Rx#JUIOodNP zhJ9bVU%pc$dH36RXFH*$wnBY$xQnoY=hyiJOd8&Z+z zulASTIhQ^E<;9b3g>(uOi`ALxH^GrA{V8$RnVN23HAlkak)Er+8bVl?M>K1C3 z_Pdi4tX3G!j+1L4JFm8Q@WGvo`kX3%{vIn?HD4t#+2ONcHb!Cmrx$Q7=CFtm*pX)M zhkt7q)<`9mtGU)bLHs#<>G2S?d}@IjQQpxmP>w*e zT&P@>xUgu{mcjXVzw57=SD?bV(e=-{ei@>{(befw`2)P8_}0mT6?hTSCl6(>U1^9Q zwgySzUtje1KP;%OmA(J@Ml*sQvF%|NSGbzlPD$ zZ%MNTa0Ouz5kcZW54sl8K=rx1z8?QyAOAQ50%YWV=(V%00`l-+Lmg2qrzuyU`9FUHLYu08O&b57A1cY{&hx*%$o1!;kkZrg z$Ha}b9Ku3ZjoKxN2H8U(q^GdLfpMt~Af+My{C*bX*Dq;7$;t|PK`C@vAa4>8QEQr8 zSXkKD47`?CR>r}?ii=5*1djUpu^CME{9%Rk>u6z<|QIpwCL?99*2hRZ0?G^nY| z8I~mqcEQ~0;GZu#!N4BW0exkEP8+ScZb;(||U?6IF=SmH9!OpRXDfdF8%VYTc?|Q~|n3d^zkZ zqRZ2&8FQGxk8^TL5cH3ZToQS_BwhO?Dz2`|&MqW$*3J3wd?UKxV?6E11N*YES^;Dg zw|#sFG=+4V^E2WrWlRjw>1GTjbt2;`h+fjsEDPw$iln;Su{8Mnf(_@75zard!$%6p zyf|-AhqNRP@+!)eX+}@Kl3#ws0O(a|bGrHSK)hR{-Y_U)QWqb%_Ns@|F>{(4@0V_W zHaiUAz?Eg?=Cf=!R@NBY_WhGm(S%~nA;ct@_jnWld$zBZ9^a!{1Zz5Y*I*cuvD2ad^#NlLU zxhzaE54yay6%vJ_8wNdih)|w6s*pzcVSqs3hkhe=LP=HUsBJgY_b-+b=;(YsFEmr) zGC_GZ=!j>?3(8Jke)5$+lBPnl*J{f>r;iVCb6ZiQ zRz8pbfln^PxyBO zZ7{QSva@8%D1L)(!7Rv#(In?yl3FlBo0;RqsG$1ufOE?HR}J3J?@r}(*wc`lxiZfj z$jn{#v6O|sMuKd&KLmPMYR^APvS^DLG~*{wJbAh(P^@Egq%)bqTWPSt2RBIuWl^hBM8#jzkL1>Q#TJ38uNY_-ZS` zwZMF>uzTwUc{%}f{!RH+d9Eq0Cf53+RKtd-eD#x!dDzq#$&F2MVldOvD(6vjR8Q%c z86e%O20sX-&P$DEd{o|QYIuX?6-GqI$nzwvrK8~`kWodYLh`CHtkVx2jVxupq+V#0 zralLms7K8gza7SUA*QC)7bPvSTii*HNY^`K&q48>JsrQ(!#Z*-{PM*3{t8fc(6<6U2Rdw|MCf&;uh>_3SoaMyw7c{?d%#HM!dz4>D+)NH(UuI zN2{k#Uw56KpYKYuQi61ec-n%HfB+STjEI2X6Rb&)ffXRWx3Mt=$7zrd!jkfw&iZ#^ z^493XHSV?Ur$P2h#nE`02MAK#ZAnJtg5rv2(R{isH!DNm^En_lys(;P>A#eAf0NrE z2+dzw&t|Wl&t*x}_v(UKV}5ETrwx`1;X)cI_6b`M2^xi!b3vFa&M! zR}K;H4)P9tOasMH1k$4@N+U{6~x{WV8qlK#4lv)9sk}lVnh) z3!U)EB7`-7fE&=xAspm0U^FJ<^{5g0{bOITKzHy!X+7w-+0S@l>CYb!9WOv`I5PH; z@~8okfVoCmxKZ)L`~_kpsw)4v&lStBg7L|T(Gj`({>_k-O?qtQptt*~)7SskW)kFE z>?z43)~{7F44R2jio6`8hwDYT;0^8r>(Hv2h}2*ijK=DC6_9NImU?FJDED>?`mGD! zmLqOQU&ijOsGIzUJ`1)F8FfE6E{M0I6%=W|Vyg!127WQ8>EM2fy4sPN-7)rC0^b&Q zP0do`{`V-Jp7TDPKf=e7>Sn7{4HcQ!PX}Oa$bF2rf95Cer*-ev`8EPGDkaXvm~Ydw zfb)83*x%!g|FTCFkVVhaE2nR-?AX}ZmF4Bd#l{klLH6fDGtdhol#Ri%d3E_4uGzxO z3_m^nCdg62KTe{I>S8#WDdOwnL!DAz&zJrK{wMsmK4ko=I(wHD7V-fTL(pwkqgYKs zRh6BIY4UvxDh63m_CqCOt`vvuX=<#BOwK!W15pRFZ#{vZxC z4>-!h&0J7f`3DrP0As7rt+50xD;XIXY@%Oktnipc*jx`365t@gPmbH4P8|`*&s6dR zOqi%2ikg}P)`j7?&&&HASl%JH6wR(%xJb;;@ zPP4`i1d0ouX^WdnJ)D5^MFD3gh9)9ocQ`R~_UUq=IF4!W1KUY-W5o3n5CFf;TW2CA z-MfKci#Qpwy}b?ZO`6q4T$T#g?m>hah+gL$jcH5xD8wX8?7rmp_U0wZq=yphsSF(xpm&3De*mO zpPE7h!wEBg6EtfcANI-mvzt;BIBK>nw)SiizlXjgzwNF`d;jY<7%k6MU{q(qPk5Q& zqFJg*$`{zs-95-8#>_0h@^#|q-T~fM;xsxqcf1=vnetDAE_yMi9~Z;eEPbH~r+ieH zUz4YZ9f>K*?rV!WGSQ%rl;)fuafO<| zl1QgL{knq}f|Y{4!gj`PF!-yy#nsBPw};bwx%XP;v+5W^?!$#2whW@(vE(LCPmQVv0`r|KieD5bOdGJ6EKP--XgvEQ*gU;)f2FHZ{tV!o{sZy ztXsJCe6r2!n|t73yZSXYD-MZk>&?Jg{Bsdj^D6ED!Oj1x1*WJ*XjlWQq*)Qaj@HKa zO_Zi4m^)2)jG&@>fB$x0I`tJ|iKs)Y+x|7`zV^-Q6$JUH*FNvF+szX&y+@Nl{0id7F7q7i@ff9 z+|@24-1x_7;KJBEN($x0E{;WH7u4JQ%U^1u41;87N6JeGKjr25k<*;ym2zZ?czNU9 z&b2emDJjazN=u82CI9ndqy@>Fmb4M_@7(rhzI68xe{yo?D# zy1>#N+DmBk3*|dIE z14YBzkDfSrp5CLIhHAb{(B-Y0q1YnV+xT@*J~%3ob@@~f+bwcNGk>|msVgYD?Vl6W zWxCyZ*4}AakJJHo&GHqOdA7Ufccw}HCvG_T!sj^Bo}pyE|zeS=ZBt zyII+uF&gyQ84YL8(RdJ-9G83`p}7_hWPXzkwUV~!>Feu2DFve8z|)cJ?}xh+RKMsW zo53ylQ;kJVpcI?RUi~wk?Ltq^WdV^kFb1>n0y_X8Vs8AtBL-6Qw5>mpN5bCxGC0y< zK>+J*>1SX>zH}Eocip~}J~1=C;>aFY3|;ZYM0oF#$d4*BJ}g7vRr?!j>s{oBy79t_ z_Nu(R;0nrG7mz+J>T~FOP23f#J}qs{EOs|UZ=Gwu|8-Shn)HH%jr(oKp5RKRr21>3 zzp4;SY5BjLPyPbrz5nzAj9^9QH1EY$!IZ#1IN!q;EkJJ^pT+ZSM*rce#2s9=f5KBh0r}{3lX!xPyws>`R)`I)&e;N zg+#NI4_f;*zo`np57Le+0P+OLb;JUW%0uclFjx(9Ij^n3dF$W+&p$A^FV^zzCCnO9 ze($VGQMbWB<1fd?U%B>Zm~3I=pFRZ|B(NJ(av7IArlU>h3_>#}>87R&PZYQyk0o82 zt#e);OrXk~hopm!`J#GQy)oI-Uk&v|#^`=xF|Z6m#W_6SD$w;*l=&ejZaq=>26Qyr z(*X8`Bu-jwTT9F7S{wCZg7@A(uT8@Ibv98*2YCCRYieC~#xRbC$9x0pFC+XMJEGqd zxququCy>++e=OS0QC{p`Is9>WrCerW)VcT~NR`}NcPeA<`MT5Opk~Qntfffv&&?DC z()~L#u^oH}sMg@zX2-eH{|n=tsb|oDelq1(-hT_bsUESLZrDw)k_O`=x~i5BaEsrd zHzpMl1UEm^>NopBd>ZWx1aoC5%a7_qk&z5V)Lb*>-1(i%h#&7GLIkG+;UkRFTbxML(^$J1JU7@T-H+~%w$rc zhM?RQHbb9~+4)qFhnpvYHp#;)%^vc~YP8KnUducx2V;Tf);^a72*YxU*(k-piG+hA* zg{632==mDaqUl}G8cj+Xg8tn=JLj<7psD{?{dB&KmJjLGUY(@3NZ`$;m2lqSj6cgc z!BMVrKOo!wBGcQiOX{wddiG#v$c2rGm8u?X6Qw4~4bAMigt;+=i%_`7Uf#^CQ+`!A zGEiTKJmvuMawNCpZt;5 zxPH8=1SNh?+pMND?KXnel&$sl&fO~q^j!UpUn*JlyS+8W{%zEUaS{%M57;Xg=cQ9o zQTdG;u5k#{?`_2vjVn=ONQq#@-N&Oys=66hwH9<=gNOU=?Fa=;olh)8BxU1Baqo$2 zw0a{|O(9h>Xiw9;m21y7DM+Snp;H|HA%xtA5J@j?8lDgH=v?7JveBn&DSH}f=1-6e z6%SCmNPW50H=Ho&kKK2P+dsfG{t-$ebPYL>@oaalR3T#E?^Uba0fA&dycE0$=aa8z z_>PM*_qvw`8b2+IEo1b=jEQ#i<=C8b{9x_*7{&SX*tTX1-%~Fq>_rrGlB~R+n-0W5 z;ITMX$RlQM)J*;}arzt~{4(FbXpH82$jkR@U%xrx5yh^(3kNS~>%v_ zRi0qzda)ctUgS@Hli#E*oyddLfZ@R4UL4f$4?Oh%%ePNI6WXFYXW$B4Os%)Nre)H#itRfBwAiX(3&$Cmk*A!y`^!UX(iQQ#t(F zkLdzPWcQgEmy0Cyx>S6j->?MY<;+Z60_rD}#{m8$!$%q5QVYzYa(s=>7!(wAw*bk% zS{PX-l6+tiH|H&Ay~CDTJ&{(a;<2YKq;0-b1J{k&nIF`CTu7cTVpnwQhDY!y1Yi0m z-JoI9qG~d5P*WR&wmCH^dbpDOV@#=F^dA`d0P}y0Tg|**t{<@U%v*Q zH1uwIx$Y!$6!ZLsfcAGEUfk@4fc9-R`yZ4af|cbq4ubap^!qnLhoT@`BnGFfr&0o!)I)AEzCi z_J_XhrYfV+;p3;~=A#$*%8h$RXU?Rhl)-5T#Gtj@YL^%@DD@_#V98B22XpVPquuN*D*Q|K|h}yy;_{WD?7RV9m1KX zhUQ=MK~&7$j#$+)Z+1^&a{qE|*w`bN?;QR2@9u&);O~#-eTHy)jJ&?CL(e67 z=DA7C+!=>b_r7afJv%zA@PXTweDIh9018oV~q7X|B4J89%WY&0{R!yWVBVIWH`SOnI7DhynVAjOk;l(eG~tadI2+ zmYLgzw{Y{bVxP$e<}XWTc4cP^X0^Q>!u%YNUdyxn>h2wmQk#0yp_Rg`!wVYenD^S3 zz(0TXcq~am+4?!hq1ru(dryINGh6I@hQ0OcCpEVl(Vgpl*guu13=ox|W(?^9)yi!l zcb20T$TPi-S9DFuv5q*M#J{wUJH+1Xj>&?4(9 zYHK$@i~hgC>2XN=P0-qS(ykebmP2HOboI0SkK6vN=4l5sKk#mI+#tD5iC8APJJ#fC zRmcOipTSexfVk(olNjv9_7jC8z@ieMkrCwOg?F;Kau~MOXZ7bh3GUv`P6lB1d8DJo zAZ8v<;jREO`)z5Tt>$a-!Vxt_6r~86JRLQ4$JiJ?Viw50sj)(jcJ=Xva+IiVH2>b$ zZ)iRuk=NJP&x@6;h<((E3&CPoK4*ufW7!hfg=_uLk?l#Ib^5)-@6juDgPo?hO0F|3rHQ*}d#Oo#5< z#|&vL_5g}@Al(jMX04ZU1D6}SH6sIi#O4-uO^MFB_CGnj&dGL%~TgJ&`zA>X*IHG6w8+Y z9%^hYBoz;5E z(UvP=i38t8ZPT8ho_f2ei*xrjTwSNm6DeQUqwgFUiWi^%NgXhl#<0Iuz9_Lq-p}IC@U%58PrcPOTiM~io`933h zxun1{5-8juh*z5eEsAT3W-;}zFNE8-jA&Z zCx+LePe+TffkVvxjnUuDV|Q({LfG>oG^?NcVBTQ08j+uHI@_yb)|R=H7(}4m0)>^t zg8(3rZyF8^izBw6zPah&=$5N~XGfhO+kQ*F#;JuOLz8W!+3j1Qg*9?Ocho^xp5q3_ zY(0u%jT%*`#vhX-AG!>cd!B-#J<(Nb=7>LkS(o*CEMjkmBJ_=R=2=-=K`*FqW=|Ox zC*TOXf}?^$=z&2eH~$N{5({fNzF!H?4+;MWZvWm-Q@zs7%t%+F$?@Ix+Cgo9UQuCZ zde`RiK_)74)Hz_0oUn`A{jd`0Um>rFIS&P&RiIF^5Y26EBRun zjIz8BK`?sm{$cWmmH&^ezYeN8+@ro>y1Pp{q#J4J?vf5^0coVWMLH#< zyE~<&JES|Ly9D9AxX(HFJI^!EjDI*fZpOX$?~3(Z>$A9)&(}fRBvx#2xvXb$2~fq> zuIY*Oid&m3l~jLEZ#}%-d(W!;yWDL2Ma5QI2An`~XjH*`yWpDLc~9Y3{T(GGR6f%l zOHp=5z;N^DOagnE>|XW`5>{>U)Ej%t4_35~ASV}!PBuPk9&mhYOUVz&0{V~h)ebH$ z*EhQ!^&>+=kV3rQHU%7iMG-zcrC=*7E8E2p4(qev*Sqcn(f&^h&{kst2+^+EGLSBk zg;uj!Z4GSt2~LWj#vEj@ytSmy&-pJLf0ZtG&AWGDlBL?$7>8!;PPJT|ic-7yZ zs7f#nd;sr2###oG=F)NnbP zvf5IN$tiS|nXHjv9q($1N>D>=Qzheew-YZ^^06yZJ>m@>zFX@LTLRM&d6WpDL-Luzz28y{I}qQg$IV@KI@5_~Fq3yMzDw2f_)STnE^nUihIi{ac$-pV{erKuxa3_qIP ztmk(2?BeMI$f!&s5Kn{l$*hP8sWi#usS3RZigY6zw#x*(gxvzrzOU?V2Bjs~ir^xH zY*anGhT7YNG;nt_F~D15)Jbl%L|i{Zi8ML1W6tUF_L~yS=l^KbVa!*LAmnv8c6uVi zC#O6xuyZkN-uDA?aM=u7Yu&o8udtIXpR9!KJY%aHB8s}~+rvfiu)uu_zuWz43uhWY zn_bc{J^gYe!E>D(*%?XtDv=`jLc~O{q}L@>wU5`uoBp5-sCqC~Epm4d3 zSf7xaY+xU%!AQ1z)W5bYCr9)N&UAWaMieE~NT=QX0w6^$&3tjK!SUmjXR-#!CBcbs z`w1gP2~*(j7&J>oHtsLB$vLsJ(Nh@+=2tUCLi zSs~KlgIK$Hws!5-wya`ou)eF*#=H2Dj(1=1{+9T|(oxasPE5-UtdO9{f)Z{=!Nm^> zF6g=_Z|!2&MpxB;=aNK2(nFPp0k3#*{^ZtOzwzcr`U~TNKs~q*Fb|jp$WVJdRlh3% z!lhU2!c)%F@&nBm{=_%cb6W*Uptm!^-Jwc`HM%d|aW*fE7edM~_Ai%q0wu#@^hdUEfi1- zmeR6(E_6Ssb?{Gl;_G`IX#Sq?*=zb^Cs2;8Rkm0|4M~q^bCerps~Z~!d!@Z_wxRIokElf;GqK$nzE9J$Qb!q~ zVS65QFFa?Wk0cb>N^1|H-2N!1r#!m5mPPM)XSLOkI!Eb2&S~Cfla~a3x znOdNkQopS^^!8b5QS^FpUf5VxY58Wlz;My7~Jkf>dmj9IiKYu9V$isYv-pSI%6GuSdLZKq@ zq>fRCrzxZ%YtP09df}@jyJ|gAHU2_eNrV(KgH922zwQH;CHcZJQ(f!cLzjGYT9t&exO_% z)syfO#d3u!PkqXRWn2*9#e8JUW&9$_xOKz9aqOl~&B60g&ds~DpjkjTmpNp;HQJXa z(j2ZH@3KBHmh~%0B7aObE{&uPt4C7zO5SiicHAu3Vd!pkdxN!CT=FZxsO0dT&4Vzl z+yJW<3;TSBpxro5QkVL%aG(=+RA3hSGi*iI20XypM{5!BYKSYNM~EBrNl6UK%ELOU zPB(!X^9M5E(yRR$=%y)a&GR&mD%j&_yZHBCK!=%c1jug{W(jjq-RrSwdUv?#91~KW zu1mg0Y?ox*YJ&4crg8H=A7msSV^}HkZE-r-jte?ng%Rl4K+U?MK8}$BTVWzrC_h&r z*bChQsH{wNK-)H1Qflhk|5v;A;Pfx71PcJD()PgkYDK};@msE^TXX}CPB=@*1}jl) z>e{S=Eiou@lnp&i$cyKXhkFIXU@p*R0qQNZ*5~#(Ex$t*(%#-aqX~dP0WphQGnP3K z2Aqnk2K*k0-unvTi4J|Y&b1tAZcTBmaiZ`VdaDmVs7e@rXBYqPf&y0NAro|BSD%^s zDm6~TzP}h~F_2~}6v+>bWh1cg{UePNVlhsa zv+|gI!JA;xbMhwl-wMqR$+Bn?fBd)iQ$M29i#*XuDUp4a{X&XR>i*9CI3?&ztInEC_L-6)S5UkmPogK}h@+x=*i}di>@r?OwII z2=Pxy;b|6#CcE)E)t=?0K)t|+Ahx+j-;squ8=ypYA6;z(f`J<2EuE8@ySOTW4iWpV zfbg8!${VuzoXzM&xHQp+QG$hqzs#}LMZ{Z@;0yp&f(Axs-`;psS+K#Ww3MX+{s3n> zEl?OEupM`D>Rr9V&kRJk;?oC+YrblV*KIBS<-fSjqnh2oC3u2?$=bD3W}G$8vDg|Q zi&4ewB$U94{duTi^hZjMOGh`j7B}9VzUze98Lrd@VnVYDbKTENODvGO2^CPb@_q!@ zsMOOIFyH2BW9KMTd44%na9TGFGTiF;_3BMlBg!tbpii8Z@%8m(Kgc5&K_jAGh}i+- zOh&D8@^N2)!vO+Oq+G1VW?4Uo%)GN*@Yj$lpHwrYt|j`VU2z)Xi@P&+u3QMDQUECQ=y!iZh)+r;IUsQB- z<%2y}(wiRTeKgO!MVF?}q$8wXrp263IZKmT2Q~$oBY&5F?$BfsIfJ49EwQF{V=eyw zsIZQ?7wP?@w+tGrzujdr$o|xCfgx)eUlk$b23>P&9T1$#RkvYEBsMcRFKL+>!O+Iz zYoM}vCoDr0%V{oclvlJ$g5${z7;jpsdRUuj><>ZGW&tP-}rJB8U? z4?Y{^dnc5s6N2$V(Ld2Yg@=0KYh4O;C|kstVjf3jYS`{yglpUFy3ZuSac=P&t+^P8 z8iYgD9~?h>{sR38#rO25(zp!E;9ev8QYPda4U9Cv_IAIYP5PHrHyP(L;)!w*ne+6f zEWuzY?_&_}0k@kQGv#hdb(b5n2uUfQBf~ZR0yq1X*Q2f9`F)nIFq88`!W^SM|9LV( zo9p52pEH_DNCOARUxGgIM!H4U^y?QT$y=xQ*>8 zKUrr=`8=3;xA7lFL|r2D&f-}>o3S!ijGpYsy_zKJvebc^m4nFMDYqLw#C(G zlv%bSr6MaqOJ}7I-M9GmATqEEj!H86^{GyG2rFvng|abj3MsPlnC7gq}q zmLG276C0TNLeq4CN4xjYo@>?5EE#Jhz4 z*c~IdkdtB)*zXT*U|SGpkW4dd*qvVuq^Wy~NJSgd1-k_1t1r6%N8|%An-|2vJa-|_ zG6@I>DX{bB%ueMeW z$36559gN+0rlus!N&g~+wCP+)WRM*J*o+;7lpcjtHc>A2l}WiI_p1Sr47xjB4APJg z>xn7(6ARf~x1;>zEaKi=t0xwzg#VT`gOd?YN3Kx&2qJ`1fSkYz>CQAc*dQqM&llLJ zS8S`}{i)GR#KbR8=cLuu3XwloiUxY=gydK!_4_L=&q+@vJ_=IqPFo_5dGY^AG$jt4P{3!W52vF zxT|ptEd8kn@(TmpRRj;=H%Kx}b;DI@udc*iT$}Tv?n}S?`+w>X)f<5NbFFF*fTn?L@_(^IzK_#JH!$qTsrPM8nK!R#b#ch_PC5Y+`+nJ)?ktD6zGuX%Cz|K!E- z(WJkTWX8%tUa|aHmqL^DEBCRXdXA^k!Rm!)eF5xQ`Q*Mm!`kSYBvH+n($>c4YZ$^B z_TO|-!n4JvhU>#1tDB&?0lKEit#f}M`Wh_HqTtf~V9n@;94Y1SXbksxczW_TeEjr5 z2vmLA8<2PEgnFvgtdnaE*Pm@RcTcgu1L;NazNn&7p60Sx@Ri?rJpcv3^=^i*_Ne1& zEQHV}6E+(S=yJpH@!an?lFR_DP640%W4FM))76z|z9<7!t=*DM8Ee z-4kM-fG?26u@m#=Exs7taTn@y_85W_l82rUz4I??AT~eDtNiP$x}-gS ziPChO8b`k_{9ByG3sN>1_}m|Y{2LzBFBWd7!~t_~tPHkWERpPO9|brl|`#DoIc z96(6lH+!%LzwaGhIwI;AV56C9Rx1ZcpML-*umtg~^`G64zm*K)1aSuJdV&r1VuW}} z+zmm$U{@ULyPDsoG(KqEkST7U=YrlJQ^C{IP)aI}to@#YidHJQ?_BnATyHfU50gcH zt2{W~!L)KTFVfx10(oy`0n~JxQ3>!pOs&hGKGA4TD?ZsngMgYt1y6P3RN`iZsL7Tg zyoqz2i>yfE&P2}dftbH;>eb9|7pY>0j^4a%`k&P=g860mel0KGWw%g1Wn`Ymhvsc; z?UPLzR83DU_44fm%&ofy;3&Q%37ZZ+TgqKKljr4xeek%q?%=kAGBiW+aRgn71td^; z9Nsa&l}I@!>P_FdT8CYd2zELfLc%LC{nHj-Au**N;Tvsj@TK3<{1e4DqtORPwh)uq zGF0q{VH;NStDj`uq6e{>0vIm>MRU6p1DL^-o4w!9Cigf37U(xo^U9S*o z)b11_&Rfb8V*L8)83A4UC9gJ5yXw>7sjAl=iDCH5wU6)w<2!D}vD+|Z;I_LD4S7u< zn1~iEt|MR+-(DUpsG^m@>zozmTyU-$iDxlbWUTo&p)q*6+UA&T(B_s4 z&|XbnfE_JX45OtUkZwoa-)bKtf8aNBEo9>V3|NQ4>pXV0woF>X9gf>@c$nb#z@AU@ z9U}VEbUvb5ct%D>EJjuVIs*iz1f~)G_0<)&A^?E7k@yeOwoc*!^}QVE0i3^PX4JU| zCaWiwTehbp_gF95hBQnq#c!|1GA5wE15#wJI%)K-)-EjJi`l+~UMs zj)%O3cLn`Se<1)7Vz+&#mT#_b|18BH@#L;xyi(q27P6p;3CLVnM@j8(RV>5rs{hJ= z2wxKDk}I&YW%?_wvczkw{-HmVG}d_gUW@vFJ;r}@3ERCPFMP7uw*f)zlTt=#etb%I zo!2Kq;kf}>UzIRg&EmwS+iq@{puX6B9hRthPPMYLm1fqw?-gyV2>Tk zk#0Q)v@)VR)Uz43_#i=;qa3z5{H$RPT)h|calw;_QI!e|*In~w%>wyy*ny3of4x4X zeU&?GuV@Ihw*r%&PAc~r%1SNc$k=o!vl{N|fg}#P`Q&8C7AixSBJSoi9adQIhXqT+ z&!{lrff-a@&h(TK8Uy+g8QrJG3~H{qJ3c8_x;u!xL~K}o3qA`OubNwLF1Fb+ z3^ea)|DG{p{=P8w0xZB`O$QB?2KZGERPw}{VvbyANMF4B$U;AHYxMIA6MA&0Phz#Y zAYgQK0t~#KW)=Od2m!;!yxQ_Y{vb_he|D5IzR6jO)iU2g#E2UaTF z9Mex0Uw_Z-?rr^2)D+paXHG16^&B?z%KM=WkL+0Af?2G@aE`-3nESst<3E3<8==+c z8;bCk?DChax>uiGqh%kh+8A!%+;1zRqQ?IKwF?REp!CyBSYeX(}LqPchncXf@V+?7uA1pDcTLwfT0DQd(JfJflbIpH%RlEJVnj;;vN6g+sI zaLhk$+DRTqI#}6-D^)>U-j+^2Hm!bv>Fn!RYheC+uziFrwM|bR+Y2{B3a1 zGx>4fyg>~Chg@ZqYLPad=O@1SA6|E-sBidjp#Ib63?sw=G<9-vXoLOaGexdWJCaaxYId-M`3_HBJRI-iarD>d{0lmJ6Zm4Ec`Qe z4hZYq04F@W3H$<}W|K9M@9pB}&!4l4ZV%^>xh?^jGMf0#v}g!);rXv2Dr4gy>nDOX zs^z@CI$~%H08s|jP~Fabeb?3~E!do%l&YQM$T^YKaRFL5C*leS+7&8w4i?K; z2}o|3D*g(3Ul>od+rO*YH_z9**8mWpl@2}S>Y=q%UfP|gl_1Lg!`I?PkdK_7oSFmW zz0NNW9k6~rZN9KbldWzYAOH{~dn6%ZGlhh4b5DWR6|!$`?$hamUDYas_&BPs)U9WB zaM7Di)?!0jdAd`%n_pPXa|shtbwRY2wW7@fonHQD*PjL9*T?GP;(!RtvrYW;-_pX( zmPhN=Q^VYn-)*Or+Eo1KAvhxr!lwcSa9j<#X2)z5%!M~~)>5JU(b0`xh6%%1rh#)|WrGq^ zN1Py!#!L?Yz{T~?`gQhw&ISwHd(apzz_A=8#_4(o(a=>k0de2Ln!>72sM&Yx-pL1+ zOp(!%{bn`M-i0&SNf1P+(Yo6mF1UUhFmT|Hz3()w_8yirU4E-lR7LE-^L=4ZZjTi5WH2^-re;6)_q|f(md{xjBiL^dO8ZD9(R+EUOR7^dGCUeB6 z@iVvD8xEc`C$J{s-|ca)wPb_XI=GPZ{_96J%;_C7A3Y0hH8XqZ9SLd0d(o8q)@xfi zlb1K>78B{}%>k8!U5~_zSoP5Avi;D?J?SFi3X3^g_vQUt%~*Mm-of-@(kD;$0AEr< zr$gSX?-<0_47UXvxO%{0n>K2ktt)K!voEFUP?JpP`DjkwO!h6dW%;sW%f#<0QHI>p zm5`nx_<+*PNv7m|y2c+(Hb?NG*fL7f&9-4*~U-I9D@lM2E7x{xvZ&*o=q^zzl+U zB1Od*H+fpOn4lam`SVw6`)aQA`z2K$42#vfm2=1 zTXc{Elyvy_*Y!B`8o*JD%h|#BaWXjdE5Znd=|D6Hixz{%=5UX^dveWl&?pVUXZ|e6 zMJIh>-nf{U6xNv3kP=$}7Mz}%+O_%9o18;P$RM8Ul`%V0M8R$q3F%FK34RL=hth=z z1r06w4O$eZr{bP`?gA7tfFIy`(6by`@&9ja24&2+JKFN{^8b(CW|(VLi>T;mB_PdV zt~Z^74IvI?W9uUnjqE&QuQG-;-i41*&=zu&l)OALORxt{AIQXE(su)Ku!aly_v5MSA8SSi@hhTsr56%&G4n&K9&EO)fS!gl*d(ACMI2cu^7PK2a_!_mpuU z%Ew!DYXnn61WH&ZSadgrycREGla;1=U0F~SK=w*sUlqC2M*#k>SwA#EV0UD2-#Fox zLs$OVpC7Kd>tGfXO@nJh_#l0h`M6mbU?--@LzQopIW#fU!sj7KVAE9#9hwvBH)6I0 z|44)*to&!!K?Ifp&9>Kmy_q-vy{v_5iBni!kp%jkQGHZ!cX)e|6wO|f@p<4!iu4oA zOn*sGyZdq3$YY(q!rSkDVQ^$rWo9;h>)+*VdC3X-1zy?(?wtpWch$pG*%7#}L&SZ;1!_@tE#M_lQRC_WvS#I)j7eVhdKnAu&_}L42qK5S#Ynb!{qVFlf;tcj0g>^KBH>*%5Ix^sFrWPPSgx) z&L9juoOmZaH{f8ta})yn^+4D?W6x%9lm{R_F3dd$cGYv(;d#rztb7%yL-r7ewK>0X zI1o#T&J@x36~lQIF&-X0t?ucEN5JFK;qXQY&D3@BCAJiV?fQ@%5;}u7HK#y9IJM&4 zoxnR_kzNkRTsvM09pKDwB!AxV@A#=HLo`T-a(+yFk>NX}No+1!X^3m)l_5L1H*x)> ziVF~HLfX9OcQ@a(D!zK$avA*L2q547kF|eT{0)(MBsn|#1;URiBh8i+*&%Q&d^6a> zp=bLnK}kg=iUO_rjklD>zt;WvDJFmlu(B}f{OxmC-r5$Cg7OHEWlCjt2+hvUHtg`M zQF@yUxZ`VWUs%VT%N)f;{lD#Kkb%z=6A{C z7{M&)|CAwrPfQHP-r3n{_kM6%VCzn&7#U!FS_C)>a5=4Y3cq>+H`O3`WX0kzG3*=N zsQV>n5O~$5`@!o-X;|U4{p%YSZiftKfDMdl!cB%)BUt$aU#%G=4oe`X#fD7S<6uEK zR5vVI883P{3n>=hgnaY16ET0<>8hXt!oiASQs(D+3k{E`l&yjik`eo^!=OOpb)gzN z(xrSR{`?nscQ}0avQDn@<_ta~VEAeB!-8gWhH126!O%fQN^-tGN$>l@j?Gpeq%C;1 zsM}~?`KKu6ZmyE(;anK2#mGY^4J7BHJbTtWI>?nLVI62#NmR?nA&k!iHjEfISM_co z^*_(%!J8HF1gZ?-@6ERIV`QdUlL6m`kRkNvj;cYf1>D!Sowq^6~8 zjQHqP5gnAS_=B?~|Gd}(L+mYcs7JtSjq(-yV}}0eBeWOva*a^G32S%mNZZgq+%~cB zq%6HIW{wY7COG52k3L-8BTs4Phx!S;%f>JH2I8t*E;sjKc;0OkYxXcY*IGXe+Rpr$DPf@dhuim!9&vR4^m)?uCl2g(Ws{_ru7iQ+qacmyD6 zfn(jDyHFPlwePecWSXOZd|DO#%*n}u=cS4JM3QF;#a$q>0)Y-wVOo*C6Rr^E)8Kjq zXeQu{DS`gVbE^E3L}#^(*Z1R21&qvV5P=WHql{RRh3pf)*|y8Xc>FlZs=Av~{Kkhy zd(8Ag5p8Q^iSM~R^lzVV80RmB5Ug;s zjV?(Pzv|mME$mNn7E!-Red4>e`YJ`<#97A0xsP;RdT9>#g^?0>=P6tG9?y!9Fvy|j zZ{u3KB(Hd6ZiEQ#{S$-hw50ofm&!H!zOXpTSh>0o%6Em`>9gF-Rq7+qa zry$$*)w+aZJ&L6yhbv?mIqD=J5QnAToA*g@QgND^hS#$;<`_4`?(9oYH0+j5Qt_hz zU`j~jm791Z&HJWsx(@+TC|95$itnjTF` zXH-XNWeBY5-zsI2ULsFXqlqK(`?Wf~GpxL`QiuO|Fb^Ce^{MZ$3E4je$HXYRx!ndM zhPW+N*Vgt3NC)z}f;Omm)w!|{@YwJ5Q-EP|5DVlOL0*yWVJAFl4#UrA2%sInBm*2g zsOjj!f`e_B>JhJ@Yjh65%?|XwU>KPL)JPh)jTVrgP*6~6YimUUbsKF7-DXN5+}zw8 zEK|e1+ z$iAyf#ENG?ysD!^$nv)`ajx@IC!_;{`iUjV)`<;^FJR8a3J z;#FQ=Zma-a;yvhW&V*aSDk4Y>NXGsNOHuC)YH}O`K&i_lDTYb`F@GX7iat=kjbYyV z{Jq}lM|MNUAqFEK5Y9?4z3jiQ1$!%o)3cmvD0J1u? zVno6rr9Ny9utUl9eoRbE8%n_sh*vq;EKWPncjQc3Dp>hOn_hUIHUC&jN=n{$qb@aS zNL7;qjr}{9HDLS#pRCWOwLVrh^yagZo15>iqQT3pYm_jxOz6QE=&!Ro2$XgThqK?j zPZFpOr4wP*RP4D!Pq8_+FH+XP4KVO zXJ>;^7ysOy@p?KCLN8~B&aFh*{EmxDh`9iG$eH7}ExXt>%*+X`q?6w!U5!rHh8aQz zqNqdO*%7%9)6@HSJz==FSD(|u zC@KBR+y7T5&cTB}p%P8LigUs1Yl15Kuixz zqA8dnU_KgNty&L)k)h=hbQ=l~C48dw--n}RMR$=a`SLx!5sm3U@>wPUuk-G!y*Bw- z8Hw;;i8?rPPn|}q0-G}gB%96Xa{B7edQB(U#W{qYS41lnA^3PN12I3pmN{1NBOMYM zeO@}SnYIiJKjVz71 zj-IIHc7{E6x_h|TeaiSVlwDk^+ObSvI`Dld$#xT8Y$c_us_L|pza!*ICQ_#XE_%$v zh0U{$X)FOLmcwKI*{%mZ9dAXyF zkqWwtB)t#iwwd?$rLAoQ$rkoQa#*uaK;Z~#9HvKH8v5z;n9Z;$CnB7Wsr-0-0BxtD zs%k=}C@&A4lqjPF0De{y3ML{-r0+gwRMR49fcFJdHj|?wkwFOsT~Hk!9mylXCXN7b zjRS|M#Ae^NpzqT?Xqe;cXVGtlDCMDk28GUG?Q@PkUGMzS8Vfr`)LNPSoqqC!JpA+8 z(~Lf}hI6d1Yz_bhI?NWU9s_Xf-u3CfuViIOpMV)s6^qhKBHVShhHC$(#R*P zz}G$Aoe}doV9;?K0au<>6oF`LB!t2sSb4=i0{#dkGe^PrU>wz^R47=((XpLyMKMZ( zrkMiC5dxpcpe{w~zAYoHw;dc+5Jw~eY;1?3wBb`q?9m7N7-CtBox7>$LHayh*UL+% z7|&U`uhF#A*?&_nxakxqyaoE@eOfW|VX6ysz^kdfC9&4(9c-VeRWJ3F%J`^bO`o!V z_Z7u^Z2Y)wJFYcH$53fJ2A%a=+0&LVodDHI&nW3sw?0L-+Km*m<>!6mZ1;PYV$4 z2Xx;)rBbxOYXTx7B+Gz!I};PgqT;eLP8wivl&-MIiKC^Ztsk?kD$4jy4=R0%a#^27 z*ngNiA1~Cr@>HN^X`{K2-zb^e{xNJM*5=0m3a3NC`cI!A7?jwwG(!CmaE-64t#v!b zXpRK!Qnz<^Mb^zMOTBCglbZ^5F=WorQ$aYW9o-o;R2&Kh!T?T_+sed+wWpw=kqC%0 zAk&X}hIFIf;v{)rE*b>a+Zh)ZH@QE%Mzl6x_F-aeO_+@>mV7tj8}Yjy-oV2N-YL8J za-BA}c99siJEWF@0%Qf;zg7r7<=|QR&o66Cr+>r!h6X!{iCBTcnAYTstY?D8)rw)` z*A;i2@`!;?TBPIYCAs#ocyDN!4U*4Y`??a^JH9uyHDZES*l8lCnmOrYjQm6tjBi9v zwK5?<$2im1g0}OvxY$+K*Ym~yFs(#C>`KsxJbPqsPtw~x4v)-SYffC%0J-n?_)FI6 z@^9Ef6S?o2Hj{1v@pU*d{7U4_(rVR}P|Cju3GMX`tI1aw##Npv#aDjlWh%;#RdwB-m26=hF`XS3J z4zQ?m+Le=0_#ylcAYId7o6k7e*kt770M0Y@#}9HVq_@s0==6Bt=t3s!4bgJf9b`nA zY`50FI5U&L6B-dMtJCQ#g!@KxBX|NWsi3Hjt57nhkF$Lcg-E8HXWH1TBdyMEeK z1jis&MVFH$*Qs1dSX_W*>jMy?9GdK&z`fzy`=uIF=}&Kvm)Hd07-aTRSPb|bHXz#A zuIK^7;bXoIZyY}c+(vG3#%n#L$(26O{{1A^Xt0=zIWciMs0GLR$sz#2QH8pH^i%PS$yT z7d`2c?(sNgbB+^P`X=oCif|Y?0wt>15E;y10atf)Jrb~lRp~Z-uj$Nxx{B$7@B{5^ zw(`=k%3z(Bx=${j-7S$38MRq%WX>DG`uScj?n0{{l4!ES>q|5XG8_MoygVuaGBe{t zDPB4{cnL+R6dG^wE&$&G2}=J&#B)ckc9!DmH~cQfWFLb+shBK2I1~Wn)^_lTpz|I| zLkR4m!ots%MC0oN5Ra_cHB2Sei2wcjca>&Y@_7WPv5NvfWZD&gs1Ve~{k?+EeIp0E zW^3!~^IMr#I>9rIbx=?8-v==EMQ;ZO*Blp@&;So-{iv6X1o3qRCkLkeyH4w^?qouR z{DtGuYnu@m)hny>4<>=624G2uP0w_7PRa(2=V=^D%zumjobSYk->pTl$RGJtd$c-u zgx*(?ftPfzm9D<2F#jZp0TYMimpFGEL-xMBqJn}Q>|d4>S^fR}xEg8(2AQo?aEowU zO2ov(@n=@}_fHRXX@nC>oj?)HIIl+mF($@h-$})O4JqXeUdWqj=WJ95L~xKNN17&? zj1|$`f@!!pKhKT;W22+6M&&IcwYCZgCGskOnZ^RIkDEO7boyV!!=|C4Dn=0+Gos;d<1B!w-U zm6i4D*Dr_du&}Vlzkho&g8%P224fOOicjqDgz#jZMRI#H6H+&{-UHr88P$6-zPDxB z7$=X(;C+<|EJd5RqOmIb@#Cr1FN=UQo`kUQv$m|1l9H@}02N=B^H!UDr-Z`SN37H= zjI#2Avf|2qv+r6J%GijTKPrwIa;($5VX5gj#g$ZL6@P0tYm$*=SayCx$A%HtnBk-V zKOy41+;q3h-?2=Al)t}mrA~iWFW=tYa#55MB@4Z?UmG1Ag<)=# zOJcaZyqx{w2q%f;VJa~|?WbnLcU}j6Q4^tTd z9nu;Pi+`^X^I0DeqF!4`Nlt-RrY@K+o2aNy;#{fROdg$&>uguW|Asj(N%b9@V)Wnf zad{N<-NfOMp@70+Mh0xKO!txPP7=(f05>zolnE$01))4%pzNr?gLQD21(A|c(LXm? zlMNs#V14igggDZ#uU#lf2?+zUSF-oAcpU>PMmX#+JSe#jX}pyO^GVLH57c|^2$3V1 z#!Rqm2RehRMhrWBJAfr9iX}ZELA(m*SKwY^T31Q==p%o?gn3v`0UK=+EiGjh977*D z@&C5L|9y{;L}wP_X{WJGLt+YlA!f*Z`mT*gHYO*(xnq`3?NqAy8{gJu-^{|yG9ULE zt5T4PF8~~nhBai$bUYrQ(mTM(6P!6?h~HJpCeS7)Cu_Vw4^@MEpRBAbaLQ))e>8RIeia1UvT&~B$I zvZx`VaY9Zz;jOLwH%yuF@jXBBjKMv$w6yek5=io_`psQ1nL=K-<)G7*UR(s1WzZ>w zC%(Fs=>Ppcltat&AC9P%(p14aRxP3;H#|-G&n8i620y%zt%WhzyC8B zsz9y>BRY(@tNY#DmJq;k2_exT#v0D+<}U;4UfD_(qXdUdjo_h<;zIVMQv^`C*!6Eo zNlD3g_DNAOF(5d?`6vhAxgoAPJztuR0BH^p9zG0!@#5D}ujyneK7IPc!@~piYcTB1 zle+@gJ3bnlxl+md&{DAYhDSZ^GIn=$0T2cFI{f|ncTyu2jznA=Kt{eJU|t$Tk(&nr zV|+%2zLeB2`xrXK`-_%?D?AiFK0bv+bMT4sL=q9{&UXew`*#xL`K;q=7%tB%iE4h! zOu%{;;x2`HS$#{zST^eC&I0ml9|k$$1If)}MCT4OGMZx`7p<)+k>y*%!6W_pkjSvh zL}0qxX-rLH3zrtsg+meJ-^$(>mqt@Kw`OD6`FxkTCyoDcU;gV|hUPmqNozdOVdB^K z4F*wNeWtpajkYt}znnzm_2R%=90UgBa1V--k|BIB)3m5)pKNVW;(O0Dg;T8xCm9-9 zkX?@E6?f>tDFoEx2vGwdRkQUBEhTAo)^u=aC|n#5sDK20+1M@zT>;RdutjO$>amZL zC+Ga>6N7XLHwQ=VUl&of@gD`nh0>Ce0bKI;wf}(Nx)`>yx@x@zbOM+MTq7f+o~HBA z18D{v@Lp^G^yZ^RBNJA@-X&yZWo^zePM)qF=uE++l0qToiAvBZQhLM1D2-8Ks{319 zG_y#3&%)fV!%w1tJ63BkHJH!4^P@5VsB%ar|+M`}1=OBuqoHAOJcFDld_@)iV z_ha+jOz=cE%#ifJTK8ZDE%nc}&U7qHrP%lQc6tKa-y4wWV^h+J9WDvzKkvR#HrOx| zHfY`W_bfy?^7_fWGz|_8FMT;1^P_FCivJnn<8|+!$O)8&x!Ov7nSk=d#BBNf|8BS9 zZl0y>wWaZ)JTeuwHyT<>h7(I^g$cgixWZGdpBdLj9(|KjK4kkwT{$_`PQ!NUns&l~ zZDE3Sg`eY`TNEQ-@bzjM9*$sXxkfY#G$^ox%_w+yJOCDK1QW<>po14VL~=HKYx3Ej`3L#I~9*VQifQ?MD%-z#1CPg;(s=vfgQjg`1*nBw$TbM zJK3nmA}wq`IYORQM>Jq_4@%UF!nq(8vxvxoGTOhK8CvWuEH0{!*iN2>p!&T~Ta2b| zZf(V9WxZBbP*T7?1hNo=BLl!uphSR+Xx4kyQwzvCG@LD*j$HNjEc*D23=;5sq)mFh zm=F@`eE<30wWsLHk{4F@gkJ{FWsb>dqHlt1Ospjf!a=C=M`nd~c5)xJi-)bNWH zKPUU9G4qGGekW(@dh9WDo|k=cMgq}Wd*Xi#l^Rvd!RP~M@4vrGAb+S|{30RwGQF11HE%jvCUQkYmuG51nrviul^-ddfgt?1Rm>Wd z>3QpFvyjw~nOw>k_gTu_C^-2KpLBKc)yP>{@e6cbmw$Spo^`ME-?k_bx;}J53xYvI zUp~#@&uYm~OL}8I9Myj+!ukH{@(r9Fm}GVl|vGPric<;hx1Caqr+A>CROl6 z{hh@d?>Y$_hl}#TH_%iVaFMvMVLs5RYp?vf+(jV_ic0F5rQ;9SCWil3=9%OPr%r=l#x9M|-*~Q2ECQ>qNY-~4n0#Ru^mp?g{3u#fS z%=M5OvNC@T4h)(dAoCqlp}|7K3mDbR5JE$apqO*2-Qx-b`8a8Na1k)b%dP^r1IU@= zN-P25ZX+K3F$W3UP?DC^yt#@ug^Tc~)B8XDUZzcT&!(du5?L?qJYS6Bl2(*D!K z)gm9z-CzFQt%KQ_nVA_02?_7B?Fa#eTuI%eu6OEFeLYt*WgMPwcA|eZ5(naDSrP|n#_NaxcPzD;zy;V=*6ll)fN-U9g)tKCE(YGY9VKCbfy#Ka2x z&L*t4l`x9R$B!Q=guem3;|>&P1CgJgYTTf;>+9b8|7ceVH5c2zZccv3IJ0E*@YI7u z8sW(a_&$IWmuNyZMDY~35}E&i&3tXr2}FQgg=)&LmUVg92m235t5HAkiGVkJSg15q zYVTY6KfHl{N_P8|#D9&$*(Omx1 z%D%KUA@A?+n&How;fcz9i@I;tGnE>w&Oo%*;HcuvY_f?-e;#o~EGn^eBR-Xa6A_uVdNPG?`h=8gUVa{q(${G%9#TpTo^}uYtJ^92 zO?=T`$v*$Eu{tX>7?M&_E$J?%Ij2MjOhg}U`!Epz4gae1*$Ez=opplp9pG}uq##Sb zJ6pB+{ZmM$B|{F}LwP`7PYSD%(-~)kx zO|cfVL^J!k>6UbI`4=SMv+n^oImt8|rGv}mkBSnB;$D2o1$&W_3Zpa4b+VS|ChugW zNEc|gxugbbWC`zVrch^wOdZ4DzYL!35GS%GMkYsuSjl60xm5}pbQgWt*|h`>G!6ngWb0(QO|e5N^PC#5GWPrpML@P1;6i=V@ZG9Y=hwB$5Z z);ZVPofLDWrzdIFSU3gWakk&QKA$^H=T-xPlj=ePmHDJmuseGlf;F4FH4T)?LnA{i zt?n{FwO%Z`H(a{Y9_-@*8f4#s#awGt>8kxD>rF46nPteMFmki&lV!t$z_LfnN(Jy^ zf{z~l<@HJ;;gMnizT7IHkqH!CH@CIdCCEmZtxPwIFtpwKpp;FWrUt`08YJjK#1~n za^lzw(_-E!r}73PF>rhX@gjK2YamUD+A!@T+IY+4XjBh zKG*ZD5u8x(c~duEo3bu*UU$8=*4Ds3Xy%~%anmBqak1J2TKZ%ScnEyd0xr@~kD%Kw zxr_|AcKq(R7ZhjG6aIm+a*JZEj z)C8L*A=hlKHS&Gk_W1j}-6YM!F+DSlyvE#6DK}5up8X9vc|3^@kA9YQ1|lNA=LI#=}!%Y%&cmb5hg&OM&pyEA5sW!yZ#fahk9DgWUA$J$%AWw~|X!XhQz z4T5wCNOyO4cXvohcPi4|NOzY~(%nc&H`3j_)3w&K_x=Izaqx*7?!M+2PwAr@VU zGJadYMWVS9Wm`)yr$Uyb|w&4M1DOL27g2fw$Wy<3108GRai6?6Em zGeZ%&E;sSpsYGW9D!pjU;|uXUjjE;L0?e+ur9**(QI{W?wk z7ijoN0{ft0S{GZhma!l zHPzM&F#LfUgP(iBcl=P~t45W^%*^K(%)O@*X64ZE1r}RVQ!|@KmW7_Stiq6jxI%Sx zYiVis=9A~;uAA75IOX9ZuUwQ4k7d6fHUzp}vmJfKc=%{qsUyL`IbU}5D|JShXp$_^ z^5~f=<^+d_r1izH+9!>lp58L1#rH+4cfT@lJ57XzvoyM$?w{?Cogbc*gCxCYPr;J+v#6Y6(vE_NYN@nJ#_*ISY)xv0e=8uoa3O;^o`Dg`DWx0^>c@`$>Rc zkhn32&XGhlT28)}w*t+(+fPsbTS9J^V(Zyb){1Z6ltGY2^J*^3YR5}UE{)Q7XM_LB zIR7FD*|4y%FdzE4mc{E)OA7N&9M$MxOiL45wBOqRAtw!2eU7R8*nBDXV)orru-W90 z9ZzTbTXI(Vilw)_FzGX_#OkW^+qPb7d(YM##hmO%Ji@8JM@Bm1`xlA$2fbrzv$D*- z{Gx>s$MJGAP7ae(t_#f?(Lr^Xf{!s@Jzmi_`7J8x{$AI!&__lY^DRO$*Y*B*tC|w? zD={ecC%eT@-=F8!TD_~#0plMW#NTzz*P7DQvY^#9Rqnck%zCzdi>4+*mLAnQ@H(pP zKEj1hyVQSW!m>9l8<|`!+!ss3{U`DRV5^GzCA+QdFPTSq@qOC-igHQ?2SXV)3eI5x zyi+lo6;ypTnh@|M1ZM_DgZBNk|Hk(Gg9SM>{tu4xM3h+cz6R@Q1c(;Qd{JEfotUIV zwo6D`&7#L}L+0M>PRD`|CRe48-o?mn-Rov6%_ZY~4LKVLtxUb*!{3?hG~KLAS9704 z@r%>beo_z9(gtR2)vXJlnw34XrM>;mv+khGOs>t5Q@va2S_?0btr=zpp`W5Wsu+@-A_;AiUG`f7gdk`x_zPC%4d^KA!yi}qsf2=_7Ok<) z`C1Y9d3p9@*Ycjtv&0A$>B7S)C;yE3TDMBe6rW8Fpl($9lXw+#*>Zi4qzc$7MOy((b9ZjZ$ zpk}L|A1UAu4p5V_6rn)rW#K+0hoevGw zBYjRw+x=0$NjUqd#Cj=$;~s1zmNhR3{R&q_N2nVbWtfD6i|nq&Zu7BFr0jAYSDr8K z`8F0a!uL^H#wi_FCM3yB^CK5Bw4hm$NM@cIVD78lT;*Y8Q^pYD75~pUr1iU z6cGkmTX|6_JN#bW5)-3*F9+Gfk&%eh)&PAyxv`PdL*=Q``_We%AA!_Adq^-OG?PX+{w9XZZAw##B1f_2KuQ=7(#t}a-1 z_A>MEyGJlP+dti`)vamA9i^or68x(^IRtn@U3FI0R|cur`un{LS?-fJbXk}oMuW=^ znw6C;5ztf&Ot+i&{BzQ=H+h?*5J~tNdQYq%*iXSC`i%Cv5715V#Cjnt+w$*B-}b~Q2Wq!nVp_mt~QappY%q}MM!-Y zr)=7!(_}Sgx@?{{CM)Zoj;2bDYGT;ppH4)1n!BjBuq|Ycx>xJr5n&))J3X~d?;ZR- zm6nDnj=ga7IZxFm_+zyDI+8Ui^-mJypJq-Q`QCo#ildsSjN)@YV@ux?zQaCdjID@M zi%EW9Q%5M^g-yfiu%IT{cH?vlU19d%;(~jZE##ug!7^Q2AL@xasDewaa#Y21`M&uo z2ej0DsRqP?36^L<(?9fEg=_{wvO|2^QwIl0G=+zam|0g_H-VZ6jCEY2IZf!Y=gpn3 z%=C!#w{yN*{b~N1e(Q8ZtE@|&854Jnm_DW71OM~WY9%m5i7Z`a>Iq7WgOpD?wgYI+ zw~Vi7v!{*XViRYb-2$Z7Xt}e#JAT_|9Op~e>W>zcyx%MuFhake)2IFVw?T+mm9HBl zu)}vl?RbnDQ}0Uk%S3o*CNFQ^cbtU`ZjK88!j=RHK{;mpDtlYPfg{ro}qR)$$lORMa1jsc}3}sAxnvQ21qBtBuI!9`tyij z&*OsI`+ICv#5Ax5jbq|EID`+dQKcUXi;C*#)q?_q0QlZ)Jxkl@R-B*j0z4W%K0a7j ziD3F65@2}aHBqEMoMyRLPou_1%C#4u-eW#36|@Di4aQ_aqGyaJ9P4|tphK4 zc;eN;06ssasY$F*!n!mp627RNHE`8A#JKPGC+enAJnl_`HsM?M(cz)@Mh~1MCl{Ac z5FXu%@g-s;PxufJuYfbEsix+gx6Tou_<*DXqu$Sb>AR~K82wC8aZUJqT!bnP?favxG3exgOlO%dqqyJpArbox^41& zmK6=>cHOp!eFk51yafJGt~(|0J-h~{%h~T;n`EmwuyRmMQSF2E&?0w?%-6py|3`$-MTUHX!a;BYtZ$yWiS3%BmWQYcNn|0LQ)8&>jATROTx{=P3Mmmi2P5cGST)1~EoKIf^Gpv-~BPN%u zv2U+T!9P_T6!LQ~p}$la8Q?(B#~uOVyl0n{)e1WcaPm%30qBTIh4y<+pmqVA*x|Ow z&}YD10R_WbbP{I}3!5|u8i#BkCq3o*YRHVHv!>cnbC#6 zNu^J$FNosL8w(z=-Tm%(Sxj2IbI`=qd_-wxg0=h@#;dBU zPzLkixD5acB(Z43{d={YVR>O0o;P!@PNBZVY*vfiEi95BYyQxlQg_-$2y+~8pUXOy z>og&>JH;lz0g5v+$K6cOp-;v+^2H0(rLuFokAmd>;(?tpi5ZBb8KJ%4aJ1i~6h>NC zAogoO#J-mss&Z!W2!<$rRE7HStpGH~~<|9R9^^w5dqDzAa zcN^{%+unM{e#H$~L*Tr(5C&ul7H>lL|70$yCU3JnNc3!mIP<>f$w-Ihf1Ui$loL_E zM;+C#kwO(Bi)95-hD4JrxyX|9O9l4)L)&=0!%yTnq;Jcaetj{*0R&004R2QWE`DBS z`>{#Zhg)fB6h;dPQtbX{v?z+)AM-?U?Zx5+u;k1nLm^)#+aUcBbx_c7_!gy2yk#u*XND! zsoKXb7a}nZK?{ra4RzCm^e#GEI8^Gf%H4|XpSjqVv1u+c2!{rbRL%+wssuF$w65Fu zc6N3^`)*@nbC8hoQEP;gg7R@7NA)W5^XH8>*=B#iX8=yZ9+en$@bh$tzy?6{fNKG8 z5>Pr~jg3H*bKuQkKLULK$av;lX)K}U5*f#DrpLzIvr@3;C3-_%j26Nh%Q$k=*Tin~-fNT0&BNBuNK?-E*e! z?dwPr#-A5-!Nh;K&%Tg&e~Hoykarhs!$4k9f3rfD{8{DwLBse1SBBI={JAw=bWKNs1zHDZxNq79 zb*_c_3o0#*cr=&#SF4bz-Sx^tW?#6B{ z=(=E|!?nT|U)N1`Q|7p3XkALxip9k4(@I#gNyfjb4T@%dXKvuI)Kdw&l@vOxo~Zbo zT+ehy3~43Y+ldNAE9oca++e{( z7Ml2@*x^wWz0+e@w*SNuM+9s5wGRR*>1E32b_gEBfIkyxKtvZk{HpBR%Vf}DhDQ1? zC95P4e*c8EeucB`vuBb>WfAq(v6=ZZki_g@Ma9LPoRpQ7 z1=A8^#XD)O=Z8YiipxRbue6|fJ0s5y8EydH48-1b;Lh7XRZPrT=o`u91q66n5wfO; zt%rCCQFLJkU5p~lq3Ab_MmDjnOKoj^w-MD#$V*SV^70M~R>}(|pEE7o z{W(%n(%cDspctAc(FJYV8JpVZ6ned;x$_)m_BZF0?Lk%F&sWq9t!f3pib9bYLkNF@ zXVhcR179;u4+qhYtnrf0F>WKD;_ZCt*yVg(CWbYAcv%m`=fMht@^YJ;=&zp8FSRvU zN`5jG%nSGm1~c>psT)7v=3?^8qWknFxBffj3&FKIfBuWkE6Njzyc>AoqLRJN8amWpzMi=|UK)K+5D$fk!U~PZOGIwn57|LNFG%EBmc;Fh4BA; zxT!rle0JwKZR@gKv!W5t^43Yfp(5-HlRXc!am#dUI)3!5Qz5vr4)J{F*0e8^Vw zYri}ygFkT-w+^vqG>u}~#!|g&S6RkC45ZFIwiiPQNNJR8- zdE$QV?RUcQy8He`gZu!`wSAu!_2Fl>q~v-#A-P^p{dqRyIz2RW^0>UrrnjOw&Eq9~ zU$7}zY%+qf^_P^})`gl7-!OISncy8&f;*0@Z?@LKvR{f;{D9Y!38NgD(8vnVD?& z-TEbse>xc#mlvw$$Q-WZ{Iu`#*R zkoL@=#XJ%QoXaKW`s3JPBoUFgrrQgI*qXHC3-`IXnaqDkFLDz_W%6k*scCjU2nc=4 z8k9#XmKcKmVMiTao-iXECEXqlhxuzRo#WQ_Hu_QhGt;{T%oUcdgeg)={tpzSKKl5x zx2i5wy~;t}bEF@U8?N z-f#A!xDA5MGtItC)f`Sud7%ZaX$XO-yk992QEf;FlyGSjGBpw^LXKe z7Z_^!|NIh+%KB1fhVYX`Br>)`19SXs956vvtLXMs zP#?9`_Lzl}jBRY*)rG5CH9U}H_IY@;BtN(nu|O3D_kUCVsHTldbJyXFOSKh3RxOth zi`EV+#$WJrUU2yF;6_+xreA6iH?)|esuKCYdA{wwME$GBu|i1^l9<>ogovb8Y`S0c z@YB7DsJ>VTcMmo!7X=(P1!*5CrTKUdtWQ21p9B@D8o200qXW`jn8CgE_y`y7b zoU6)(n=|s;=TcMJd1--S7TEmmAJiH7tg$y|=GMNYl?yKLd#vdH^u}2D3Fc_|zlB0% z*T5B%(!?PpMiG?;6coPgY-@8M|T%wY^h+|Gc#gw|5&HwI^YmMFhH94ul zowwip8+JD^1OlqF&@8Y@_ge(e$(1C0vo|g!=}WfqKBY?gU^5Z!Jkab$q}(nw8Xaf9 zn#ZvlW91{1@Y_8X_j|!-l~stDMcl1#K;J~l2cDbthJJKmc9kynS=YnI)NpQMDLL|7|KGban=~l6U5>K4tc_}j`8e1(ipoYl(`9)<+GD#{<(`sS-xH27V zmwYT*D)~KJtPLwT4ZDs`Ix$XRK-oCl;KN15(Qm&Gv2VdRs4d;oQ`dn-hj& zUd#B!#tz&r?H#}UqPo}3N#n!<8zlQ0y#EQ+RW1KEoK6z9?L5qs>U3uF)kbM)q;4Bv z<4VUx_afNs_XHAyGxC7+5#ZmX-wbUv`S`xDyT^rtmO$CW00$=-Scoy&op-=;Z|BcG z50vh-tw8Q@=ncFz09V9T_A|^e?3iC!$&!o!N``4spt?aqL`?Apx{qrjDEW0T90?C` z6il)Z3JVKc3Tt+Y$2f_t?PIV`Ixf)SDb>W&v0bW8CeLS^!mV@JCdB44&T}Ou;?QNd zxd9t*0Yd(({(K=EXq0x`mf5Y|)M41hk9It+gN)Alx1!>&SSmEu+FQk@cUaNfcTBBS z$#;8G{IE;>c}+&Rx|DMYLEY;=V>sOY)9yXl(`agA|^!$#3#B_f|*b*ta>R&Eshj(=`y! z0DSWgUd{EHJn?ud(e?H>nc2V(y(P=8Mj4Ll45NwK%8?|cjui~vZ@bL4gt}N3E$_pK13QMNi@|2 z-YyuNEI(^mv^<`XA;(1T9x&OnRwb*TJ8-5ng<#V zj$vNgfras6x7JK-tAHVr#{P_Wd3$D(=IH3C+3H>c64fln9cO{!7ufvXL1W$Ar}g|2*((ChngAY-nD6r~p$Mzmv-Kwxm-via(j+tfO< z+`-I|>nBm4%>#!5Z=R9baM9u7QE?{<07?K0NBv zB!gP_+j-khEiC%=_N)3Pz~dbJP|rKMOykQeIRdz5c4ksC&WMI}5W|ydiOIz>m9qU4 zUtnp4dc~4kiz87X?7-3~+D?{SdifIX?Q(yg4f}@>BR3zv^YG-j*nQtkep@#YqrL7Z zD#jRLi1n8J`wCJ6hM=%m=5>I6y1?p+DReB&@{p)r1V4@~b7y1m23&5B(MNRAC!>}n z*%h;l=T}#Kuom^~1lm%2Bg)@|TnU36?9C^j$;*D9B{39Zx%x@fE`;*x`^${oArw+O zhpl3d(Vo|D?}?}THfz7H=0n}2Ny8{kRc2<(o;N~(zEOm*d1hJ2)i8*?Vp(a?gFy)+ z0AfVxulP4-o(yHhn&*8f9;|^QS)bkpWT24ZtrKi|zSrI1b^Jh&>VBTGSLS;ngCY0J zKCrL*wb&dUv=m{R^Tg%uy zxb-Y%BIQJr7@g4K>n)f!HdhYkg{LRnHFa{%6w?7C2(o)i?!37zBdh2rL zofew+nA9vRlS3!?)6Va3#2=^q?U$r@W}zv#JBw56O>J)dgp=sDHJi$q^F(~MGu;OH z(E(iUqU|W384`ko=iM@2WR)7fM^z%G?>Q%h!n!t+Xi^;;f6b@hCtsM%jIj_j?S!b( zJT7}V<>iP|Yt;NBxdQ`|F~5TqJb>{E0A|IXba{Aw0BcTNwE1rT+s#t|S6m$~sKlZ? zs9?X>bUc`$**U7z`$4NwIl?tJKcAv!-uoE~MA8`J%mcUP7i{{JrpT=RzNIA|5|URg zT8bh+PK}U(t800tzPeN6^y5rvGcs`+9x?H1ju50<4G`?Z{jjnBtGB(UYd8bO9CqD= zz`)m*H2|A7%#KS=hFun>eQkU#TLXuP2({b>$fB6W`uYrJGx*UC-zRQwc%hi{o(d+i z#Y3Lg1k=^fC_hs3N#kQotZ$N)hepskE~NT<>l4CEcst5ZTO$qeY^2zM59OxwogJ-UdU-_wc-a?Thte zxe^XphVRK#ycXZP*C_b;ITA}K%EF-(NjnQ$zG+ZJ;Pe>rDHM!(xIX3o;RqP$>YsXE z2eQSwbCV^b0eSg~t>y}Ue2;*GJ}?d6)(zv#--uRn@RzI8^>R~s0)I8+v+bR6G_=gR zw2IS0P+`-ItyY8QITewh8L1sfo*1uC(@^e!BzUe9q70Y9`F) z>Gf-)W0Jq!G5pj_c9r(dOp(Aqw_}m&%L6gx%X$VL1XWawF+|f{v>zP2-_irMP ztkj*D$LZa2qi@b!%Keh((wp8)eNNM!6^D0G66y;yd%+0g-oW-|bf99Hxre%WQ6h499IRit^2tMBFalK6khMOlR z*)cJltor$^2#9#hJ^lS*nUEns5a}B8ukng?QsrUf2o*-Y! z!PuDefehaB^(!G?G9Z2wIMn9@s3@rmk7-en&&7A*vp|A9?&uaW_~zc`ZD+rh+|LZz z@$}yJj9a@~<{wW=`Vq|Q7|D}eogWdIG_-R^YXFL#)S?A>CFUQUjn%Q(teIzpXpj+D_oxmogX&adV!OF zPlciEWkCZzZG&n2doD-SN5_1b_y5BJOvXt*yFMxBcZ@CyV}^7M@h0Sl0eob1R~$ z@3TOF03jb#;3sw8U?&jC?+`UpM&tCLGu0O!0feD5yvT(%f92Gr!N1q!uqMj#8SZP* zI+l#?e^%Vvk0Y%BaXpeZ(qwzAc+0)D^cJWOX<69>v$N) zmFM{~nGzhc2C7q+4NXGu!-nxmW&SHCW|nAnrx*8Y%my{HfzjuJDqNGFf!MIfb73f3 zA?jujf98R=0_tiDyOHvNXz`QR;Q%0D8tDRs`I}C~%H8>#+jYXIzU*w9n>(Ga)2^JX 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 literal 0 HcmV?d00001 diff --git a/pi-chrome/test-suite/browsergym-action-space.json b/pi-chrome/test-suite/browsergym-action-space.json new file mode 100644 index 0000000..5d7c873 --- /dev/null +++ b/pi-chrome/test-suite/browsergym-action-space.json @@ -0,0 +1,44 @@ +{ + "subsets": { + "chat": ["send_msg_to_user"], + "infeas": ["report_infeasible"], + "bid": ["click", "dblclick", "hover", "fill", "clear", "press", "focus", "select_option", "scroll", "drag_and_drop", "upload_file"], + "coord": ["mouse_move", "mouse_up", "mouse_down", "mouse_click", "mouse_dblclick", "mouse_drag_and_drop", "mouse_upload_file", "scroll_at", "keyboard_down", "keyboard_up", "keyboard_press", "keyboard_type", "keyboard_insert_text"], + "nav": ["go_back", "go_forward", "goto"], + "tab": ["tab_close", "tab_focus", "new_tab"] + }, + "signatures": { + "fill": "fill(bid, value, enable_autocomplete_menu=false)", + "click": "click(bid, button='left', modifiers=[])", + "dblclick": "dblclick(bid, button='left', modifiers=[])", + "hover": "hover(bid)", + "press": "press(bid, key_comb)", + "focus": "focus(bid)", + "clear": "clear(bid)", + "select_option": "select_option(bid, options)", + "scroll": "scroll(delta_x, delta_y)", + "drag_and_drop": "drag_and_drop(from_bid, to_bid)", + "upload_file": "upload_file(bid, file)", + "mouse_move": "mouse_move(x, y)", + "mouse_click": "mouse_click(x, y, button='left')", + "mouse_dblclick": "mouse_dblclick(x, y, button='left')", + "mouse_up": "mouse_up(x, y, button='left')", + "mouse_down": "mouse_down(x, y, button='left')", + "mouse_drag_and_drop": "mouse_drag_and_drop(from_x, from_y, to_x, to_y)", + "scroll_at": "scroll_at(x, y, dx, dy)", + "keyboard_press": "keyboard_press(key)", + "keyboard_down": "keyboard_down(key)", + "keyboard_up": "keyboard_up(key)", + "keyboard_type": "keyboard_type(text)", + "keyboard_insert_text": "keyboard_insert_text(text)", + "goto": "goto(url)", + "go_back": "go_back()", + "go_forward": "go_forward()", + "tab_close": "tab_close()", + "tab_focus": "tab_focus(index)", + "new_tab": "new_tab()", + "send_msg_to_user": "send_msg_to_user(text)", + "report_infeasible": "report_infeasible(reason)", + "noop": "noop(wait_ms=1000)" + } +} diff --git a/pi-chrome/test-suite/challenges/01-is-trusted-click.html b/pi-chrome/test-suite/challenges/01-is-trusted-click.html new file mode 100644 index 0000000..50a8050 --- /dev/null +++ b/pi-chrome/test-suite/challenges/01-is-trusted-click.html @@ -0,0 +1,28 @@ + + +01 isTrusted click + + + +

+

Goal: click the green button. Page only accepts event.isTrusted === true.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/02-is-trusted-keyboard.html b/pi-chrome/test-suite/challenges/02-is-trusted-keyboard.html new file mode 100644 index 0000000..4fce713 --- /dev/null +++ b/pi-chrome/test-suite/challenges/02-is-trusted-keyboard.html @@ -0,0 +1,50 @@ + + +02 isTrusted keyboard + + + +
+

Goal: type hello into the box. Each character must arrive via a trusted keydown/keypress/input.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/03-webdriver-flag.html b/pi-chrome/test-suite/challenges/03-webdriver-flag.html new file mode 100644 index 0000000..c5953f1 --- /dev/null +++ b/pi-chrome/test-suite/challenges/03-webdriver-flag.html @@ -0,0 +1,51 @@ + + +03 webdriver / runtime fingerprint + + + +
+

Auto-checks runtime properties commonly inspected by bot-detection scripts.

+

+
+ + diff --git a/pi-chrome/test-suite/challenges/04-mouse-entropy.html b/pi-chrome/test-suite/challenges/04-mouse-entropy.html new file mode 100644 index 0000000..e4f4693 --- /dev/null +++ b/pi-chrome/test-suite/challenges/04-mouse-entropy.html @@ -0,0 +1,34 @@ + + +04 mouse entropy + + + +
+

Goal: click the green button after moving the mouse in an organic path. Page requires + ≥15 mousemove events with non-zero movementX/Y variance before the click.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/05-event-timing.html b/pi-chrome/test-suite/challenges/05-event-timing.html new file mode 100644 index 0000000..16269ea --- /dev/null +++ b/pi-chrome/test-suite/challenges/05-event-timing.html @@ -0,0 +1,34 @@ + + +05 event timing + + + +
+

Goal: click the button 3 times. Page rejects when pointerdown→pointerup is <30ms + or always identical, and when between-click gaps are too uniform.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/06-click-coordinates.html b/pi-chrome/test-suite/challenges/06-click-coordinates.html new file mode 100644 index 0000000..e35ae84 --- /dev/null +++ b/pi-chrome/test-suite/challenges/06-click-coordinates.html @@ -0,0 +1,29 @@ + + +06 click coordinates + + + +
+

Goal: click the green button 5 times. Page rejects when clicks always land on the exact element center.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/07-pointer-properties.html b/pi-chrome/test-suite/challenges/07-pointer-properties.html new file mode 100644 index 0000000..753c523 --- /dev/null +++ b/pi-chrome/test-suite/challenges/07-pointer-properties.html @@ -0,0 +1,29 @@ + + +07 pointer properties + + + +
+

Goal: click the button. Page inspects pointerType, pressure, + movementX/Y on the preceding pointermove, and that pointerId is non-zero.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/08-keyboard-cadence.html b/pi-chrome/test-suite/challenges/08-keyboard-cadence.html new file mode 100644 index 0000000..1de9bbd --- /dev/null +++ b/pi-chrome/test-suite/challenges/08-keyboard-cadence.html @@ -0,0 +1,37 @@ + + +08 keyboard cadence + + + +
+

Goal: type secret. Page demands per-key keydown+keypress+keyup with non-uniform gaps and non-zero hold time.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/09-composition-input.html b/pi-chrome/test-suite/challenges/09-composition-input.html new file mode 100644 index 0000000..b4b2002 --- /dev/null +++ b/pi-chrome/test-suite/challenges/09-composition-input.html @@ -0,0 +1,45 @@ + + +09 framework input invariants + + + +
+

Goal: type abc. Page asserts React-style invariants: beforeinput + fires before input, both per-character, inputType==="insertText", + value mutates between events, and no compositionstart for plain typing.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/10-user-activation.html b/pi-chrome/test-suite/challenges/10-user-activation.html new file mode 100644 index 0000000..6e3b9c1 --- /dev/null +++ b/pi-chrome/test-suite/challenges/10-user-activation.html @@ -0,0 +1,40 @@ + + +10 user activation gates + + + +
+

Goal: click the button. Handler then tries gated APIs (clipboard.writeText, fullscreen). + Success requires navigator.userActivation.isActive and at least one gated API to succeed.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/11-honeypot.html b/pi-chrome/test-suite/challenges/11-honeypot.html new file mode 100644 index 0000000..26a6240 --- /dev/null +++ b/pi-chrome/test-suite/challenges/11-honeypot.html @@ -0,0 +1,36 @@ + + +11 honeypot fields + + + +
+

Goal: fill only the visible "Name" field with alex and submit. + Three honeypots (a, b, c) are present that a human cannot see/tab to. Filling any honeypot fails.

+
+ + + + + + + + +
+
+ + diff --git a/pi-chrome/test-suite/challenges/12-fingerprint.html b/pi-chrome/test-suite/challenges/12-fingerprint.html new file mode 100644 index 0000000..5acf2ce --- /dev/null +++ b/pi-chrome/test-suite/challenges/12-fingerprint.html @@ -0,0 +1,59 @@ + + +12 fingerprint consistency + + + +
+

Auto-check. Looks for cross-API consistency mismatches that betray instrumented Chrome.

+

+
+ + diff --git a/pi-chrome/test-suite/challenges/13-focus-order.html b/pi-chrome/test-suite/challenges/13-focus-order.html new file mode 100644 index 0000000..90cbcaa --- /dev/null +++ b/pi-chrome/test-suite/challenges/13-focus-order.html @@ -0,0 +1,31 @@ + + +13 focus order + + + +
+

Goal: focus the input by clicking it (not .focus()), then type x. + Pointerdown must precede focus, and :focus-visible must be false for pointer focus.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/14-wheel-scroll.html b/pi-chrome/test-suite/challenges/14-wheel-scroll.html new file mode 100644 index 0000000..4f03f24 --- /dev/null +++ b/pi-chrome/test-suite/challenges/14-wheel-scroll.html @@ -0,0 +1,28 @@ + + +14 wheel scroll + + + +
+

Goal: scroll the box to the bottom. Page rejects raw scrollTop assignment without + wheel events.

+
+
scroll me ⬇

...lots of content...
+
+
+ + diff --git a/pi-chrome/test-suite/challenges/15-drag-drop-datatransfer.html b/pi-chrome/test-suite/challenges/15-drag-drop-datatransfer.html new file mode 100644 index 0000000..e0b6d12 --- /dev/null +++ b/pi-chrome/test-suite/challenges/15-drag-drop-datatransfer.html @@ -0,0 +1,73 @@ + + +15 drag-drop DataTransfer + + + +
+

Goal: drag the red box into the drop zone. Page asserts a full HTML5 + drag sequence with a populated DataTransfer — synthetic pointer drags + dispatched without dragstart/drop + DataTransfer payload fail.

+
+
+ DRAG +
+
+ DROP HERE +
+
+
+ + diff --git a/pi-chrome/test-suite/challenges/16-contenteditable-selection.html b/pi-chrome/test-suite/challenges/16-contenteditable-selection.html new file mode 100644 index 0000000..fea2adc --- /dev/null +++ b/pi-chrome/test-suite/challenges/16-contenteditable-selection.html @@ -0,0 +1,54 @@ + + +16 contenteditable selection + + + +
+

Goal: focus the editable region and type hello. Page asserts that + window.getSelection() reflects per-keystroke caret movement + (rangeCount===1, collapsed caret, anchor inside the editor, + offset advances 1 per char) and that selectionchange fires.

+
+
+ + diff --git a/pi-chrome/test-suite/challenges/17-paste-clipboard.html b/pi-chrome/test-suite/challenges/17-paste-clipboard.html new file mode 100644 index 0000000..c19e291 --- /dev/null +++ b/pi-chrome/test-suite/challenges/17-paste-clipboard.html @@ -0,0 +1,48 @@ + + +17 paste clipboard + + + +
+

Goal: paste the string pi-chrome into the input via a real OS paste + (Cmd/Ctrl+V firing a trusted paste event with populated + clipboardData). Programmatic value= or setRangeText + fail. The input event's inputType must be + insertFromPaste.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/18-native-select.html b/pi-chrome/test-suite/challenges/18-native-select.html new file mode 100644 index 0000000..7ca7002 --- /dev/null +++ b/pi-chrome/test-suite/challenges/18-native-select.html @@ -0,0 +1,56 @@ + + +18 native select option + + + +
+

Goal: select the option banana from the native <select>. + Page asserts a real picker interaction: mousedown/pointerdown + on the <select> (opening the OS picker), then a change event with + isTrusted=true. Programmatically setting .value or clicking the + option element directly fails (clicking <option> does not change selection in + real browsers).

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/19-hover-dwell.html b/pi-chrome/test-suite/challenges/19-hover-dwell.html new file mode 100644 index 0000000..21ce80a --- /dev/null +++ b/pi-chrome/test-suite/challenges/19-hover-dwell.html @@ -0,0 +1,50 @@ + + +19 hover dwell + + + +
+

Goal: hover the trigger and wait until the Confirm button reveals (≥600ms dwell + with continuous pointermove activity inside the trigger), then click Confirm. + Instant hover-then-click without dwell, or hover with no intermediate pointermove, + looks robotic and fails.

+
+ hover me +
+ +
+ + diff --git a/pi-chrome/test-suite/challenges/20-react-value-tracker.html b/pi-chrome/test-suite/challenges/20-react-value-tracker.html new file mode 100644 index 0000000..fb217d1 --- /dev/null +++ b/pi-chrome/test-suite/challenges/20-react-value-tracker.html @@ -0,0 +1,78 @@ + + +20 React _valueTracker + + + +
+

Goal: set the input to react-ok in a way that React would accept. + The page mimics React's _valueTracker: it stamps the input with a + hidden tracker holding the last-seen value. An input event is only + considered "framework-real" if the tracker's recorded value differs from the + current input.value at the moment the event fires + (i.e. somebody used the native HTMLInputElement.value setter and + dispatched input — what React internals require).

+

Setting el.value = ... directly and dispatching a synthetic input + fails because the native setter wasn't used. Calling setRangeText without + dispatching input also fails.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/21-keyboard-modifiers.html b/pi-chrome/test-suite/challenges/21-keyboard-modifiers.html new file mode 100644 index 0000000..870bef6 --- /dev/null +++ b/pi-chrome/test-suite/challenges/21-keyboard-modifiers.html @@ -0,0 +1,65 @@ + + +21 keyboard modifiers + + + +
+

Goal: focus the input and type a capital A via Shift+a + (not Caps Lock, not pasted, not assigned). Page asserts a real Shift chord: + keydown for Shift arrives before keydown for + A; the A event has shiftKey=true, + key==="A", code==="KeyA", getModifierState("Shift")===true; + and keyup for Shift arrives after keyup for A.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/22-touch-events.html b/pi-chrome/test-suite/challenges/22-touch-events.html new file mode 100644 index 0000000..042ddf4 --- /dev/null +++ b/pi-chrome/test-suite/challenges/22-touch-events.html @@ -0,0 +1,66 @@ + + +22 touch events + + + +
+

Goal: tap the green pad on a touch-capable device (or via DevTools touch + emulation). Page asserts a real touchstart/touchend sequence: + touches / changedTouches are + TouchList objects containing Touch instances with + identifier, clientX/Y, radiusX/Y, and + force in [0,1]. Synthetic PointerEvent with + pointerType="touch" alone does not satisfy this — touch events must + fire too.

+

Note for caller: requires Chrome touch emulation on (DevTools → Sensors → + "Touch: Force enabled") or a real touchscreen. This is intentional: pi-chrome's + synthetic pointerType:'touch' does NOT also dispatch TouchEvents.

+
+ TAP +
+
+ + diff --git a/pi-chrome/test-suite/challenges/23-stack-trace-fingerprint.html b/pi-chrome/test-suite/challenges/23-stack-trace-fingerprint.html new file mode 100644 index 0000000..902f95a --- /dev/null +++ b/pi-chrome/test-suite/challenges/23-stack-trace-fingerprint.html @@ -0,0 +1,76 @@ + + +23 stack-trace fingerprint + + + +
+

Goal: click the button. The page samples call stacks from inside several + instrumented globals (Function.prototype.toString, + document.querySelector, Element.prototype.click) and inspects + the stack of this script's own handler. It fails if it sees telltales of + evaluator-injected frames (e.g. at <anonymous> as the only frame, the + bridge's new Function wrapper, callFunctionOn, + executeScript, or extension URLs).

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/24-viewport-edge-clicks.html b/pi-chrome/test-suite/challenges/24-viewport-edge-clicks.html new file mode 100644 index 0000000..b1ef598 --- /dev/null +++ b/pi-chrome/test-suite/challenges/24-viewport-edge-clicks.html @@ -0,0 +1,51 @@ + + +24 viewport edge clicks + + + +
+

Goal: click the button. Page rejects any pointer event whose + clientX/clientY falls outside the viewport + (x < 0, x > innerWidth, etc.) or lands on a coordinate + that's not actually inside the button's getBoundingClientRect(). + Real pointing devices can't dispatch clicks at negative coordinates; + bridges that pass uncoerced selector-center coords (or coordinates from a + stale layout) trip this.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/25-pointer-continuity.html b/pi-chrome/test-suite/challenges/25-pointer-continuity.html new file mode 100644 index 0000000..c441045 --- /dev/null +++ b/pi-chrome/test-suite/challenges/25-pointer-continuity.html @@ -0,0 +1,62 @@ + + +25 pointer continuity + + + +
+

Goal: click A, then click B. Page requires pointer continuity: + between the two clicks there must be intermediate pointermove events + whose path roughly connects the two click points (no teleport). Two clicks + >100px apart with zero moves between is a synthetic-bridge tell.

+
+ +
+ +
+
+ + diff --git a/pi-chrome/test-suite/challenges/26-mousemove-rate.html b/pi-chrome/test-suite/challenges/26-mousemove-rate.html new file mode 100644 index 0000000..6b3722f --- /dev/null +++ b/pi-chrome/test-suite/challenges/26-mousemove-rate.html @@ -0,0 +1,57 @@ + + +26 mousemove rate + + + +
+

Goal: sweep the mouse across the box for ~1s, then click the green button. + Page measures the inter-event timestamp distribution of mousemove + events. Real pointing devices fire at ~60–250 Hz (median Δt 4–18ms). Bridges + that flood synthetic moves on a tight for-loop or + setTimeout(...,1) produce a degenerate distribution + (most Δt < 2ms, or all Δt identical, or median > 50ms with no jitter).

+
+ sweep me +
+ +
+ + diff --git a/pi-chrome/test-suite/challenges/27-scroll-momentum.html b/pi-chrome/test-suite/challenges/27-scroll-momentum.html new file mode 100644 index 0000000..981ec7b --- /dev/null +++ b/pi-chrome/test-suite/challenges/27-scroll-momentum.html @@ -0,0 +1,66 @@ + + +27 scroll momentum tail + + + +
+

Goal: scroll the box to its bottom using a real wheel/trackpad gesture. + Page records the deltaY sequence and requires a momentum-like tail: + a peak |deltaY|, then a decreasing decay over several events + (≥4 wheel events whose |deltaY| is strictly less than the previous + by >5%). Constant or single-shot wheel events (one big delta then nothing) + fail.

+
+
scroll me ⬇ … continues …
+
+
+ + diff --git a/pi-chrome/test-suite/challenges/28-intersection-visibility.html b/pi-chrome/test-suite/challenges/28-intersection-visibility.html new file mode 100644 index 0000000..1c90e33 --- /dev/null +++ b/pi-chrome/test-suite/challenges/28-intersection-visibility.html @@ -0,0 +1,72 @@ + + +28 IntersectionObserver visibility + + + +
+

Goal: scroll until the hidden TARGET button comes into view, then click it. + Page uses an IntersectionObserver to track when TARGET crosses the + viewport. Real scrolling produces a sequence of intersection updates with + gradually increasing intersectionRatio (0 → 1) across animation + frames. Programmatic teleports (scrollIntoView({behavior:"instant"}), + direct scrollTop=) produce exactly one observer callback with + ratio≈1 and no rAF samples in between — fail.

+
+
keep scrolling …
+ +
+
+ + diff --git a/pi-chrome/test-suite/challenges/29-shadow-dom-controls.html b/pi-chrome/test-suite/challenges/29-shadow-dom-controls.html new file mode 100644 index 0000000..bb2355c --- /dev/null +++ b/pi-chrome/test-suite/challenges/29-shadow-dom-controls.html @@ -0,0 +1,44 @@ + + +29 shadow DOM controls + + + +
+

Goal: discover controls inside an open Shadow DOM, click the button, then type shadow-ok into the shadow input.

+
+
+ + diff --git a/pi-chrome/test-suite/challenges/30-iframe-targeting.html b/pi-chrome/test-suite/challenges/30-iframe-targeting.html new file mode 100644 index 0000000..b77eac2 --- /dev/null +++ b/pi-chrome/test-suite/challenges/30-iframe-targeting.html @@ -0,0 +1,44 @@ + + +30 iframe targeting + + + +
+

Goal: interact with controls inside a same-origin iframe. Type frame-ok then click Submit inside the frame.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/31-file-upload.html b/pi-chrome/test-suite/challenges/31-file-upload.html new file mode 100644 index 0000000..0fddc60 --- /dev/null +++ b/pi-chrome/test-suite/challenges/31-file-upload.html @@ -0,0 +1,30 @@ + + +31 file upload + + + +
+

Goal: attach test-suite/fixtures/pi-chrome-upload.txt. Page reads name and contents from the File object.

+ +

+
+ + diff --git a/pi-chrome/test-suite/challenges/32-keyboard-tab-navigation.html b/pi-chrome/test-suite/challenges/32-keyboard-tab-navigation.html new file mode 100644 index 0000000..16c7966 --- /dev/null +++ b/pi-chrome/test-suite/challenges/32-keyboard-tab-navigation.html @@ -0,0 +1,61 @@ + + +32 keyboard tab navigation + + + +
+

Goal: use keyboard only. Click Start, then Tab through fields, type one, two, three, Tab to Submit, press Enter.

+ +
+ + + + +
+

+
+ + diff --git a/pi-chrome/test-suite/challenges/33-network-console-capture.html b/pi-chrome/test-suite/challenges/33-network-console-capture.html new file mode 100644 index 0000000..812345b --- /dev/null +++ b/pi-chrome/test-suite/challenges/33-network-console-capture.html @@ -0,0 +1,33 @@ + + +33 network and console capture + + + +
+

Goal: click button. Page emits console messages and performs a fetch. Benchmark harness should verify chrome_list_console_messages and chrome_list_network_requests captured them.

+ +

+
+ + diff --git a/pi-chrome/test-suite/challenges/34-dialog-handling.html b/pi-chrome/test-suite/challenges/34-dialog-handling.html new file mode 100644 index 0000000..25845a7 --- /dev/null +++ b/pi-chrome/test-suite/challenges/34-dialog-handling.html @@ -0,0 +1,28 @@ + + +34 dialog handling + + + +
+

Goal: handle browser dialogs. Click Prompt, enter pi-chrome, accept it; then click Confirm and accept it. Browser automation must expose/dismiss native dialogs.

+ + + +

+
+ + diff --git a/pi-chrome/test-suite/challenges/35-target-blank-popup.html b/pi-chrome/test-suite/challenges/35-target-blank-popup.html new file mode 100644 index 0000000..0813625 --- /dev/null +++ b/pi-chrome/test-suite/challenges/35-target-blank-popup.html @@ -0,0 +1,35 @@ + + +35 target blank popup + + + +
+

Goal: click target=_blank link, detect new tab, switch to it, and verify child page reports PASS.

+
Open child tab +

+
+ + diff --git a/pi-chrome/test-suite/challenges/36-modal-focus-trap.html b/pi-chrome/test-suite/challenges/36-modal-focus-trap.html new file mode 100644 index 0000000..811207c --- /dev/null +++ b/pi-chrome/test-suite/challenges/36-modal-focus-trap.html @@ -0,0 +1,49 @@ + + +36 modal focus trap + + + +
+

Goal: open modal, use Tab to cycle focus inside modal only, then Escape closes it and focus returns to opener.

+ + +
+ + diff --git a/pi-chrome/test-suite/challenges/37-autocomplete-combobox.html b/pi-chrome/test-suite/challenges/37-autocomplete-combobox.html new file mode 100644 index 0000000..91f35c2 --- /dev/null +++ b/pi-chrome/test-suite/challenges/37-autocomplete-combobox.html @@ -0,0 +1,42 @@ + + +37 autocomplete combobox + + + +
+

Goal: type as, use ArrowDown then Enter to choose Aster Lamp from ARIA combobox.

+ + +
+ + diff --git a/pi-chrome/test-suite/challenges/38-spa-route-change.html b/pi-chrome/test-suite/challenges/38-spa-route-change.html new file mode 100644 index 0000000..533c38d --- /dev/null +++ b/pi-chrome/test-suite/challenges/38-spa-route-change.html @@ -0,0 +1,40 @@ + + +38 SPA route change + + + +
+

Goal: navigate within SPA using links/buttons. URL must change via history.pushState without full reload. Then go Back to Settings, forward to Billing, and Save.

+ +
+
+ + diff --git a/pi-chrome/test-suite/challenges/39-strict-csp-fallback.html b/pi-chrome/test-suite/challenges/39-strict-csp-fallback.html new file mode 100644 index 0000000..93bd9cf --- /dev/null +++ b/pi-chrome/test-suite/challenges/39-strict-csp-fallback.html @@ -0,0 +1,14 @@ + + + +39 strict CSP fallback + + + +
+

Goal: page blocks unsafe-eval. Agent should fall back to screenshot/coordinate observation and click the target without relying on snapshot/evaluate.

+

The green target is fixed at known viewport coordinates. Use a screenshot, then click inside it.

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/39-strict-csp-fallback.js b/pi-chrome/test-suite/challenges/39-strict-csp-fallback.js new file mode 100644 index 0000000..4f2d91e --- /dev/null +++ b/pi-chrome/test-suite/challenges/39-strict-csp-fallback.js @@ -0,0 +1,27 @@ +Challenge.init({ id: "strict-csp-fallback", instructions: "use screenshot/coordinates; click CSP TARGET" }); + +const btn = document.getElementById("cspTarget"); +btn.style.cssText = [ + "position:fixed", + "left:220px", + "top:220px", + "width:180px", + "height:72px", + "font:700 16px system-ui", + "background:#1f7a1f", + "color:white", + "border:0", + "border-radius:10px", + "box-shadow:0 0 0 4px rgba(31,122,31,.25)" +].join(";"); + +document.getElementById("cspTarget").addEventListener("click", (e) => { + const r = btn.getBoundingClientRect(); + const bad = []; + if (!e.isTrusted) bad.push("click isTrusted=false"); + if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) { + bad.push(`click coordinates ${e.clientX},${e.clientY} outside target rect ${Math.round(r.left)},${Math.round(r.top)},${Math.round(r.right)},${Math.round(r.bottom)}`); + } + if (bad.length) Challenge.fail(...bad); + else Challenge.pass("strict CSP page completed via trusted viewport click"); +}); diff --git a/pi-chrome/test-suite/challenges/40-dynamic-wait-readiness.html b/pi-chrome/test-suite/challenges/40-dynamic-wait-readiness.html new file mode 100644 index 0000000..adf006d --- /dev/null +++ b/pi-chrome/test-suite/challenges/40-dynamic-wait-readiness.html @@ -0,0 +1,41 @@ + + +40 dynamic wait readiness + + + +
+

Goal: wait until an async control exists and is enabled, then click it. Direct early clicks or fixed sleeps should not be required.

+
Loading async action…
+
+
+ + diff --git a/pi-chrome/test-suite/challenges/41-tab-lifecycle.html b/pi-chrome/test-suite/challenges/41-tab-lifecycle.html new file mode 100644 index 0000000..2ba002d --- /dev/null +++ b/pi-chrome/test-suite/challenges/41-tab-lifecycle.html @@ -0,0 +1,44 @@ + + +41 tab lifecycle + + + +
+

Goal: create a new tab, inspect it, close it, return to parent, then verify lifecycle state.

+

+ +
+ + diff --git a/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.html b/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.html new file mode 100644 index 0000000..745c90e --- /dev/null +++ b/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.html @@ -0,0 +1,16 @@ + + + +42 strict CSP evaluate/snapshot + + + +
+

Goal: this page ships a strict CSP (script-src 'self', no unsafe-eval), which blocks eval/new Function. chrome_evaluate and chrome_snapshot must still work because they run through CDP, which is not subject to page CSP.

+

A secret token is exposed only at window.__cspToken — it is never written into the DOM. Use chrome_evaluate to read it, type it into the field (snapshot/uid to find the field), then click Verify.

+ + + +
+ + diff --git a/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.js b/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.js new file mode 100644 index 0000000..57351c0 --- /dev/null +++ b/pi-chrome/test-suite/challenges/42-strict-csp-evaluate.js @@ -0,0 +1,21 @@ +Challenge.init({ + id: "strict-csp-evaluate", + instructions: "under strict CSP: read window.__cspToken via chrome_evaluate, type it into the field, click Verify", +}); + +// Secret available only via JS evaluation. It is intentionally NOT rendered into the DOM and +// is defined non-enumerable, so the only way to obtain it is to evaluate window.__cspToken in +// the page (which proves chrome_evaluate works despite script-src 'self' blocking eval). +const token = "csp-" + Math.random().toString(36).slice(2, 10); +Object.defineProperty(window, "__cspToken", { value: token, enumerable: false, configurable: false, writable: false }); + +document.getElementById("verify").addEventListener("click", (e) => { + const bad = []; + if (!e.isTrusted) bad.push("verify click isTrusted=false (use trusted/CDP input)"); + const val = (document.getElementById("tokenInput").value || "").trim(); + if (val !== token) { + bad.push(`token mismatch: got "${val}" expected "${token}" — chrome_evaluate must read window.__cspToken under strict CSP`); + } + if (bad.length) Challenge.fail(...bad); + else Challenge.pass("strict CSP: chrome_evaluate read the hidden token via CDP and trusted input submitted it"); +}); diff --git a/pi-chrome/test-suite/fixtures/pi-chrome-upload.txt b/pi-chrome/test-suite/fixtures/pi-chrome-upload.txt new file mode 100644 index 0000000..b1901d8 --- /dev/null +++ b/pi-chrome/test-suite/fixtures/pi-chrome-upload.txt @@ -0,0 +1 @@ +pi-chrome upload fixture diff --git a/pi-chrome/test-suite/fixtures/sites/mini-shop/cheats.js b/pi-chrome/test-suite/fixtures/sites/mini-shop/cheats.js new file mode 100644 index 0000000..7407ee4 --- /dev/null +++ b/pi-chrome/test-suite/fixtures/sites/mini-shop/cheats.js @@ -0,0 +1,12 @@ +window.MiniShopCheats = { + "mini-shop-red-second-cheapest": [ + { tool: "chrome_click", params: { selector: "button[aria-label=\"add Ruby Notebook\"]" } }, + { tool: "chrome_click", params: { selector: "#checkout" } } + ], + run: { + "mini-shop-red-second-cheapest": async () => { + document.querySelector('button[aria-label="add Ruby Notebook"]').click(); + document.querySelector('#checkout').click(); + } + } +}; diff --git a/pi-chrome/test-suite/fixtures/sites/mini-shop/grader.js b/pi-chrome/test-suite/fixtures/sites/mini-shop/grader.js new file mode 100644 index 0000000..74605f4 --- /dev/null +++ b/pi-chrome/test-suite/fixtures/sites/mini-shop/grader.js @@ -0,0 +1,29 @@ +window.MiniShopGrader = { + validate(task, state, products, run, seed = 0) { + let bad = []; + const rubric = {}; + if (task === 'mini-shop-red-second-cheapest') { + const reds = products.filter(p => p.color === 'red').sort((a,b)=>a.price-b.price); + const target = reds[1]; + rubric.addedTarget = state.cart.includes(target.id); + rubric.noWrongItems = state.cart.every(id => id === target.id); + rubric.checkedOut = /^MS-\d{4}$/.test(state.orderId || ''); + if (!rubric.addedTarget) bad.push(`cart missing second-cheapest red item ${target.name}`); + const wrong = state.cart.filter(id => id !== target.id); + if (wrong.length) bad.push(`cart contains wrong product(s): ${wrong.join(',')}`); + if (!rubric.checkedOut) bad.push('missing checkout order id'); + } else bad.push(`unknown task ${task}`); + const passed = bad.length === 0; + return { reward: passed ? 1 : 0, done: passed, message: passed ? 'all deterministic mini-shop checks passed' : bad.join('; '), info: { task, run, seed, rubric, bad, state } }; + }, + grade(task, state, products, run, seed = 0) { + const validation = this.validate(task, state, products, run, seed); + const verdict = validation.done ? 'PASS' : 'PENDING'; + window.__taskId = task; + window.__taskVerdict = verdict; + window.__taskReason = validation.done ? [validation.message] : validation.info.bad; + window.__taskValidation = validation; + try { localStorage.setItem(`pi-chrome-task:${task}:${run}`, JSON.stringify({ id: task, run, seed, verdict, reason: window.__taskReason, validation, state, ts: Date.now() })); } catch {} + return validation; + } +}; diff --git a/pi-chrome/test-suite/fixtures/sites/mini-shop/index.html b/pi-chrome/test-suite/fixtures/sites/mini-shop/index.html new file mode 100644 index 0000000..0fd63ff --- /dev/null +++ b/pi-chrome/test-suite/fixtures/sites/mini-shop/index.html @@ -0,0 +1,55 @@ + + +MiniShop hermetic site + + + +
+

MiniShop

+
Cart 0 items
+

+
+
+ + + + diff --git a/pi-chrome/test-suite/fixtures/sites/mini-shop/tasks.json b/pi-chrome/test-suite/fixtures/sites/mini-shop/tasks.json new file mode 100644 index 0000000..6812baf --- /dev/null +++ b/pi-chrome/test-suite/fixtures/sites/mini-shop/tasks.json @@ -0,0 +1,9 @@ +[ + { + "id": "mini-shop-red-second-cheapest", + "difficulty": "L2", + "intent": "Add the second-cheapest red product to cart, checkout, and leave the generated order id visible.", + "gradeSource": "harness", + "grader": "MiniShopGrader.grade(task, state, products, run)" + } +] diff --git a/pi-chrome/test-suite/index.html b/pi-chrome/test-suite/index.html new file mode 100644 index 0000000..ac95c9d --- /dev/null +++ b/pi-chrome/test-suite/index.html @@ -0,0 +1,193 @@ + + +pi-chrome browser-control benchmark + + +
+

pi-chrome browser-control benchmark

+

This benchmark measures how well Chrome-control tools let agents do real browser work: DOM discovery, trusted input, keyboard/focus, scroll, drag/drop, files, frames, clipboard, and observability.

+ +
+
+ + + + + +
+

Open a row, drive it with chrome_* tools, then return here. Verdicts are read from localStorage. Expected outcomes come from manifest.json.

+ +
+ +
+ + + + + + + +
idgatecategoryverdictexpectedbaselineriskgoal / notesopen
+ +

Long-horizon hermetic tasks

+

These WebArena/BrowserGym-inspired tasks use deterministic in-page graders and fresh $RUN_ID state. They test multi-step browser work beyond event fidelity.

+ + + +
iddifficultycategoryintentrequiresopen
+ +

Agent run loop

+
python3 -m http.server 8765
+# For each manifest entry:
+# 1. chrome_navigate http://127.0.0.1:8765/<file>
+# 2. chrome_snapshot before acting; prefer uid selectors.
+# 3. Execute recipe with selected mode, adapting descriptive frame/shadow selectors to tool uids.
+# 4. chrome_evaluate JSON.stringify({v:window.__verdict,r:window.__reason,e:window.__events?.slice(-20)})
+# 5. Compare verdict to manifest.expected[mode]; CONDITIONAL means inspect prerequisites/notes.
+
+ + diff --git a/pi-chrome/test-suite/manifest.json b/pi-chrome/test-suite/manifest.json new file mode 100644 index 0000000..0ebb313 --- /dev/null +++ b/pi-chrome/test-suite/manifest.json @@ -0,0 +1,1681 @@ +[ + { + "id": "is-trusted-click", + "file": "challenges/01-is-trusted-click.html", + "category": "trusted-input", + "goal": "Click handler receives browser-trusted pointer/click events.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "notes": [ + "Core debugger/CDP smoke test." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "trusted-input" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "is-trusted-keyboard", + "file": "challenges/02-is-trusted-keyboard.html", + "category": "trusted-input", + "goal": "Typing produces trusted keydown/input sequence.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#t", + "text": "hello", + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "trusted-input" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "webdriver-flag", + "file": "challenges/03-webdriver-flag.html", + "category": "fingerprint", + "goal": "Real profile has no WebDriver/headless tells.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_evaluate", + "params": { + "expression": "JSON.stringify({v:window.__verdict,r:window.__reason})" + } + } + ], + "notes": [ + "Environment-sensitive; classify unexpected failures as profile warnings first." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "fingerprint" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "mouse-entropy", + "file": "challenges/04-mouse-entropy.html", + "category": "pointer-humanization", + "goal": "Click is preceded by organic mousemove path.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "event-timing", + "file": "challenges/05-event-timing.html", + "category": "timing", + "goal": "Repeated clicks have plausible holds and gaps.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + } + ], + "flakeRisk": "medium", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "timing" + ], + "requires": {}, + "thresholds": {}, + "notes": [ + "Timing thresholds can be tuned via ?threshold.=value for slower CI or high-polling devices." + ], + "gate": "core" + }, + { + "id": "click-coordinates", + "file": "challenges/06-click-coordinates.html", + "category": "pointer-humanization", + "goal": "Clicks land inside target but not always exact center.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "pointer-properties", + "file": "challenges/07-pointer-properties.html", + "category": "pointer-humanization", + "goal": "Pointer events carry plausible pointerType, pressure, movement, id.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "keyboard-cadence", + "file": "challenges/08-keyboard-cadence.html", + "category": "keyboard", + "goal": "Typing emits per-character key events with non-uniform cadence.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#t", + "text": "secret" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "keyboard" + ], + "requires": {}, + "thresholds": {}, + "notes": [ + "Timing thresholds can be tuned via ?threshold.=value for slower CI or high-polling devices." + ], + "gate": "quality" + }, + { + "id": "composition-input", + "file": "challenges/09-composition-input.html", + "category": "keyboard", + "goal": "Plain ASCII typing has correct beforeinput/input order without fake composition.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#t", + "text": "abc" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "keyboard" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "user-activation", + "file": "challenges/10-user-activation.html", + "category": "activation-gates", + "goal": "User activation unlocks clipboard/fullscreen-style gated APIs.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "CONDITIONAL" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "verdictDelayMs": 800, + "prerequisites": [ + "secure origin", + "clipboard/fullscreen allowed by browser policy" + ], + "flakeRisk": "high", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "activation-gates" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "honeypot", + "file": "challenges/11-honeypot.html", + "category": "agent-safety", + "goal": "Agent fills only visible intended field, not hidden traps.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_fill", + "params": { + "selector": "#name", + "text": "alex" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "button[type=submit]" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "agent-safety" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "fingerprint", + "file": "challenges/12-fingerprint.html", + "category": "fingerprint", + "goal": "Browser fingerprint is plausible for normal Chrome profile.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "CONDITIONAL", + "manual": "CONDITIONAL" + }, + "recipe": [ + { + "tool": "chrome_evaluate", + "params": { + "expression": "JSON.stringify({v:window.__verdict,r:window.__reason})" + } + } + ], + "prerequisites": [ + "non-headless normal Chrome profile" + ], + "flakeRisk": "medium", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "fingerprint" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "focus-order", + "file": "challenges/13-focus-order.html", + "category": "focus-keyboard", + "goal": "Pointer-driven focus precedes input and does not set focus-visible keyboard affordance.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#t", + "text": "x", + "trusted": true + } + } + ], + "flakeRisk": "medium", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "focus-keyboard" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "wheel-scroll", + "file": "challenges/14-wheel-scroll.html", + "category": "scroll", + "goal": "Scrollable element moves via wheel events, not scrollTop teleport.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_scroll", + "params": { + "selector": "#box", + "deltaY": 1200 + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "scroll" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "drag-drop-datatransfer", + "file": "challenges/15-drag-drop-datatransfer.html", + "category": "drag-drop", + "goal": "HTML5 drag cycle delivers trusted events and populated DataTransfer.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_drag", + "params": { + "fromSelector": "#src", + "toSelector": "#dst", + "trusted": true + } + } + ], + "notes": [ + "May need CDP Input.dispatchDragEvent rather than mouse drag on some Chrome versions." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "drag-drop" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "contenteditable-selection", + "file": "challenges/16-contenteditable-selection.html", + "category": "editing", + "goal": "Contenteditable typing advances selection/caret like user typing.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#ed", + "text": "hello" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "editing" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "paste-clipboard", + "file": "challenges/17-paste-clipboard.html", + "category": "clipboard", + "goal": "Paste uses OS/browser clipboard path with trusted paste event and clipboardData.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#t", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "v", + "modifiers": { + "metaKey": true + }, + "trusted": true + } + } + ], + "prerequisites": [ + "OS clipboard contains pi-chrome", + "use ctrlKey instead of metaKey on Windows/Linux" + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "clipboard" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "native-select", + "file": "challenges/18-native-select.html", + "category": "native-controls", + "goal": "Native select changes via trusted picker/keyboard path, not .value assignment.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#s", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "ArrowDown", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "ArrowDown", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Enter", + "trusted": true + } + } + ], + "flakeRisk": "high", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "native-controls" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "hover-dwell", + "file": "challenges/19-hover-dwell.html", + "category": "hover", + "goal": "Hover reveal includes dwell, pointer activity, and delayed trusted confirm click.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_hover", + "params": { + "selector": "#trig", + "trusted": true + } + }, + { + "tool": "wait", + "params": { + "ms": 800 + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "hover" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "react-value-tracker", + "file": "challenges/20-react-value-tracker.html", + "category": "frameworks", + "goal": "Programmatic fill uses native value setter so React-style tracker sees change.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_fill", + "params": { + "selector": "#t", + "text": "react-ok" + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "frameworks" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "keyboard-modifiers", + "file": "challenges/21-keyboard-modifiers.html", + "category": "keyboard", + "goal": "Shift+a chord has correct ordering, code/key/modifier state, trusted events.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_key", + "params": { + "key": "a", + "modifiers": { + "shiftKey": true + }, + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "keyboard" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "touch-events", + "file": "challenges/22-touch-events.html", + "category": "touch", + "goal": "Touch tap produces real TouchEvent/Touch surface with radius/force.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "CONDITIONAL" + }, + "recipe": [ + { + "tool": "chrome_tap", + "params": { + "selector": "#pad" + } + } + ], + "prerequisites": [ + "touch events enabled/supported" + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "touch" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "stack-trace-fingerprint", + "file": "challenges/23-stack-trace-fingerprint.html", + "category": "fingerprint", + "goal": "Click handler stack does not expose extension/eval automation frames.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "notes": [ + "Adversarial probe; selector resolution itself can be detected by monkey-patched DOM APIs.", + "Should fail only on concrete automation URLs/Runtime frames, not generic anonymous stack formatting." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "fingerprint" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "viewport-edge-clicks", + "file": "challenges/24-viewport-edge-clicks.html", + "category": "pointer-humanization", + "goal": "Click coordinates are valid viewport coordinates inside target.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "pointer-continuity", + "file": "challenges/25-pointer-continuity.html", + "category": "pointer-humanization", + "goal": "Far-apart clicks are bridged by mid-span pointermove samples.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#a", + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#b", + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "mousemove-rate", + "file": "challenges/26-mousemove-rate.html", + "category": "pointer-humanization", + "goal": "Mouse sweep produces plausible move frequency and jitter before trusted Done click.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_hover", + "params": { + "selector": "#pad", + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go", + "trusted": true + } + } + ], + "notes": [ + "Needs a first-class pointer path/sweep tool for deterministic automation.", + "Timing thresholds can be tuned via ?threshold.=value for slower CI or high-polling devices." + ], + "flakeRisk": "high", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "pointer-humanization" + ], + "requires": {}, + "thresholds": {}, + "gate": "quality" + }, + { + "id": "scroll-momentum", + "file": "challenges/27-scroll-momentum.html", + "category": "scroll", + "goal": "Wheel gesture has trusted events with ramp/decay momentum tail.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "CONDITIONAL" + }, + "recipe": [ + { + "tool": "chrome_scroll", + "params": { + "selector": "#box", + "deltaY": 1600, + "trusted": true + } + } + ], + "notes": [ + "Physical detent mouse wheels may fail decay requirement; trackpads usually pass." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "scroll" + ], + "requires": {}, + "prerequisites": [ + "Trackpad-like momentum; physical detent mouse wheels may fail decay requirement even for manual users." + ], + "gate": "quality" + }, + { + "id": "intersection-visibility", + "file": "challenges/28-intersection-visibility.html", + "category": "scroll-visibility", + "goal": "Scrolling reveals target gradually with IO/rAF samples before trusted click.", + "expected": { + "synthetic": "FAIL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_scroll", + "params": { + "selector": "#scroller", + "deltaY": 1200, + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#target", + "trusted": true + } + } + ], + "flakeRisk": "medium", + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "scroll-visibility" + ], + "requires": {}, + "gate": "quality" + }, + { + "id": "shadow-dom-controls", + "file": "challenges/29-shadow-dom-controls.html", + "category": "dom-complexity", + "goal": "Agent discovers and interacts with controls inside open Shadow DOM.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#host >>> button" + } + }, + { + "tool": "chrome_type", + "params": { + "selector": "#host >>> input", + "text": "shadow-ok" + } + } + ], + "notes": [ + "Selector syntax is descriptive; tools may need uid from snapshot rather than CSS.", + "Current recipe uses descriptive >>> shadow selector syntax; runners must adapt to supported uids/selectors or use evaluate until bridge supports shadow piercing." + ], + "manualBaseline": "unverified", + "prerequisites": [ + "pi-chrome shadow-piercing selectors or shadow-root-aware snapshot uids" + ], + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "dom-complexity" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "iframe-targeting", + "file": "challenges/30-iframe-targeting.html", + "category": "frames", + "goal": "Agent targets interactive controls inside a same-origin iframe and reports verdict to parent.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "iframe#ifr >> #insideText", + "text": "frame-ok" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "iframe#ifr >> #insideButton" + } + } + ], + "notes": [ + "Selector syntax is descriptive; benchmark checks whether tool exposes frame-aware uids/selectors." + ], + "manualBaseline": "unverified", + "prerequisites": [ + "pi-chrome currently targets frameId 0 for many actions; iframe drilling needs frame-targeting support" + ], + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "frames" + ], + "requires": {}, + "gate": "conditional" + }, + { + "id": "file-upload", + "file": "challenges/31-file-upload.html", + "category": "files", + "goal": "Agent attaches a local file to an input and page reads expected name/content.", + "expected": { + "synthetic": "PASS", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_upload_file", + "params": { + "selector": "#file", + "paths": [ + "$PWD/test-suite/fixtures/pi-chrome-upload.txt" + ] + } + } + ], + "manualBaseline": "unverified", + "notes": [ + "Recipe paths may contain $PWD; external runners should expand shell-style placeholders before calling chrome_upload_file." + ], + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "files" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "keyboard-tab-navigation", + "file": "challenges/32-keyboard-tab-navigation.html", + "category": "focus-keyboard", + "goal": "Keyboard-only user flow tabs through fields and submits with Enter.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#start", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_type", + "params": { + "text": "one", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_type", + "params": { + "text": "two", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_type", + "params": { + "text": "three", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Enter", + "trusted": true + } + } + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "focus-keyboard" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "network-console-capture", + "file": "challenges/33-network-console-capture.html", + "category": "observability", + "goal": "Interaction emits console logs and fetch traffic for tool-level capture APIs.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_list_console_messages", + "params": { + "clear": true + } + }, + { + "tool": "chrome_list_network_requests", + "params": { + "clear": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#go" + } + }, + { + "tool": "chrome_list_console_messages", + "params": {} + }, + { + "tool": "chrome_list_network_requests", + "params": {} + } + ], + "notes": [ + "Page verdict only checks in-page fetch result; benchmark harness should also assert captured console/network entries.", + "First chrome_list_console_messages/chrome_list_network_requests call installs capture hooks; do not move clear/list calls after click." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "difficulty": "L1", + "tags": [ + "observability" + ], + "requires": {}, + "gate": "core" + }, + { + "id": "dialog-handling", + "file": "challenges/34-dialog-handling.html", + "category": "dialogs", + "difficulty": "L2", + "goal": "Agent detects and handles alert/confirm/prompt/beforeunload browser dialogs.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "CONDITIONAL", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#promptBtn", + "trusted": true + } + }, + { + "tool": "dialog", + "params": { + "type": "prompt", + "text": "pi-chrome", + "accept": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#confirmBtn", + "trusted": true + } + }, + { + "tool": "dialog", + "params": { + "type": "confirm", + "accept": true + } + } + ], + "requires": { + "dialogs": true + }, + "prerequisites": [ + "browser-dialog handling API or manual user intervention" + ], + "tags": [ + "dialogs", + "native-browser-ui" + ], + "notes": [ + "If dialog handling is unsupported, page may block after click; this is expected signal." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "gate": "conditional" + }, + { + "id": "target-blank-popup", + "file": "challenges/35-target-blank-popup.html", + "category": "popups", + "difficulty": "L1", + "goal": "Agent handles target=_blank new tab, discovers it via tab list, and inspects child tab verdict.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#open", + "trusted": true + } + }, + { + "tool": "chrome_tab", + "params": { + "action": "list" + } + }, + { + "tool": "chrome_snapshot", + "params": { + "titleIncludes": "target blank popup" + } + } + ], + "requires": { + "popups": true + }, + "tags": [ + "tabs", + "popup" + ], + "notes": [ + "Child tab writes PASS to shared localStorage; harness should verify tab count/title too." + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "gate": "core" + }, + { + "id": "modal-focus-trap", + "file": "challenges/36-modal-focus-trap.html", + "category": "focus-keyboard", + "difficulty": "L2", + "goal": "Keyboard focus stays trapped inside modal and Escape closes with focus restoration.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#open", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Tab", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Escape", + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "modal", + "focus", + "keyboard" + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "gate": "core" + }, + { + "id": "autocomplete-combobox", + "file": "challenges/37-autocomplete-combobox.html", + "category": "forms", + "difficulty": "L2", + "goal": "Agent uses ARIA combobox keyboard path: type filter, ArrowDown, Enter.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_type", + "params": { + "selector": "#combo", + "text": "as", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "ArrowDown", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "Enter", + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "aria", + "combobox", + "forms" + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "gate": "core" + }, + { + "id": "spa-route-change", + "file": "challenges/38-spa-route-change.html", + "category": "spa-routing", + "difficulty": "L1", + "goal": "Agent recognizes SPA pushState navigation and completes route-local action without full reload.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_click", + "params": { + "selector": "#settings", + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#billing", + "trusted": true + } + }, + { + "tool": "chrome_key", + "params": { + "key": "ArrowLeft", + "modifiers": { + "altKey": true + }, + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#billing", + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#save", + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "spa", + "history", + "navigation" + ], + "manualBaseline": "unverified", + "gradeSource": "page", + "notes": [ + "Requires a real browser Back action (e.g. Alt+Left) to exercise popstate before Save." + ], + "gate": "core" + }, + { + "id": "strict-csp-fallback", + "file": "challenges/39-strict-csp-fallback.html", + "category": "csp", + "difficulty": "L2", + "gate": "core", + "goal": "Operate a page with strict CSP that blocks unsafe-eval by falling back to screenshot/viewport-coordinate input.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_screenshot", + "params": { + "path": ".pi/chrome-screenshots/strict-csp-fallback.png" + } + }, + { + "tool": "chrome_click", + "params": { + "x": 310, + "y": 256, + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "csp", + "screenshot", + "coordinate-click", + "strict-csp" + ], + "notes": [ + "This challenge exercises the pure screenshot + viewport-coordinate path (no snapshot/evaluate needed). Note: as of the CDP CSP bypass, chrome_snapshot/chrome_evaluate DO work here even though script-src omits unsafe-eval (see challenge 42 strict-csp-evaluate). Read verdict from dashboard/localStorage after leaving the CSP page." + ], + "manualBaseline": "unverified", + "gradeSource": "page" + }, + { + "id": "dynamic-wait-readiness", + "file": "challenges/40-dynamic-wait-readiness.html", + "category": "lazy-loading", + "difficulty": "L1", + "gate": "core", + "goal": "Wait for an asynchronously mounted and enabled control before clicking it.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_wait_for", + "params": { + "kind": "selector", + "value": "#readyAction[data-ready=\"true\"]:not([disabled])", + "timeoutMs": 3000 + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#readyAction", + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "wait", + "async-dom", + "readiness" + ], + "notes": [ + "Covers explicit wait primitive and avoids fixed sleep as readiness strategy." + ], + "manualBaseline": "unverified", + "gradeSource": "page" + }, + { + "id": "tab-lifecycle", + "file": "challenges/41-tab-lifecycle.html", + "category": "popups", + "difficulty": "L2", + "gate": "core", + "goal": "Create a new tab, inspect it, close it, return to the parent tab, and verify tab lifecycle state.", + "expected": { + "synthetic": "CONDITIONAL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_tab", + "params": { + "action": "new", + "url": "http://127.0.0.1:8765/challenges/41-tab-lifecycle.html?child=1" + } + }, + { + "tool": "chrome_tab", + "params": { + "action": "list" + } + }, + { + "tool": "chrome_snapshot", + "params": { + "titleIncludes": "tab lifecycle" + } + }, + { + "tool": "chrome_tab", + "params": { + "action": "close", + "targetId": "" + } + }, + { + "tool": "chrome_tab", + "params": { + "action": "activate", + "targetId": "" + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#verify", + "trusted": true + } + } + ], + "requires": { + "popups": true + }, + "tags": [ + "tabs", + "new-tab", + "close-tab", + "activate-tab" + ], + "notes": [ + "Recipe contains dynamic tab IDs; runner must substitute ids from chrome_tab list. Complements target=_blank by testing explicit new/close/activate tools." + ], + "manualBaseline": "unverified", + "gradeSource": "page" + }, + { + "id": "strict-csp-evaluate", + "file": "challenges/42-strict-csp-evaluate.html", + "category": "csp", + "difficulty": "L2", + "gate": "core", + "goal": "On a page with strict CSP (script-src 'self', no unsafe-eval), use chrome_evaluate + chrome_snapshot via CDP to read a JS-only secret (window.__cspToken), then submit it with trusted input.", + "expected": { + "synthetic": "FAIL", + "trusted": "PASS", + "manual": "PASS" + }, + "recipe": [ + { + "tool": "chrome_evaluate", + "params": { + "expression": "window.__cspToken" + } + }, + { + "tool": "chrome_fill", + "params": { + "selector": "#tokenInput", + "value": "$RESULT_OF_chrome_evaluate", + "trusted": true + } + }, + { + "tool": "chrome_click", + "params": { + "selector": "#verify", + "trusted": true + } + } + ], + "requires": { + "cdp": true + }, + "tags": [ + "csp", + "evaluate", + "snapshot", + "strict-csp", + "cdp-bypass" + ], + "notes": [ + "Regression guard for the CDP CSP bypass: chrome_evaluate and chrome_snapshot must work even though script-src omits unsafe-eval (eval/new Function are blocked). The token is non-enumerable and never in the DOM, so it can only be obtained via chrome_evaluate. Runner must substitute the evaluate result into the fill value. synthetic FAIL is due to the trusted-click gate, not eval availability." + ], + "manualBaseline": "unverified", + "gradeSource": "page" + } +] diff --git a/pi-chrome/test-suite/manifest.schema.json b/pi-chrome/test-suite/manifest.schema.json new file mode 100644 index 0000000..fa63c58 --- /dev/null +++ b/pi-chrome/test-suite/manifest.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "pi-chrome benchmark manifest", + "description": "Manifest for browser-control benchmark pages. Recipes are canonical intent, not guaranteed raw tool JSON: runners may need to adapt descriptive selectors (frame/shadow) and expand path placeholders such as ${REPO_ROOT} before invoking tools.", + "type": "array", + "items": { + "type": "object", + "required": ["id", "file", "category", "goal", "expected", "recipe"], + "additionalProperties": true, + "properties": { + "id": { "type": "string" }, + "file": { "type": "string" }, + "category": { "$ref": "#/$defs/category" }, + "difficulty": { "enum": ["L1", "L2", "L3"] }, + "gate": { "description": "Ship-gate bucket. core blocks releases, conditional requires declared capability/environment, quality is adversarial/flaky signal and should not block general ship.", "enum": ["core", "conditional", "quality"] }, + "tags": { "type": "array", "items": { "type": "string" } }, + "goal": { "type": "string" }, + "gradeSource": { "enum": ["page", "harness", "both"], "default": "page" }, + "expected": { + "type": "object", + "description": "Expected verdict by execution mode. CONDITIONAL means prerequisites, browser policy, hardware, or missing tool primitives decide outcome.", + "required": ["synthetic", "trusted", "manual"], + "properties": { + "synthetic": { "$ref": "#/$defs/outcome" }, + "trusted": { "$ref": "#/$defs/outcome" }, + "manual": { "$ref": "#/$defs/outcome" } + } + }, + "manualBaseline": { "description": "Whether human baseline has been explicitly verified in this repo/environment.", "enum": ["unverified", "verified"] }, + "manualBaselineVerifiedAt": { "type": "string", "format": "date-time" }, + "recipe": { + "type": "array", + "description": "Ordered tool intent. Params may include descriptive selectors (e.g. frame/shadow notation) or path placeholders for runners to adapt.", + "items": { + "type": "object", + "required": ["tool", "params"], + "properties": { + "tool": { "type": "string" }, + "params": { "type": "object", "description": "Tool params. For upload recipes, paths are strings after runner expansion; ${REPO_ROOT} means repository root." } + } + } + }, + "requires": { + "type": "object", + "additionalProperties": true, + "properties": { + "cdp": { "type": "boolean" }, + "clipboard": { "type": "boolean" }, + "touch": { "type": "boolean" }, + "secureOrigin": { "type": "boolean" }, + "fileSystem": { "type": "boolean" }, + "dialogs": { "type": "boolean" }, + "downloads": { "type": "boolean" }, + "popups": { "type": "boolean" } + } + }, + "prerequisites": { "type": "array", "items": { "type": "string" } }, + "notes": { "type": "array", "items": { "type": "string" } }, + "thresholds": { "type": "object", "additionalProperties": true }, + "timeoutMs": { "type": "number" }, + "verdictDelayMs": { "type": "number" }, + "flakeRisk": { "enum": ["low", "medium", "high"] } + } + }, + "$defs": { + "outcome": { "enum": ["PASS", "FAIL", "SKIP", "WARN", "CONDITIONAL"] }, + "category": { + "enum": [ + "activation-gates", "agent-safety", "aria-snapshot", "canvas-svg", "clipboard", "contextmenu", "csp", "dialogs", "dom-complexity", "downloads", "drag-drop", "editing", "files", "fingerprint", "focus-keyboard", "forms", "frames", "frameworks", "hover", "keyboard", "lazy-loading", "native-controls", "observability", "pointer-humanization", "popups", "scroll", "scroll-visibility", "spa-routing", "task-completion", "timing", "touch", "trusted-input" + ] + } + } +} diff --git a/pi-chrome/test-suite/notes/browsergym-compat.md b/pi-chrome/test-suite/notes/browsergym-compat.md new file mode 100644 index 0000000..1eaea41 --- /dev/null +++ b/pi-chrome/test-suite/notes/browsergym-compat.md @@ -0,0 +1,70 @@ +# BrowserGym compatibility contract + +Pi-chrome benchmark tasks mirror BrowserGym without importing Gymnasium. + +## Task fields + +Each `task-manifest.json` entry maps to `AbstractBrowserTask` concepts: + +- `taskId` / `id` — stable namespaced id +- `seed` — deterministic state seed +- `viewport`, `slowMoMs`, `timeoutMs`, `locale`, `timezoneId` +- `goal` and `goalObject` — text and OpenAI-style message objects +- `setupUrl` — page URL that performs setup from `task`, `run`, `seed` +- `validateScript` — JS validate hook returning reward contract +- `cheatScript` — gold recipe used to sanity-check grader +- `maxSteps`, `humanBaseline`, `difficulty`, `tags` +- `actionSubsets`, `allowedActions` — BrowserGym HighLevelActionSet metadata + +## Reset / step return + +Runner should expose: + +```text +reset() -> (obs, info) +step(action) -> (obs, reward, terminated, truncated, info) +``` + +`terminated` means task done. `truncated` means max step or timeout. + +## Validate return + +In-page validators return: + +```js +{ reward, done, message, info } +``` + +`reward` is incremental. Current hermetic tasks use binary terminal reward. `info.rubric` carries per-check details. + +## Observation schema target + +Runner observations should include BrowserGym-like keys: + +- `chat_messages` +- `goal`, `goal_object` +- `open_pages_urls`, `open_pages_titles`, `active_page_index` +- `url`, `screenshot`, `dom_object`, `axtree_object` +- `extra_element_properties` +- `focused_element_bid` +- `last_action`, `last_action_error` +- `elapsed_time` + +## BID plan + +BrowserGym uses `bid` attributes as stable element handles. Pi-chrome currently returns snapshot `uid`. Compatibility path: + +1. During snapshot, assign every interactive element a deterministic `bid`. +2. Return both `uid` and `bid`. +3. Add `visibility`, `bbox`, `clickable`, `set_of_marks` metadata. +4. Maintain uid↔bid mapping so agents can use either addressing mode. + +Target attributes: + +- `bid` +- `browsergym_visibility_ratio` +- `browsergym_set_of_marks` + +## Action subsets + +See `../browsergym-action-space.json` for BrowserGym-compatible subsets and function signatures. diff --git a/pi-chrome/test-suite/notes/bypass-ideas.md b/pi-chrome/test-suite/notes/bypass-ideas.md new file mode 100644 index 0000000..7980aaa --- /dev/null +++ b/pi-chrome/test-suite/notes/bypass-ideas.md @@ -0,0 +1,79 @@ +# Historical bypass notes + +This file predates the current `trusted:true` / `chrome.debugger` path and is +kept as design history. For current benchmark expectations, recipes, and +capability notes, use `../manifest.json`. + +Older context: `pi-chrome` originally ran mostly through a content-script bridge. +That limited the solution space — most "real-event" tricks needed the +`chrome.debugger` API or `chrome.input.synthesizeMouseEvent`. + +## 01–02 isTrusted click / keyboard +**Hard.** `Event.isTrusted` is true only for events the browser dispatched +itself. Two viable paths: + +- **`chrome.debugger` + `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`.** + Adds `"debugger"` permission. Chrome shows a "X is debugging this browser" + banner, but events arrive as `isTrusted=true`. Best fix for the highest-value + cases; gate it behind a toggle the user opts into. +- **`chrome.input.ime.*`** — not applicable for general typing. + +Today's `dispatchEvent` path will always fail any `isTrusted` check. + +## 03 / 12 webdriver + fingerprint +Cheap wins: +- Stub `navigator.webdriver` via `chrome.scripting.executeScript({ world: "MAIN", injectImmediately: true, … })` that defines `navigator.webdriver` getter to `undefined`. Already half-supported through `initScript` on `chrome_navigate` — extend to a per-tab content script registered with `chrome.scripting.registerContentScripts({ runAt: "document_start", world: "MAIN" })`. +- Spoof languages/plugins/permissions only if the user explicitly enables it — by default the real Chrome profile already looks legitimate. The bigger risk is `chrome_evaluate` *itself* leaving traces (Function-constructor stack frames in errors). + +## 04 mouse entropy +Generate a humanised pointer path before any click: +- Bezier interpolation from current cursor pos (or a random off-target start) to the target point. +- 20–60 steps, easing curve, ±jitter on each step, varying `movementX/Y`. +- Implement in `clickPage` before the existing pointerdown sequence; reuse `pointerEventSequence` for each interpolated point. + +## 05 event timing +- Insert `await sleep(rand(40,140))` between `pointerdown` and `pointerup`. +- Insert `await sleep(rand(80,220))` between successive synthesised clicks when + the caller issues them in a loop. +- Vary by ±20%. + +## 06 click coordinates +- After resolving target rect in `clickPage`, pick `(cx ± rand(-rw*0.3,rw*0.3), cy ± rand(-rh*0.3,rh*0.3))` instead of dead center. Already returns the chosen point — keep that. + +## 07 pointer properties +- Set `pressure: 0.5` for `pointerdown` (currently inits don't set pressure; browser default for mouse pointerdown is 0.5). +- Set `pointerId: 1` for mouse (already done) and 2+ for touch. +- Carry `movementX/Y` from the previous interpolated step (needs path state). + +## 08 / 09 keyboard cadence + framework invariants +- Per character: dispatch `keydown` (cancelable, fills `key` + `code` + `keyCode`), then `keypress` for printables, then `beforeinput` `{inputType:"insertText", data:ch}`, then mutate the input's value to `cur+ch` via the native setter, then `input` `{inputType:"insertText", data:ch}`, then `keyup`. Sleep 40–120 ms between chars. +- Today the bridge fills the entire value in one go and emits one `beforeinput`/`input`. Replace with the per-char loop above. +- Make sure not to dispatch `compositionstart`/`compositionend` for plain ASCII. + +## 10 user activation +**Impossible with synthetic events.** `navigator.userActivation.isActive` only +flips on browser-trusted events. Same fix as 01/02 — `chrome.debugger` path. +Document the limitation; offer a `useDebuggerForActivationGates` opt-in. + +## 11 honeypot +Pure agent-side concern. Before filling/clicking by selector, the bridge could: +- Reject targets with `display:none`, `visibility:hidden`, `aria-hidden=true`, + or off-screen position (`getBoundingClientRect()` outside viewport bounds and + not scrolled-into-view), unless the caller passes `force: true`. +- That logic largely exists (`elementVisible` / `occludedBy` are returned) — + promote it from informational to a guard. + +## 13 focus order +- When clicking a focusable element, dispatch `pointerdown` first; only after + `mousedown` defaults run let focus settle naturally (browser will move focus + on the synthetic `mousedown` only if `isTrusted` is true — alas, no). Manual + workaround: dispatch the pointer/mouse sequence, then explicitly call + `target.focus({ preventScroll: true })`, then dispatch `focus` with + `{ relatedTarget: previouslyFocused }`. Set `:focus-visible` heuristic via + `target.blur(); target.focus({ focusVisible: false })` (Chrome 124+). + +## 14 wheel scroll +- For scroll operations, dispatch sequential `wheel` events with `deltaY` + chunks (e.g., 60–120 per tick), and let the browser scroll, instead of + setting `scrollTop` directly. Provide `chrome_scroll({ uid, dy, dx })` as a + first-class tool. diff --git a/pi-chrome/test-suite/notes/profiles.md b/pi-chrome/test-suite/notes/profiles.md new file mode 100644 index 0000000..8f55797 --- /dev/null +++ b/pi-chrome/test-suite/notes/profiles.md @@ -0,0 +1,22 @@ +# Calibration profiles + +Benchmark results depend on Chrome profile, OS, input device, and extension mode. +Record these with every run: + +- OS and version +- Chrome version +- pi-chrome package version +- companion extension version +- trusted mode: off / auto / on +- input hardware: trackpad vs detent mouse, touch support +- permissions: clipboard, fullscreen, downloads +- viewport size and deviceScaleFactor +- loaded privacy/security extensions + +Known environment-sensitive areas: + +- WebGL fingerprint can warn/fail in VMs, remote desktops, GPU-disabled Chrome. +- Scroll momentum expects trackpad-like decay; detent mouse wheels may fail manually. +- Touch tests require touch events enabled/supported. +- Dialog/download/file-picker tests require native browser UI handling or manual intervention. +- Stack traces vary across Chrome versions; tests should only fail concrete automation URLs/Runtime frames. diff --git a/pi-chrome/test-suite/notes/runner-spec.md b/pi-chrome/test-suite/notes/runner-spec.md new file mode 100644 index 0000000..24c97ab --- /dev/null +++ b/pi-chrome/test-suite/notes/runner-spec.md @@ -0,0 +1,29 @@ +# Recipe runner spec + +Future Node/Pi runner should: + +1. Read `manifest.json`. +2. Probe environment capabilities (`trusted`, `clipboard`, `touch`, `dialogs`, `downloads`, `fileSystem`, `strictCspFallback`). +3. For each entry: + - read `gate` (`core`, `conditional`, `quality`) and score in that bucket + - skip if `requires` is unsatisfied and expected is `CONDITIONAL` + - navigate to challenge URL + - execute `recipe` in order + - wait `verdictDelayMs || 500` + - evaluate `JSON.stringify({v:window.__verdict,r:window.__reason,d:window.Challenge?.state?.details,e:window.__events?.slice(-20)})`, except strict-CSP pages where eval is expected to fail; read persisted verdict from dashboard/localStorage after leaving the CSP page + - compare to `expected[mode]` +4. Output: + - Markdown summary + - JSON details + - JUnit XML for CI + +Runner must adapt recipe intent: + +- expand `${REPO_ROOT}` path placeholders +- adapt unsupported shadow/iframe selector notation to snapshot uids or evaluate fallback +- preserve hook install ordering for console/network capture tests +- substitute dynamic tab ids from `chrome_tab list` recipes +- support screenshot/coordinate fallback for strict CSP pages where snapshot/evaluate are blocked +- record whether trusted/CDP path was used + +Long-horizon task runner should read `task-manifest.json`, replace `$RUN_ID`, solve task, then evaluate task grader expression. diff --git a/pi-chrome/test-suite/notes/scoring.md b/pi-chrome/test-suite/notes/scoring.md new file mode 100644 index 0000000..5edb7f5 --- /dev/null +++ b/pi-chrome/test-suite/notes/scoring.md @@ -0,0 +1,44 @@ +# Benchmark scoring + +## Unit challenges (`manifest.json`) + +Score actual verdict against `expected[mode]`, not raw PASS count. + +Gate buckets: + +- `core`: headline release gate. Exact expected verdict required. +- `conditional`: gate only when declared capability/prerequisite is present; otherwise report skipped/conditional. +- `quality`: adversarial humanization/fingerprint signal. Report trend and regressions, but do not block general release unless explicitly promoted. + +Expected values: + +- `PASS` / `FAIL` expected: exact match required inside active gate bucket. +- `CONDITIONAL`: exclude from core headline score unless environment prerequisite is declared present. +- `SKIP` / `WARN`: report separately. + +Recommended flake policy: + +- Run each non-destructive challenge up to 2 retries. +- Take best verdict only for known-flaky timing/scroll tests. +- Always keep all reasons/events in detailed report. + +Difficulty weighting: + +- L1 = 1 +- L2 = 2 +- L3 = 3 + +Report both unweighted and weighted score. Also report per-category score. + +## Long-horizon tasks (`task-manifest.json`) + +Task score is deterministic grader PASS / not PASS. Record: + +- action count +- wall time +- tools used +- observation mode (`snapshot`, screenshot, accessibility, evaluate) +- whether direct state mutation/evaluate was allowed +- final grader reason + +Do not use LLM judge for hermetic tasks unless grader cannot express the target. diff --git a/pi-chrome/test-suite/scenarios/choredesk/cheats.js b/pi-chrome/test-suite/scenarios/choredesk/cheats.js new file mode 100644 index 0000000..112679f --- /dev/null +++ b/pi-chrome/test-suite/scenarios/choredesk/cheats.js @@ -0,0 +1,49 @@ +window.ChoreDeskCheats = { + "choredesk-l1-ticket-priority": [ + { tool: "chrome_fill", params: { selector: "select[aria-label=\"T-104 priority\"]", text: "High" } }, + { tool: "chrome_fill", params: { selector: "select[aria-label=\"T-104 status\"]", text: "In Progress" } }, + { tool: "chrome_click", params: { selector: "button[aria-label=\"save tickets T-104\"]" } } + ], + "choredesk-l2-refund-message": [ + { tool: "chrome_click", params: { selector: "a[data-view=\"customers\"]" } }, + { tool: "chrome_click", params: { selector: "a[data-view=\"orders\"]" } }, + { tool: "chrome_click", params: { selector: "a[data-view=\"messages\"]" } }, + { tool: "chrome_fill", params: { selector: "#msgSubject", text: "Refund review ORD-778" } }, + { tool: "chrome_fill", params: { selector: "#msgBody", text: "Noah Kim noah.kim@example.com order ORD-778 total $84.20" } }, + { tool: "chrome_click", params: { selector: "#sendMsg" } } + ], + "choredesk-l3-restock-ticket": [ + { tool: "chrome_click", params: { selector: "a[data-view=\"catalog\"]" } }, + { tool: "chrome_click", params: { selector: "a[data-view=\"inventory\"]" } }, + { tool: "chrome_fill", params: { selector: "input[aria-label=\"AST-LAMP reorder quantity\"]", text: "12" } }, + { tool: "chrome_click", params: { selector: "button[aria-label=\"save inventory AST-LAMP\"]" } }, + { tool: "chrome_click", params: { selector: "a[data-view=\"tickets\"]" } }, + { tool: "chrome_fill", params: { selector: "input[aria-label=\"T-205 internal note\"]", text: "reordered 12 AST-LAMP" } }, + { tool: "chrome_click", params: { selector: "button[aria-label=\"save tickets T-205\"]" } } + ], + run: { + "choredesk-l1-ticket-priority": async () => { + await window.__choredesk.cheatHelpers.route("tickets"); + window.__choredesk.cheatHelpers.set("select[aria-label=\"T-104 priority\"]", "High"); + window.__choredesk.cheatHelpers.set("select[aria-label=\"T-104 status\"]", "In Progress"); + document.querySelector("button[aria-label=\"save tickets T-104\"]").click(); + }, + "choredesk-l2-refund-message": async () => { + await window.__choredesk.cheatHelpers.route("customers"); + await window.__choredesk.cheatHelpers.route("orders"); + await window.__choredesk.cheatHelpers.route("messages"); + window.__choredesk.cheatHelpers.set("#msgSubject", "Refund review ORD-778"); + window.__choredesk.cheatHelpers.set("#msgBody", "Noah Kim noah.kim@example.com order ORD-778 total $84.20"); + document.querySelector("#sendMsg").click(); + }, + "choredesk-l3-restock-ticket": async () => { + await window.__choredesk.cheatHelpers.route("catalog"); + await window.__choredesk.cheatHelpers.route("inventory"); + window.__choredesk.cheatHelpers.set("input[aria-label=\"AST-LAMP reorder quantity\"]", "12"); + document.querySelector("button[aria-label=\"save inventory AST-LAMP\"]").click(); + await window.__choredesk.cheatHelpers.route("tickets"); + window.__choredesk.cheatHelpers.set("input[aria-label=\"T-205 internal note\"]", "reordered 12 AST-LAMP"); + document.querySelector("button[aria-label=\"save tickets T-205\"]").click(); + } + } +}; diff --git a/pi-chrome/test-suite/scenarios/choredesk/index.html b/pi-chrome/test-suite/scenarios/choredesk/index.html new file mode 100644 index 0000000..6481ac0 --- /dev/null +++ b/pi-chrome/test-suite/scenarios/choredesk/index.html @@ -0,0 +1,239 @@ + + +ChoreDesk hermetic task site + + + +
+
+

ChoreDesk

+ + + +
+
+
PENDING
+

+

+  
+ +
+
+ + + diff --git a/pi-chrome/test-suite/serve.sh b/pi-chrome/test-suite/serve.sh new file mode 100755 index 0000000..45efc3c --- /dev/null +++ b/pi-chrome/test-suite/serve.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Serve the test suite on a fixed local port so pi-chrome can drive it. +cd "$(dirname "$0")" +PORT="${PORT:-8765}" +echo "serving http://127.0.0.1:${PORT}/" +exec python3 -m http.server "${PORT}" --bind 127.0.0.1 diff --git a/pi-chrome/test-suite/task-manifest.json b/pi-chrome/test-suite/task-manifest.json new file mode 100644 index 0000000..e780510 --- /dev/null +++ b/pi-chrome/test-suite/task-manifest.json @@ -0,0 +1,416 @@ +[ + { + "id": "choredesk-l1-ticket-priority", + "site": "choredesk", + "startUrl": "scenarios/choredesk/index.html?task=choredesk-l1-ticket-priority&run=$RUN_ID&seed=$SEED", + "difficulty": "L1", + "category": "enterprise-crud", + "intent": "Set ticket T-104 priority to High and status to In Progress, then save it.", + "expectedActions": { + "min": 3, + "max": 6 + }, + "requires": [ + "snapshot", + "click", + "select/change", + "form editing" + ], + "grader": { + "type": "inPageProgrammatic", + "expression": "window.__taskVerdict || (window.__taskValidation?.done ? \"PASS\" : \"FAIL\")", + "passValue": "PASS" + }, + "browserGymLike": { + "observation": [ + "dom", + "accessibility_tree", + "screenshot_optional" + ], + "actionSpace": [ + "click", + "type", + "select", + "evaluate_optional" + ], + "reset": "navigate startUrl with fresh $RUN_ID or click Reset run", + "stepReturn": "obs,reward,terminated,truncated,info" + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "slowMoMs": 1000, + "timeoutMs": 5000, + "locale": null, + "timezoneId": null, + "humanBaseline": null, + "actionSubsets": [ + "bid", + "nav", + "tab", + "infeas", + "chat" + ], + "rewardShape": "binary", + "taskId": "choredesk.L1.ticket-priority", + "seed": 101, + "maxSteps": 8, + "allowedActions": [ + "click", + "fill", + "select_option", + "goto", + "report_infeasible", + "send_msg_to_user" + ], + "setupUrl": "scenarios/choredesk/index.html?task=choredesk-l1-ticket-priority&run=$RUN_ID&seed=$SEED", + "validateScript": "scenarios/choredesk/index.html#window.__choredesk.validate()", + "cheatScript": "scenarios/choredesk/cheats.js#choredesk-l1-ticket-priority", + "tags": [ + "form-fill", + "ticket", + "select" + ], + "rubric": [ + { + "id": "priorityHigh", + "weight": 0.33, + "description": "T-104 priority is High" + }, + { + "id": "statusInProgress", + "weight": 0.33, + "description": "T-104 status is In Progress" + }, + { + "id": "saved", + "weight": 0.34, + "description": "T-104 was saved" + } + ], + "goal": "Set ticket T-104 priority to High and status to In Progress, then save it.", + "goalObject": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Set ticket T-104 priority to High and status to In Progress, then save it." + } + ] + } + ], + "gradeSource": "page" + }, + { + "id": "choredesk-l2-refund-message", + "site": "choredesk", + "startUrl": "scenarios/choredesk/index.html?task=choredesk-l2-refund-message&run=$RUN_ID&seed=$SEED", + "difficulty": "L2", + "category": "cross-page-memory", + "intent": "Resolve a customer billing task by looking up customer email and order total on separate pages, then composing a message with exact required fields.", + "expectedActions": { + "min": 8, + "max": 14 + }, + "requires": [ + "multi-page navigation", + "table lookup", + "working memory", + "form composition" + ], + "grader": { + "type": "inPageProgrammatic", + "expression": "window.__taskVerdict || (window.__taskValidation?.done ? \"PASS\" : \"FAIL\")", + "passValue": "PASS" + }, + "browserGymLike": { + "observation": [ + "dom", + "accessibility_tree", + "screenshot_optional" + ], + "actionSpace": [ + "click", + "type", + "select", + "evaluate_optional" + ], + "reset": "navigate startUrl with fresh $RUN_ID or click Reset run", + "stepReturn": "obs,reward,terminated,truncated,info" + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "slowMoMs": 1000, + "timeoutMs": 5000, + "locale": null, + "timezoneId": null, + "humanBaseline": null, + "actionSubsets": [ + "bid", + "nav", + "tab", + "infeas", + "chat" + ], + "rewardShape": "binary", + "taskId": "choredesk.L2.refund-message", + "seed": 202, + "maxSteps": 16, + "allowedActions": [ + "click", + "fill", + "select_option", + "goto", + "report_infeasible", + "send_msg_to_user" + ], + "setupUrl": "scenarios/choredesk/index.html?task=choredesk-l2-refund-message&run=$RUN_ID&seed=$SEED", + "validateScript": "scenarios/choredesk/index.html#window.__choredesk.validate()", + "cheatScript": "scenarios/choredesk/cheats.js#choredesk-l2-refund-message", + "tags": [ + "lookup", + "cross-page-memory", + "message-compose" + ], + "rubric": [ + { + "id": "messageSubject", + "weight": 0.34, + "description": "Billing message subject matches" + }, + { + "id": "email", + "weight": 0.33, + "description": "Body includes Noah email" + }, + { + "id": "total", + "weight": 0.33, + "description": "Body includes exact order total" + } + ], + "goal": "Resolve a customer billing task by looking up customer email and order total on separate pages, then composing a message with exact required fields.", + "goalObject": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Resolve a customer billing task by looking up customer email and order total on separate pages, then composing a message with exact required fields." + } + ] + } + ], + "gradeSource": "page" + }, + { + "id": "choredesk-l3-restock-ticket", + "site": "choredesk", + "startUrl": "scenarios/choredesk/index.html?task=choredesk-l3-restock-ticket&run=$RUN_ID&seed=$SEED", + "difficulty": "L3", + "category": "cross-page-state-update", + "intent": "Use ticket text to identify an item, map item to SKU in Catalog, update Inventory reorder quantity, and add exact internal note back on the ticket.", + "expectedActions": { + "min": 12, + "max": 20 + }, + "requires": [ + "multi-page navigation", + "entity resolution", + "state mutation", + "exact text note", + "audit-save behavior" + ], + "grader": { + "type": "inPageProgrammatic", + "expression": "window.__taskVerdict || (window.__taskValidation?.done ? \"PASS\" : \"FAIL\")", + "passValue": "PASS" + }, + "browserGymLike": { + "observation": [ + "dom", + "accessibility_tree", + "screenshot_optional" + ], + "actionSpace": [ + "click", + "type", + "select", + "evaluate_optional" + ], + "reset": "navigate startUrl with fresh $RUN_ID or click Reset run", + "stepReturn": "obs,reward,terminated,truncated,info" + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "slowMoMs": 1000, + "timeoutMs": 5000, + "locale": null, + "timezoneId": null, + "humanBaseline": null, + "actionSubsets": [ + "bid", + "nav", + "tab", + "infeas", + "chat" + ], + "rewardShape": "binary", + "taskId": "choredesk.L3.restock-ticket", + "seed": 303, + "maxSteps": 24, + "allowedActions": [ + "click", + "fill", + "select_option", + "goto", + "report_infeasible", + "send_msg_to_user" + ], + "setupUrl": "scenarios/choredesk/index.html?task=choredesk-l3-restock-ticket&run=$RUN_ID&seed=$SEED", + "validateScript": "scenarios/choredesk/index.html#window.__choredesk.validate()", + "cheatScript": "scenarios/choredesk/cheats.js#choredesk-l3-restock-ticket", + "tags": [ + "entity-resolution", + "cross-page-state-update", + "exact-note" + ], + "rubric": [ + { + "id": "inventory", + "weight": 0.4, + "description": "AST-LAMP reorder quantity is 12 and saved" + }, + { + "id": "ticketNote", + "weight": 0.4, + "description": "T-205 note contains exact phrase" + }, + { + "id": "audit", + "weight": 0.2, + "description": "Both save audit entries exist" + } + ], + "goal": "Use ticket text to identify an item, map item to SKU in Catalog, update Inventory reorder quantity, and add exact internal note back on the ticket.", + "goalObject": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Use ticket text to identify an item, map item to SKU in Catalog, update Inventory reorder quantity, and add exact internal note back on the ticket." + } + ] + } + ], + "gradeSource": "page" + }, + { + "id": "mini-shop-red-second-cheapest", + "site": "mini-shop", + "startUrl": "fixtures/sites/mini-shop/index.html?task=mini-shop-red-second-cheapest&run=$RUN_ID&seed=$SEED", + "difficulty": "L2", + "category": "task-completion", + "intent": "Add the second-cheapest red product to cart, checkout, and leave the generated order id visible.", + "expectedActions": { + "min": 5, + "max": 9 + }, + "requires": [ + "filter/sort reasoning", + "cart state", + "checkout flow" + ], + "grader": { + "type": "inPageProgrammatic", + "expression": "window.__taskVerdict || (window.__taskValidation?.done ? \"PASS\" : \"FAIL\")", + "passValue": "PASS" + }, + "browserGymLike": { + "observation": [ + "dom", + "accessibility_tree", + "screenshot_optional" + ], + "actionSpace": [ + "click", + "read_table", + "evaluate_optional" + ], + "reset": "navigate startUrl with fresh $RUN_ID", + "stepReturn": "obs,reward,terminated,truncated,info" + }, + "viewport": { + "width": 1280, + "height": 720 + }, + "slowMoMs": 1000, + "timeoutMs": 5000, + "locale": null, + "timezoneId": null, + "humanBaseline": null, + "actionSubsets": [ + "bid", + "nav", + "tab", + "infeas", + "chat" + ], + "rewardShape": "binary", + "taskId": "mini-shop.L2.red-second-cheapest", + "seed": 404, + "maxSteps": 12, + "allowedActions": [ + "click", + "goto", + "report_infeasible", + "send_msg_to_user" + ], + "setupUrl": "fixtures/sites/mini-shop/index.html?task=mini-shop-red-second-cheapest&run=$RUN_ID&seed=$SEED", + "validateScript": "fixtures/sites/mini-shop/grader.js#MiniShopGrader.validate", + "cheatScript": "fixtures/sites/mini-shop/cheats.js#mini-shop-red-second-cheapest", + "tags": [ + "shopping", + "sort", + "cart", + "checkout" + ], + "rubric": [ + { + "id": "addedTarget", + "weight": 0.4, + "description": "Cart includes second-cheapest red product" + }, + { + "id": "noWrongItems", + "weight": 0.3, + "description": "Cart has no wrong products" + }, + { + "id": "checkedOut", + "weight": 0.3, + "description": "Checkout produced order id" + } + ], + "goal": "Add the second-cheapest red product to cart, checkout, and leave the generated order id visible.", + "goalObject": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Add the second-cheapest red product to cart, checkout, and leave the generated order id visible." + } + ] + } + ], + "gradeSource": "both" + } +] diff --git a/pi-chrome/test-suite/task-manifest.schema.json b/pi-chrome/test-suite/task-manifest.schema.json new file mode 100644 index 0000000..b8031fc --- /dev/null +++ b/pi-chrome/test-suite/task-manifest.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "pi-chrome long-horizon task manifest", + "description": "BrowserGym-inspired task manifest: setup URL, deterministic seed, goal object, action subsets, reward contract, optional cheat recipe, and in-page validate hook.", + "type": "array", + "items": { + "type": "object", + "required": ["id", "taskId", "site", "startUrl", "setupUrl", "difficulty", "category", "goal", "goalObject", "grader", "actionSubsets", "maxSteps"], + "additionalProperties": true, + "properties": { + "id": { "type": "string" }, + "taskId": { "type": "string" }, + "site": { "type": "string" }, + "seed": { "type": "integer" }, + "startUrl": { "type": "string", "description": "Relative URL. $RUN_ID and $SEED should be replaced by runner for isolated deterministic state." }, + "setupUrl": { "type": "string" }, + "validateScript": { "type": "string" }, + "cheatScript": { "type": "string" }, + "viewport": { "type": "object", "properties": { "width": { "type": "integer" }, "height": { "type": "integer" } } }, + "slowMoMs": { "type": "integer" }, + "timeoutMs": { "type": "integer" }, + "locale": { "type": ["string", "null"] }, + "timezoneId": { "type": ["string", "null"] }, + "difficulty": { "enum": ["L1", "L2", "L3"] }, + "category": { "enum": ["enterprise-crud", "cross-page-memory", "cross-page-state-update", "task-completion"] }, + "gradeSource": { "enum": ["page", "harness", "both"] }, + "tags": { "type": "array", "items": { "type": "string" } }, + "intent": { "type": "string" }, + "goal": { "type": "string" }, + "goalObject": { "type": "array", "items": { "type": "object" } }, + "expectedActions": { "type": "object", "required": ["min", "max"], "properties": { "min": { "type": "integer" }, "max": { "type": "integer" } } }, + "maxSteps": { "type": "integer" }, + "humanBaseline": { "type": ["number", "null"], "minimum": 0, "maximum": 1 }, + "actionSubsets": { "type": "array", "items": { "enum": ["chat", "infeas", "bid", "coord", "nav", "tab"] } }, + "allowedActions": { "type": "array", "items": { "type": "string" } }, + "requires": { "type": "array", "items": { "type": "string" } }, + "rewardShape": { "enum": ["binary", "graded", "shaped"] }, + "rubric": { "type": "array", "items": { "type": "object", "required": ["id", "weight", "description"], "properties": { "id": { "type": "string" }, "weight": { "type": "number" }, "description": { "type": "string" } } } }, + "grader": { + "type": "object", + "required": ["type", "expression", "passValue"], + "properties": { + "type": { "enum": ["inPageProgrammatic"] }, + "expression": { "type": "string", "description": "Should evaluate to BrowserGym-like validate object or legacy {v,r}." }, + "passValue": { "type": "string" } + } + }, + "browserGymLike": { + "type": "object", + "properties": { + "observation": { "type": "array", "items": { "type": "string" } }, + "actionSpace": { "type": "array", "items": { "type": "string" } }, + "reset": { "type": "string" }, + "stepReturn": { "const": "obs,reward,terminated,truncated,info" } + } + } + } + } +} diff --git a/pi-chrome/test-suite/unit/automation-target.test.mjs b/pi-chrome/test-suite/unit/automation-target.test.mjs new file mode 100644 index 0000000..8a6ea2b --- /dev/null +++ b/pi-chrome/test-suite/unit/automation-target.test.mjs @@ -0,0 +1,395 @@ +// Unit harness for pi-chrome's dedicated automation tab/window isolation in service_worker.js. +// +// Feature under test: pi-chrome must never navigate or replace the user's active tab. Page and +// navigation actions without an explicit target are routed to a dedicated automation target that +// the *calling Pi session* created and owns. Ownership is session-scoped (one extension brokers +// every session) and mirrored to chrome.storage.session so a service-worker restart re-hydrates +// it instead of orphaning the window. Cleanup closes only the calling session's owned target. +// +// Like csp-eval.test.mjs we load the *real* worker into a vm sandbox with a stateful chrome.* +// mock, then exercise the real helpers and the real dispatch() paths. Chrome state (tabs/windows/ +// storage.session) can be shared across two sandbox loads to simulate a service-worker restart. + +import vm from "node:vm"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const workerPath = path.resolve(__dirname, "../../extensions/chrome-profile-bridge/browser-extension/service_worker.js"); +const src = fs.readFileSync(workerPath, "utf8"); + +let failures = 0; +let passes = 0; +function ok(cond, msg) { + if (cond) { passes++; } + else { failures++; console.error(` ✗ ${msg}`); } +} +async function throwsWith(fn, re, msg) { + try { await fn(); ok(false, `${msg} (expected throw)`); } + catch (e) { ok(re.test(String(e.message || e)), `${msg} (got: ${e.message})`); } +} + +// ---- stateful Chrome mock. `state` (tabs/windows/storage) can be shared to simulate a +// service-worker restart: the browser keeps its tabs/windows/session-storage, the worker memory +// is wiped (a fresh sandbox). +function makeChromeState() { + const tabs = new Map(); // id -> { id, windowId, url, active, groupId } + const windows = new Map(); // id -> { id } + const groups = new Map(); // groupId -> { id, title, color, collapsed, windowId } + const storage = {}; // chrome.storage.session backing + let nextTabId = 1; + let nextWindowId = 1; + let nextGroupId = 1; + const alloc = { tab: () => nextTabId++, window: () => nextWindowId++, group: () => nextGroupId++ }; + + // Seed a user window with two real user tabs (Gmail + a research article, the active one). + const userWindowId = alloc.window(); + windows.set(userWindowId, { id: userWindowId }); + const userGmail = { id: alloc.tab(), windowId: userWindowId, url: "https://mail.google.com/", active: false, groupId: -1 }; + const userArticle = { id: alloc.tab(), windowId: userWindowId, url: "https://example.com/research-article", active: true, groupId: -1 }; + tabs.set(userGmail.id, userGmail); + tabs.set(userArticle.id, userArticle); + + return { tabs, windows, groups, storage, alloc, userWindowId, userGmail, userArticle }; +} + +function makeChrome(state, { withWindows = true, withStorage = true, withTabGroups = false } = {}) { + const { tabs, windows, groups, storage, alloc, userWindowId } = state; + const noop = () => {}; + const listener = { addListener: noop, removeListener: noop }; + + const chrome = { + runtime: { id: "unittestextension", getManifest: () => ({ version: "0.0.0" }), onInstalled: listener, onStartup: listener, lastError: null }, + alarms: { onAlarm: listener, create: noop, clear: noop, clearAll: noop }, + action: { onClicked: listener }, + debugger: { sendCommand: noop, attach: async () => {}, detach: async () => {}, getTargets: (cb) => cb([]), onDetach: listener }, + scripting: { executeScript: async () => [{ result: undefined }], registerContentScripts: async () => {}, unregisterContentScripts: async () => {} }, + webNavigation: { onCommitted: listener }, + tabs: { + onUpdated: listener, + query: async (q = {}) => { + let list = [...tabs.values()]; + if (q.active === true) list = list.filter((t) => t.active); + if (typeof q.windowId === "number") list = list.filter((t) => t.windowId === q.windowId); + return list.map((t) => ({ ...t })); + }, + get: async (id) => { const t = tabs.get(id); if (!t) throw new Error(`No tab with id ${id}`); return { ...t }; }, + create: async ({ url = "about:blank", active = false, windowId = userWindowId } = {}) => { + const tab = { id: alloc.tab(), windowId, url, active, groupId: -1 }; + tabs.set(tab.id, tab); + return { ...tab }; + }, + update: async (id, props = {}) => { const t = tabs.get(id); if (!t) throw new Error(`No tab with id ${id}`); Object.assign(t, props); return { ...t }; }, + remove: async (id) => { tabs.delete(id); }, + group: async ({ groupId, tabIds = [] } = {}) => { + let gid = groupId; + if (typeof gid !== "number") { + gid = alloc.group(); + const firstTab = tabs.get(tabIds[0]); + groups.set(gid, { id: gid, title: "", color: "grey", collapsed: false, windowId: firstTab ? firstTab.windowId : userWindowId }); + } + for (const tid of tabIds) { const t = tabs.get(tid); if (t) t.groupId = gid; } + return gid; + }, + ungroup: async (id) => { const ids = Array.isArray(id) ? id : [id]; for (const tid of ids) { const t = tabs.get(tid); if (t) t.groupId = -1; } }, + }, + storage: withStorage ? { + session: { + get: async (key) => (key in storage ? { [key]: storage[key] } : {}), + set: async (obj) => { Object.assign(storage, obj); }, + }, + } : undefined, + }; + + if (withTabGroups) { + chrome.tabGroups = { + query: async ({ windowId } = {}) => [...groups.values()].filter((g) => windowId === undefined || g.windowId === windowId).map((g) => ({ ...g })), + get: async (id) => { const g = groups.get(id); if (!g) throw new Error(`No group ${id}`); return { ...g }; }, + update: async (id, props = {}) => { const g = groups.get(id); if (!g) throw new Error(`No group ${id}`); Object.assign(g, props); return { ...g }; }, + }; + } + + if (withWindows) { + chrome.windows = { + create: async ({ url = "about:blank", focused = false } = {}) => { + const id = alloc.window(); + windows.set(id, { id }); + const tab = { id: alloc.tab(), windowId: id, url, active: true, groupId: -1 }; + tabs.set(tab.id, tab); + return { id, focused, tabs: [{ ...tab }] }; + }, + get: async (id) => { const w = windows.get(id); if (!w) throw new Error(`No window with id ${id}`); return { ...w }; }, + remove: async (id) => { windows.delete(id); for (const [tid, t] of [...tabs]) if (t.windowId === id) tabs.delete(tid); }, + update: async () => {}, + }; + } else { + chrome.windows = { update: async () => {} }; // no create/get/remove -> tab fallback path + } + + return chrome; +} + +function loadWorker(chrome) { + const noop = () => {}; + const sandbox = { + console, JSON, Date, Math, Promise, Array, Object, String, Number, Boolean, + Error, TypeError, Map, Set, BigInt, Symbol, structuredClone, + setTimeout, clearTimeout, setInterval: () => 0, clearInterval: noop, + fetch: async () => { throw new Error("no network in unit test"); }, + navigator: { userAgent: "unit-test" }, + WebSocket: function () {}, + chrome, + }; + sandbox.globalThis = sandbox; + sandbox.self = sandbox; + vm.createContext(sandbox); + vm.runInContext(src, sandbox); + return sandbox; +} + +const SK = "session:alpha"; // a representative sessionKey + +async function run() { + // ===== Isolation: navigation does not touch the user's active/other tabs. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state)); + const userActiveUrl = state.userArticle.url; + + const nav = await w.dispatch("page.navigate", { url: "https://pi.test/task", waitUntilLoad: false, sessionKey: SK }); + ok(state.userArticle.url === userActiveUrl, "navigate: active user tab (research article) is not overwritten"); + ok(state.userGmail.url === "https://mail.google.com/", "navigate: other user tab (Gmail) untouched"); + ok(nav.url === "https://pi.test/task", "navigate: automation target navigated to requested URL"); + ok(nav.id !== state.userArticle.id && nav.id !== state.userGmail.id, "navigate: did not reuse any user tab"); + ok(nav.windowId !== state.userWindowId, "navigate: automation target lives in a dedicated window"); + + const status = await w.dispatch("automation.status", { sessionKey: SK }); + ok(status.tabId === nav.id && status.windowId === nav.windowId, "ownership: target ids tracked for the session"); + ok(w.isPiChromeOwnedTarget(nav.id, SK) === true, "ownership: isPiChromeOwnedTarget(owned, session) === true"); + ok(w.isPiChromeOwnedTarget(state.userArticle.id) === false, "ownership: user tab is never owned (any session)"); + + // Reuse: a later navigation reuses the same owned target. + const nav2 = await w.dispatch("page.navigate", { url: "https://pi.test/step-2", waitUntilLoad: false, sessionKey: SK }); + ok(nav2.id === nav.id && nav2.windowId === nav.windowId, "reuse: second navigation reuses the same automation window/tab"); + ok(state.userArticle.url === userActiveUrl, "reuse: user tab still untouched after second navigation"); + + // Cleanup closes only the owned window; user tabs/windows survive. + const cleanup = await w.dispatch("automation.cleanup", { sessionKey: SK }); + ok(cleanup.closedWindowId === nav.windowId, "cleanup: closed the owned window"); + ok(state.tabs.has(state.userArticle.id) && state.tabs.has(state.userGmail.id), "cleanup: user tabs never closed"); + ok(state.windows.has(state.userWindowId), "cleanup: user window never closed"); + ok(!state.tabs.has(nav.id), "cleanup: the owned automation tab is gone"); + const status2 = await w.dispatch("automation.status", { sessionKey: SK }); + ok(status2.tabId === null && status2.windowId === null, "cleanup: ownership cleared"); + } + + // ===== Session-group integration: the dedicated-window tab joins this session's group. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state, { withTabGroups: true })); + // index.ts tags page.* actions with joinSessionGroup + sessionGroupTitle; replicate that here. + const groupTitle = "Pi Session: alpha"; + const nav = await w.dispatch("page.navigate", { + url: "https://pi.test/grouped", waitUntilLoad: false, + sessionKey: SK, joinSessionGroup: true, sessionGroupTitle: groupTitle, + }); + const navTab = state.tabs.get(nav.id); + ok(navTab.windowId !== state.userWindowId, "group: automation tab is in its dedicated window"); + ok(typeof navTab.groupId === "number" && navTab.groupId >= 0, "group: automation tab joined a tab group"); + const grp = state.groups.get(navTab.groupId); + ok(grp && grp.title === groupTitle, "group: the group is titled with this session's title"); + ok(grp.windowId === navTab.windowId, "group: the session group lives inside the dedicated automation window (not the user window)"); + + // A second page action reuses the same tab and does not spawn a second group. + const groupsBefore = state.groups.size; + await w.dispatch("page.navigate", { url: "https://pi.test/grouped-2", waitUntilLoad: false, sessionKey: SK, joinSessionGroup: true, sessionGroupTitle: groupTitle }); + ok(state.groups.size === groupsBefore, "group: reusing the automation tab does not create a second group"); + } + + // ===== tab.new joins the existing session group instead of creating one group per window. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state, { withTabGroups: true })); + const groupTitle = "Pi Session: alpha"; + const nav = await w.dispatch("page.navigate", { + url: "https://pi.test/group-owner", waitUntilLoad: false, + sessionKey: SK, joinSessionGroup: true, sessionGroupTitle: groupTitle, + }); + const navTab = state.tabs.get(nav.id); + const groupId = navTab.groupId; + const groupsBefore = state.groups.size; + + const opened = await w.dispatch("tab.new", { url: "https://pi.test/new-tab", groupTitle, sessionKey: SK }); + ok(state.groups.size === groupsBefore, "tab.new-group: did not create another same-session group"); + ok(opened.tab.groupId === groupId, "tab.new-group: opened tab joined the existing session group"); + ok(opened.tab.windowId === nav.windowId, "tab.new-group: opened tab was created in the existing group's window"); + + const forced = await w.dispatch("tab.new", { url: "https://pi.test/no-opt-out", groupTitle, group: false, sessionKey: SK }); + ok(forced.tab.groupId === groupId, "tab.new-group: group:false is ignored; tab still joins the session group"); + ok(state.groups.size === groupsBefore, "tab.new-group: group:false does not create another group"); + + const blankTitle = await w.dispatch("tab.new", { url: "https://pi.test/blank-title", groupTitle: "", group: false, sessionKey: SK }); + ok(typeof blankTitle.tab.groupId === "number" && blankTitle.tab.groupId >= 0, "tab.new-group: groupTitle:'' still creates a grouped tab"); + ok(blankTitle.group.title === "Pi", "tab.new-group: blank groupTitle falls back to a group instead of opting out"); + + const nav2 = await w.dispatch("page.navigate", { + url: "https://pi.test/new-automation-target", waitUntilLoad: false, + sessionKey: "session:beta", joinSessionGroup: true, sessionGroupTitle: groupTitle, + }); + ok(state.groups.size === groupsBefore + 1, "automation-target-group: reused the existing session group, only blank-title Pi group was extra"); + ok(nav2.groupId === groupId, "automation-target-group: new automation target joined the existing session group"); + ok(nav2.windowId === nav.windowId, "automation-target-group: new automation target was created in the existing group's window"); + } + + // ===== tab.new never leaves an ungrouped tab behind when grouping fails. ===== + { + const state = makeChromeState(); + const chrome = makeChrome(state, { withTabGroups: true }); + const w = loadWorker(chrome); + const tabsBefore = state.tabs.size; + chrome.tabs.group = async () => { throw new Error("group blew up"); }; + + await throwsWith( + () => w.dispatch("tab.new", { url: "https://pi.test/group-fail", groupTitle: "Pi Session: alpha", sessionKey: SK }), + /group blew up/, + "tab.new-group-fail: surfaces grouping error", + ); + ok(state.tabs.size === tabsBefore, "tab.new-group-fail: closes the created tab instead of leaving it ungrouped"); + } + + // ===== Grouping is best-effort: a tabGroups failure must not break navigation. ===== + { + const state = makeChromeState(); + const chrome = makeChrome(state, { withTabGroups: true }); + chrome.tabs.group = async () => { throw new Error("group blew up"); }; + const w = loadWorker(chrome); + const nav = await w.dispatch("page.navigate", { url: "https://pi.test/group-fail", waitUntilLoad: false, sessionKey: SK, joinSessionGroup: true, sessionGroupTitle: "Pi Session: alpha" }); + ok(nav.url === "https://pi.test/group-fail", "group-fail: navigation still succeeds when grouping throws"); + ok(state.tabs.get(nav.id).windowId !== state.userWindowId, "group-fail: still used the dedicated automation window"); + } + + // ===== Concurrency: two sessions get separate windows; cleanup is per-session. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state)); + const a = await w.dispatch("page.navigate", { url: "https://pi.test/a", waitUntilLoad: false, sessionKey: "session:A" }); + const b = await w.dispatch("page.navigate", { url: "https://pi.test/b", waitUntilLoad: false, sessionKey: "session:B" }); + ok(a.id !== b.id && a.windowId !== b.windowId, "concurrency: each session gets its own dedicated window/tab"); + ok(w.isPiChromeOwnedTarget(a.id, "session:A") && !w.isPiChromeOwnedTarget(a.id, "session:B"), "concurrency: ownership is scoped to the creating session"); + + // Cleaning up session A must not touch session B's target. + await w.dispatch("automation.cleanup", { sessionKey: "session:A" }); + ok(!state.tabs.has(a.id), "concurrency: cleanup closed session A's tab"); + ok(state.tabs.has(b.id), "concurrency: cleanup left session B's tab open"); + const bStatus = await w.dispatch("automation.status", { sessionKey: "session:B" }); + ok(bStatus.tabId === b.id, "concurrency: session B still owns its target after A cleanup"); + } + + // ===== Service-worker restart / reconnect: persisted ownership re-hydrates from storage. ===== + { + const state = makeChromeState(); + const w1 = loadWorker(makeChrome(state)); + const nav = await w1.dispatch("page.navigate", { url: "https://pi.test/persist", waitUntilLoad: false, sessionKey: SK }); + ok(typeof state.storage.piChromeAutomationTargets === "object", "restart: ownership was persisted to storage.session"); + + // Simulate the MV3 service worker being suspended and restarted: fresh sandbox (memory wiped), + // same browser tabs/windows + same session storage. + const w2 = loadWorker(makeChrome(state)); + const statusAfterRestart = await w2.dispatch("automation.status", { sessionKey: SK }); + ok(statusAfterRestart.tabId === nav.id && statusAfterRestart.windowId === nav.windowId, "restart: re-hydrated the owned target from storage"); + + // A navigation after restart must REUSE the existing window, not orphan it with a new one. + const windowsBefore = state.windows.size; + const nav2 = await w2.dispatch("page.navigate", { url: "https://pi.test/persist-2", waitUntilLoad: false, sessionKey: SK }); + ok(nav2.id === nav.id && nav2.windowId === nav.windowId, "restart: navigation after restart reuses the persisted window (no orphan)"); + ok(state.windows.size === windowsBefore, "restart: no new window created after restart"); + + // Cleanup after restart works and clears persisted state. + await w2.dispatch("automation.cleanup", { sessionKey: SK }); + const persisted = state.storage.piChromeAutomationTargets || {}; + ok(!(SK in persisted), "restart: cleanup removed the session from persisted storage"); + } + + // ===== Restart after the user manually closed the window: no orphan, fresh target. ===== + { + const state = makeChromeState(); + const w1 = loadWorker(makeChrome(state)); + const nav = await w1.dispatch("page.navigate", { url: "https://pi.test/closed", waitUntilLoad: false, sessionKey: SK }); + await state.windows.delete(nav.windowId); // user closed pi-chrome's window + for (const [tid, t] of [...state.tabs]) if (t.windowId === nav.windowId) state.tabs.delete(tid); + + const w2 = loadWorker(makeChrome(state)); // SW restart + const nav2 = await w2.dispatch("page.navigate", { url: "https://pi.test/reopened", waitUntilLoad: false, sessionKey: SK }); + ok(nav2.id !== nav.id, "restart-after-close: a fresh automation target is created when the persisted one is gone"); + ok(state.tabs.has(nav2.id), "restart-after-close: new target exists"); + } + + // ===== tab.* management never auto-creates / never falls back to the user's active tab. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state)); + const windowsBefore = state.windows.size; + const tabsBefore = state.tabs.size; + + await throwsWith( + () => w.dispatch("tab.close", { sessionKey: SK }), + /no automation tab yet|Pass targetId/, + "tab.close: with no target and no owned target, errors instead of closing the user's active tab", + ); + ok(state.tabs.has(state.userArticle.id), "tab.close: user's active tab was NOT closed"); + ok(state.windows.size === windowsBefore && state.tabs.size === tabsBefore, "tab.close: did not spawn a throwaway tab/window"); + + await throwsWith(() => w.dispatch("tab.activate", { sessionKey: SK }), /no automation tab yet|Pass targetId/, "tab.activate: errors with no target/owned target"); + + // Once an automation target exists, management actions operate on it (not on the user tab). + const nav = await w.dispatch("page.navigate", { url: "https://pi.test/manage", waitUntilLoad: false, sessionKey: SK }); + const closed = await w.dispatch("tab.close", { sessionKey: SK }); + ok(closed.closed === nav.id, "tab.close: with an owned target, closes that target"); + ok(state.tabs.has(state.userArticle.id), "tab.close: user tab still safe after closing the owned target"); + } + + // ===== Explicit targeting still works on any existing tab (no regression). ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state)); + const nav = await w.dispatch("page.navigate", { url: "https://pi.test/explicit", targetId: String(state.userGmail.id), waitUntilLoad: false, sessionKey: SK }); + ok(nav.id === state.userGmail.id, "explicit: targetId routes to the requested existing tab"); + ok(state.userGmail.url === "https://pi.test/explicit", "explicit: explicitly targeted tab is navigated"); + const status = await w.dispatch("automation.status", { sessionKey: SK }); + ok(status.tabId === null, "explicit: explicit targeting does not create/claim an automation target"); + } + + // ===== Window-unavailable fallback: a dedicated TAB is used, and the user's window is safe. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state, { withWindows: false })); + const target = await w.getOrCreateAutomationTarget(SK); + ok(target.id !== state.userArticle.id && target.id !== state.userGmail.id, "fallback: created a dedicated tab, not a user tab"); + ok(w.isPiChromeOwnedTarget(target.id, SK) === true, "fallback: dedicated tab is owned"); + const cleanup = await w.cleanupAutomationTarget(SK); + ok(cleanup.closedTabId === target.id && cleanup.closedWindowId === null, "fallback: cleanup closes only the owned tab (never the shared window)"); + ok(state.windows.has(state.userWindowId), "fallback: cleanup never closes the user/shared window"); + ok(state.tabs.has(state.userArticle.id) && state.tabs.has(state.userGmail.id), "fallback: cleanup leaves user tabs intact"); + } + + // ===== Robust cleanup: no-op when nothing created, and when target already closed manually. ===== + { + const state = makeChromeState(); + const w = loadWorker(makeChrome(state)); + const empty = await w.cleanupAutomationTarget(SK); + ok(empty.closedWindowId === null && empty.closedTabId === null, "cleanup: no-op when nothing was ever created"); + + const t = await w.getOrCreateAutomationTarget(SK); + // User closed pi-chrome's window manually (Chrome closes its tabs too). + state.windows.delete(t.windowId); + for (const [tid, tab] of [...state.tabs]) if (tab.windowId === t.windowId) state.tabs.delete(tid); + const stale = await w.cleanupAutomationTarget(SK); + ok(stale.closedWindowId === null && stale.closedTabId === null, "cleanup: robust when owned window was already closed"); + } + + console.log(`\n${passes} passed, ${failures} failed`); + if (failures) process.exit(1); +} + +run().catch((e) => { console.error(e); process.exit(1); }); diff --git a/pi-chrome/test-suite/unit/csp-eval.test.mjs b/pi-chrome/test-suite/unit/csp-eval.test.mjs new file mode 100644 index 0000000..69f7943 --- /dev/null +++ b/pi-chrome/test-suite/unit/csp-eval.test.mjs @@ -0,0 +1,192 @@ +// Unit harness for the CSP-bypass layer in service_worker.js. +// +// The real CSP bypass (CDP Runtime.evaluate not being subject to page CSP) can only be +// proven in a browser — see challenge 39-strict-csp-fallback. These tests instead validate +// the JS *logic* of the refactor that the bypass depends on: +// - evaluateInTab: wrapper-string construction, expression/statement fallback, value +// marker round-trip (undefined/function/symbol/bigint/Error/DOMRect), error propagation. +// - executeInTab: 2-phase define-then-invoke, envelope unwrap, error propagation, and that +// all real HELPER_FUNCS serialize+assign without a parse error. +// - page.waitFor: service-worker-side polling via evaluateInTab (selector + expression). +// +// We load the worker into a vm sandbox with mocked chrome.* APIs, then replace `cdp` with a +// shim that evaluates the expression in a separate "page world" vm context (simulating CDP +// Runtime.evaluate returnByValue). No browser, no network, no deps. + +import vm from "node:vm"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const workerPath = path.resolve(__dirname, "../../extensions/chrome-profile-bridge/browser-extension/service_worker.js"); +const src = fs.readFileSync(workerPath, "utf8"); + +let failures = 0; +let passes = 0; +function ok(cond, msg) { + if (cond) { passes++; } + else { failures++; console.error(` ✗ ${msg}`); } +} +async function throwsWith(fn, re, msg) { + try { await fn(); ok(false, `${msg} (expected throw)`); } + catch (e) { ok(re.test(String(e.message || e)), `${msg} (got: ${e.message})`); } +} + +// ---- page world: simulates the page's MAIN world for Runtime.evaluate ---- +const pageGlobals = { + console, JSON, Date, Math, Promise, Object, Array, String, Number, Boolean, + Error, TypeError, SyntaxError, RangeError, BigInt, Symbol, structuredClone, + setTimeout, parseInt, parseFloat, isNaN, + document: { + title: "page title", + _present: new Set(), + querySelector(sel) { return this._present.has(sel) ? { sel } : null; }, + }, +}; +pageGlobals.window = pageGlobals; +pageGlobals.globalThis = pageGlobals; +const pageWorld = vm.createContext(pageGlobals); + +// Simulate CDP Runtime.evaluate returnByValue serialization. +function toCdpResult(v) { + if (v === undefined) return { result: { type: "undefined" } }; + if (v === null) return { result: { type: "object", subtype: "null", value: null } }; + const t = typeof v; + if (t === "number" || t === "string" || t === "boolean") + return { result: { type: t, value: v } }; + // object/array: returnByValue deep-clones JSON-able structures + return { result: { type: "object", value: JSON.parse(JSON.stringify(v)) } }; +} + +// ---- worker sandbox ---- +const noop = () => {}; +const listener = { addListener: noop, removeListener: noop }; +const sandbox = { + console, JSON, Date, Math, Promise, Array, Object, String, Number, Boolean, + Error, TypeError, Map, Set, BigInt, Symbol, structuredClone, + setTimeout, clearTimeout, + setInterval: () => 0, + clearInterval: noop, + fetch: async () => { throw new Error("no network in unit test"); }, + navigator: { userAgent: "unit-test" }, + WebSocket: function () {}, + chrome: { + runtime: { id: "unittestextension", getManifest: () => ({ version: "0.0.0" }), onInstalled: listener, onStartup: listener, lastError: null }, + alarms: { onAlarm: listener, create: noop, clear: noop, clearAll: noop }, + action: { onClicked: listener }, + debugger: { sendCommand: noop, attach: async () => {}, detach: async () => {}, getTargets: (cb) => cb([]) }, + scripting: { executeScript: async () => [{ result: undefined }] }, + tabs: { query: async () => [], get: async () => ({}), create: async () => ({}), update: async () => ({}), remove: async () => {} }, + windows: { update: async () => {} }, + webNavigation: { onCommitted: listener }, + }, +}; +sandbox.globalThis = sandbox; +sandbox.self = sandbox; +vm.createContext(sandbox); +vm.runInContext(src, sandbox); + +// ---- override the page-touching primitives with the page-world shim ---- +sandbox.attachDebugger = async () => ({}); +sandbox.bringToFront = async () => {}; +sandbox.getTabByParams = async (p) => ({ id: (p && p.targetId) || 1, windowId: 1 }); +sandbox.cdp = async (_tabId, method, params) => { + if (method !== "Runtime.evaluate") return {}; + try { + const value = await vm.runInContext(params.expression, pageWorld); + return toCdpResult(value); + } catch (e) { + return { exceptionDetails: { exception: { className: e.name, description: String(e.stack || e.message) }, text: "Uncaught " + String(e) } }; + } +}; +// Phase-2 of executeInTab: run the injected wrapper func against the page world, +// where Phase-1 (via cdp shim above) already defined window.__piAction + helpers. +sandbox.chrome.scripting.executeScript = async ({ func, args }) => { + const fn = vm.runInContext("(" + func.toString() + ")", pageWorld); + const result = await fn(...(args || [])); + return [{ result }]; +}; + +const { evaluateInTab, executeInTab, dispatch } = sandbox; + +async function run() { + // ===== evaluateInTab: primitives & objects ===== + ok((await evaluateInTab({ expression: "2 + 2" })) === 4, "evaluate: arithmetic expression"); + ok((await evaluateInTab({ expression: "document.title" })) === "page title", "evaluate: expression without return"); + ok((await evaluateInTab({ expression: "'a' + 'b'" })) === "ab", "evaluate: string concat"); + const obj = await evaluateInTab({ expression: "({a:1, b:[2,3]})" }); + ok(obj && obj.a === 1 && obj.b[1] === 3, "evaluate: object literal round-trips"); + + // ===== value markers ===== + ok((await evaluateInTab({ expression: "void 0" })) === undefined, "evaluate: undefined marker -> undefined"); + ok((await evaluateInTab({ expression: "10n" })) === "10", "evaluate: bigint marker -> string"); + ok(/^\[Function:/.test(await evaluateInTab({ expression: "(function foo(){})" })), "evaluate: function marker"); + ok((await evaluateInTab({ expression: "Promise.resolve(42)" })) === 42, "evaluate: promise is awaited"); + + // DOMRect-like (toJSON + width/height/top) is expanded, not flattened to {} + const rect = await evaluateInTab({ expression: "({ x:1,y:2,width:3,height:4,top:2,right:4,bottom:6,left:1, toJSON(){return {}} })" }); + ok(rect && rect.width === 3 && rect.bottom === 6, "evaluate: DOMRect-like expanded"); + + // ===== statement-form fallback (expression form is a SyntaxError) ===== + // `let x=...; x` is not a valid expression, so the wrapper must retry as a statement body. + ok((await evaluateInTab({ expression: "let x = 5; x" })) === undefined, "evaluate: statement form falls back (no return -> undefined)"); + ok((await evaluateInTab({ expression: "let y = 7; return y" })) === 7, "evaluate: statement form with explicit return"); + + // ===== error propagation ===== + await throwsWith(() => evaluateInTab({ expression: "throw new Error('boom')" }), /chrome_evaluate failed[\s\S]*boom/, "evaluate: runtime error propagates"); + + // ===== executeInTab: 2-phase define + invoke ===== + // Real HELPER_FUNCS get serialized + assigned in Phase 1; a parse error there would throw here. + const sum = await executeInTab({ targetId: 1 }, function add(a, b) { return a + b; }, [3, 4]); + ok(sum === 7, "executeInTab: action runs with args after helper injection"); + + const asyncResult = await executeInTab({ targetId: 1 }, async function asyncEcho(v) { return v * 2; }, [21]); + ok(asyncResult === 42, "executeInTab: async action awaited"); + + await throwsWith( + () => executeInTab({ targetId: 1 }, function boom() { throw new Error("action failed"); }, []), + /action failed/, + "executeInTab: thrown action error propagates via envelope", + ); + + // ===== page.waitFor (service-worker-side polling) ===== + pageGlobals.document._present.add("#ready"); + const wf = await dispatch("page.waitFor", { targetId: 1, kind: "selector", value: "#ready", timeoutMs: 1000, intervalMs: 20 }); + ok(wf && typeof wf.elapsedMs === "number", "waitFor: selector present resolves"); + + const wfExpr = await dispatch("page.waitFor", { targetId: 1, kind: "expression", value: "1 === 1", timeoutMs: 1000, intervalMs: 20 }); + ok(wfExpr && typeof wfExpr.elapsedMs === "number", "waitFor: truthy expression resolves"); + + await throwsWith( + () => dispatch("page.waitFor", { targetId: 1, kind: "selector", value: "#never", timeoutMs: 120, intervalMs: 30 }), + /Timed out after 120ms/, + "waitFor: missing selector times out", + ); + + // ===== usKeyLayoutForChar / cdpKeyInfo: US-layout key codes ===== + // Regression: punctuation must NOT use charCodeAt() (".":46 collides with VK_DELETE, + // "-":45 with VK_INSERT), which made apps drop the char on keydown. + const { usKeyLayoutForChar, cdpKeyInfo } = sandbox; + const period = usKeyLayoutForChar("."); + ok(period.code === "Period" && period.keyCode === 190 && !period.needShift, "keylayout: '.' -> Period/190 (not 46)"); + const dash = usKeyLayoutForChar("-"); + ok(dash.code === "Minus" && dash.keyCode === 189, "keylayout: '-' -> Minus/189 (not 45)"); + const slash = usKeyLayoutForChar("/"); + ok(slash.code === "Slash" && slash.keyCode === 191, "keylayout: '/' -> Slash/191"); + const at = usKeyLayoutForChar("@"); + ok(at.code === "Digit2" && at.keyCode === 50 && at.needShift, "keylayout: '@' -> Digit2/50 + shift"); + const A = usKeyLayoutForChar("A"); + ok(A.code === "KeyA" && A.keyCode === 65 && A.needShift, "keylayout: 'A' -> KeyA/65 + shift"); + const a = usKeyLayoutForChar("a"); + ok(a.code === "KeyA" && a.keyCode === 65 && !a.needShift, "keylayout: 'a' -> KeyA/65 no shift"); + const dot = cdpKeyInfo("."); + ok(dot.code === "Period" && dot.windowsVirtualKeyCode === 190 && dot.text === ".", "cdpKeyInfo: '.' -> Period/190 with text"); + const ent = cdpKeyInfo("Enter"); + ok(ent.code === "Enter" && ent.windowsVirtualKeyCode === 13, "cdpKeyInfo: named key 'Enter' unaffected"); + + console.log(`\n${passes} passed, ${failures} failed`); + if (failures) process.exit(1); +} + +run().catch((e) => { console.error(e); process.exit(1); }); diff --git a/pi-chrome/test/session-snapshot-store.test.ts b/pi-chrome/test/session-snapshot-store.test.ts new file mode 100644 index 0000000..f2c7d96 --- /dev/null +++ b/pi-chrome/test/session-snapshot-store.test.ts @@ -0,0 +1,100 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { SessionSnapshotStore, snapshotTargetKey } from "../src/session-snapshot-store.ts"; +import { CHROME_TOOL_NAMES, formatChromeFindMatches, formatSnapshotCapture } from "../extensions/chrome-profile-bridge/index.ts"; + +test("snapshotTargetKey prefers explicit target selectors", () => { + assert.equal(snapshotTargetKey(), "default"); + assert.equal(snapshotTargetKey({ titleIncludes: "Inbox" }), "title:Inbox"); + assert.equal(snapshotTargetKey({ urlIncludes: "example.com" }), "url:example.com"); + assert.equal(snapshotTargetKey({ targetId: "42", urlIncludes: "ignored" }), "target:42"); +}); + +test("SessionSnapshotStore atomically keeps only the latest capture per target", async () => { + const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-snapshot-")); + const store = new SessionSnapshotStore(workspace, "session/private/id"); + const first = await store.save({ mode: "interactive", targetId: 42, elements: [{ uid: "el-1" }], matches: [] }); + const second = await store.save({ mode: "changes", targetId: 42, elements: [{ uid: "el-2" }], matches: [{ uid: "el-2" }] }); + + assert.equal(first.metadata.path, second.metadata.path); + assert.notEqual(first.metadata.captureId, second.metadata.captureId); + assert.equal(second.metadata.elementCount, 1); + assert.equal(second.metadata.matchCount, 1); + assert.equal(second.metadata.targetId, 42); + assert.ok(!second.metadata.path.includes("private")); + + const stored = JSON.parse(await readFile(second.absolutePath, "utf8")); + assert.equal(stored.version, 1); + assert.equal(stored.captureId, second.metadata.captureId); + assert.equal(stored.snapshot.elements[0].uid, "el-2"); + + if (process.platform !== "win32") { + assert.equal((await stat(store.sessionRoot)).mode & 0o777, 0o700); + assert.equal((await stat(second.absolutePath)).mode & 0o777, 0o600); + } + + await store.clear(); + await assert.rejects(stat(second.absolutePath), { code: "ENOENT" }); +}); + +test("target invalidation and session cleanup do not affect another session", async () => { + const workspace = await mkdtemp(join(tmpdir(), "pi-chrome-isolation-")); + const firstSession = new SessionSnapshotStore(workspace, "session-a"); + const secondSession = new SessionSnapshotStore(workspace, "session-b"); + const firstTarget = { targetId: "101" }; + const secondTarget = { targetId: "202" }; + const first = await firstSession.save({ elements: [] }, firstTarget); + const otherTarget = await firstSession.save({ elements: [] }, secondTarget); + const otherSession = await secondSession.save({ elements: [] }, firstTarget); + + await firstSession.invalidate(firstTarget); + await assert.rejects(stat(first.absolutePath), { code: "ENOENT" }); + await stat(otherTarget.absolutePath); + await stat(otherSession.absolutePath); + + await firstSession.clear(); + await assert.rejects(stat(otherTarget.absolutePath), { code: "ENOENT" }); + await stat(otherSession.absolutePath); + await secondSession.clear(); +}); + +test("snapshot result formatting stays bounded and points to Context Mode", () => { + const text = formatSnapshotCapture({ + version: 1, + captureId: "capture-1", + capturedAt: "2026-08-28T00:00:00.000Z", + path: ".pi/chrome-context/session/target/snapshot.json", + bytes: 80_000, + targetKey: "target:42", + targetId: 42, + mode: "interactive", + elementCount: 80, + matchCount: 3, + staleAfterInteraction: true, + }); + + assert.match(text, /ctx_execute_file/); + assert.match(text, /\.pi\/chrome-context\/session\/target\/snapshot\.json/); + assert.doesNotMatch(text, /raw-secret-page-content/); +}); + +test("chrome_find formatting is capped and authorization tracks all 21 tools", () => { + const matches = Array.from({ length: 25 }, (_, index) => ({ + kind: "element", + uid: `el-${index}`, + role: "button", + label: `Button ${index}`, + rect: { x: index, y: index, width: 10, height: 10 }, + })); + const text = formatChromeFindMatches({ query: "button", matches }); + + assert.match(text, /el-0/); + assert.match(text, /el-19/); + assert.doesNotMatch(text, /el-20/); + assert.equal(CHROME_TOOL_NAMES.length, 21); + assert.ok(CHROME_TOOL_NAMES.includes("chrome_find")); + assert.ok(CHROME_TOOL_NAMES.includes("chrome_inspect")); +}); diff --git a/tests/extension-load.test.ts b/tests/extension-load.test.ts index bd81e78..876bfb9 100644 --- a/tests/extension-load.test.ts +++ b/tests/extension-load.test.ts @@ -74,9 +74,10 @@ test("all package extensions load together without global registration conflicts assert.ok(packageJson.pi.extensions.includes("./pi-ask-user/index.ts"), "the bundle must load pi-ask-user"); assert.equal(packageJson.dependencies["pi-ask-user"], "file:./pi-ask-user"); assert.ok(packageJson.files.includes("pi-ask-user"), "the packed bundle must include pi-ask-user source"); - const chromeExtension = "./node_modules/pi-chrome/extensions/chrome-profile-bridge/index.ts"; - assert.equal(packageJson.dependencies["pi-chrome"], "0.15.46", "the browser bridge package must stay exactly pinned"); - assert.ok(packageJson.pi.extensions.includes(chromeExtension), "the bundle must load pi-chrome's Chrome profile bridge"); + const chromeExtension = "./pi-chrome/extensions/chrome-profile-bridge/index.ts"; + assert.equal(packageJson.dependencies["pi-chrome"], "file:./pi-chrome", "the browser bridge must use the locally maintained source"); + assert.ok(packageJson.files.includes("pi-chrome"), "the packed bundle must include the locally maintained pi-chrome source"); + assert.ok(packageJson.pi.extensions.includes(chromeExtension), "the bundle must load the local Chrome profile bridge directly"); assert.ok( packageJson.pi.extensions.indexOf(chromeExtension) < packageJson.pi.extensions.indexOf("./pi-tool-search/extensions/index.ts"), diff --git a/tests/tool-routing.test.ts b/tests/tool-routing.test.ts index 2858f3c..b34a839 100644 --- a/tests/tool-routing.test.ts +++ b/tests/tool-routing.test.ts @@ -50,6 +50,14 @@ test("context routing analyzes large files without a full read", () => { assert.match(section, /host permission approval does not bypass this Context Mode boundary/); }); +test("Chrome snapshot routing keeps raw page data out of model context", () => { + const section = buildToolRoutingSection(["chrome_snapshot", "ctx_execute_file"]); + + assert.match(section, /immediately analyze the returned workspace-relative capture path with ctx_execute_file/); + assert.match(section, /never use read or cat for the raw JSON/); + assert.match(section, /take a fresh snapshot after any interaction/); +}); + test("hashline routing uses small reads and fresh anchors for edits", () => { const section = buildToolRoutingSection(["read", "edit"]);