Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
11 commits
Select commit Hold shift + click to select a range
55cbd58
📝 sctl Browser 第 5 期原始 CDP spec
CodFrm Oct 9, 2026
f47608b
📝 第 5 期 spec 修订:设置下载行为的请求成功但不起作用(Playwright 连接时一定会发,报错会让 connectOverC…
CodFrm Oct 9, 2026
1f4ab3f
✨ 第 5 期协议与扩展:端点用的目标列表、UA、打开关闭标签页、标签页通知、端点占用豁免空闲断开与 ENDPOINT_CONNECTED
CodFrm Oct 9, 2026
118537d
✨ 第 5 期 bridge:支持多个浏览器监听者、路由标签页通知、忘记实例(含离线)时通知监听者
CodFrm Oct 9, 2026
2321505
✨ 第 5 期 sctl cdp send:原始 CDP 命令(拒绝破坏性命令、弹框期间照常发送、Chrome 错误原样返回)、MCP c…
CodFrm Oct 9, 2026
4359cd8
✨ 第 5 期页面所有权移交:Manager.HandOver 取消并等待该浏览器上的 sctl 命令与空闲断开、结束录制、先关弹框再断开…
CodFrm Oct 9, 2026
8ec96d9
✨ 第 5 期 CDP 端点:每个浏览器一个端点(随机密钥只在内存、重复创建同一地址)、/cdp/<密钥>/json/version 与 …
CodFrm Oct 9, 2026
46d252b
✨ 第 5 期 CDP 端点模拟:Emulator 在根会话上模拟浏览器(getVersion 真实 UA、setDownloadBeha…
CodFrm Oct 9, 2026
45ace62
🔌 第 5 期 CDP 端点:daemon 启用 Emulator,删除 UnsupportedHook 及其测试,protocol.md…
CodFrm Oct 9, 2026
1197fba
🐛 第 5 期 spec 验证:浏览器离线时 sctl cdp status/close 仍能查看并撤销点名浏览器的端点地址(新增 bri…
CodFrm Oct 9, 2026
249a5b6
🐛 第 5 期代码审查:附加准备期间 Chrome 断开调试器时 attachToTarget 返回错误而不是交出一个从未报告附加的会话;…
CodFrm Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ internal/client/ internal/daemon/ internal/pkg/
The authority always lives on the extension side for ScriptCat's write and source-disclosure gates: the daemon
approves no write and discloses no source on its own — it forwards the request and blocks until a human
decides in the browser. Browser control (`sctl browsers`, `tabs`, `windows`, `groups`, `reading-list`, `bookmarks`,
`history`, `browsing-data`, `recent`, `downloads`, `cookies`, `extensions`), page automation (`sctl page`), and page
debugging (`sctl debug`) are the deliberate exception: they have no human gate by design, so any control-token holder
can drive a paired `sctl Browser` instance and the pages in it, and read their console and network traffic (headers
and bodies unmasked), immediately. Only the L2 browser-data operations — deleting bookmarks and
`history`, `browsing-data`, `recent`, `downloads`, `cookies`, `extensions`), page automation (`sctl page`), page
debugging (`sctl debug`), and raw CDP (`sctl cdp`) are the deliberate exception: they have no human gate by design, so
any control-token holder can drive a paired `sctl Browser` instance and the pages in it, and read their console and
network traffic (headers and bodies unmasked), immediately. Only the L2 browser-data operations — deleting bookmarks and
uninstalling an extension — are gated: the `sctl Browser` extension holds them until a human approves in its own
approval window. See [`docs/threat-model.md`](./docs/threat-model.md). Full process model and package responsibilities are
in [`docs/architecture.md`](./docs/architecture.md).
Expand Down
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,13 @@ troubleshooting.
| `sctl debug network [--url S] [--method M] [--status 404\|4xx] [--type T] [--failed] [--after CURSOR] [--limit N]` | List a tab's network requests, oldest first. |
| `sctl debug request <ID> [--body]` | Show one request's headers, request body, timing, and, with `--body`, response body. |
| `sctl debug clear` | Empty a tab's debug records without detaching the debugger. |
| `sctl cdp send <Method> [--params '<JSON object>'] [--tab N] [--timeout D]` | Send one raw Chrome DevTools Protocol command to a tab's page and print Chrome's result. |
| `sctl cdp endpoint` / `sctl cdp status` / `sctl cdp close` | Create or show a browser's CDP endpoint addresses for Playwright `connectOverCDP` and Puppeteer `connect`, show whether a client is connected and when the endpoint expires, or close it. |

Run `sctl --help` or `sctl <command> --help` for usage and flags. Write operations block
until the user approves, rejects, or closes the confirmation flow in ScriptCat; browser control commands run
immediately with no approval step (see [`docs/threat-model.md`](./docs/threat-model.md)). `tabs`, `windows`, `groups`,
`reading-list`, `bookmarks`, `history`, `browsing-data`, `recent`, `downloads`, `cookies`, `extensions`, `page`, and `debug` accept `--browser <name|id>` (or `SCTL_BROWSER`) to pick an instance when more than one is online.
`reading-list`, `bookmarks`, `history`, `browsing-data`, `recent`, `downloads`, `cookies`, `extensions`, `page`, `debug`, and `cdp` accept `--browser <name|id>` (or `SCTL_BROWSER`) to pick an instance when more than one is online.
Destructive browser operations need explicit confirmation: `reading-list rm`, `history rm`, `history clear`, `browsing-data clear`, `downloads cancel`, `erase` and `delete-file`, `cookies rm` and `clear`, and `extensions disable` run only with `--yes` (MCP:
`confirm: true`); without it nothing runs and the command exits with code 3. `bookmarks rm <id>...` needs human
approval instead: the browser opens an approval window and the command waits, exiting 0 once the bookmarks are
Expand Down Expand Up @@ -292,6 +294,75 @@ size given. When Chrome no longer keeps a body — the page navigated away since
there is none, the page never read it, or it is over Chrome's limit of about 20 MB — or while a JS dialog is open in
the tab, the reason is printed and the command still exits 0. An unknown or dropped ID fails with `NOT_FOUND` (exit code 3).

`sctl cdp send <Method>` sends one raw Chrome DevTools Protocol command, such as `Page.getNavigationHistory`, to the
top-level page of a tab, and prints Chrome's result as indented JSON plus `tabId` and `contentTrust`. `--params` takes
a JSON object; `--tab`, `--browser`, and `--timeout` work as for `page` commands. The tab is attached like for a page
command, the command queues with page and debug commands on the same tab, and it is sent even while a JS dialog is
open (so `Page.handleJavaScriptDialog` works; a command Chrome blocks during a dialog waits until `--timeout`).
Only the top-level page is addressed, and the events a command causes are not returned. A command Chrome rejects or
does not know fails with `INVALID_REQUEST` (exit code 3) carrying Chrome's own error; a result over 4 MiB fails with
`PAYLOAD_TOO_LARGE`. `Page.disable`, `Runtime.disable`, `Network.disable`, `Log.disable`,
`Emulation.setFocusEmulationEnabled`, `Target.setAutoAttach`, and `Target.detachFromTarget` are refused without being
sent, because they break state sctl depends on. Every other command is sent as is and its effects are yours to undo:
a persistent setting such as `Emulation.setDeviceMetricsOverride` or `Network.setExtraHTTPHeaders` affects later page
commands until you restore it; after `Fetch.enable` every request of the tab hangs, since nothing handles the paused
requests; `Debugger.enable` plus `Debugger.pause` freezes the page. Recover with `Fetch.disable` or `Debugger.resume`,
or `sctl page detach` and attach again. Like `page` and `debug`, it has no human gate. The MCP equivalent is `cdp_send`
(see [`docs/mcp.md`](./docs/mcp.md)).

`sctl cdp endpoint` creates a CDP endpoint for the browser, or shows the one it already has, and prints two addresses:
`http://<daemon address>/cdp/<secret>` for Playwright `chromium.connectOverCDP` and
`ws://<daemon address>/cdp/<secret>/devtools/browser/<id>` for Puppeteer `connect({browserWSEndpoint})`, plus whether a
client is connected and when the endpoint expires. A connected client gets full control of every tab Chrome lets a
debugger attach to in that browser, with no approval. The address carries a random secret and is itself the
credential, so treat it like a password. One client can connect at a time; while it is connected, `sctl page`,
`sctl debug`, and `sctl cdp send` on that browser fail with `ENDPOINT_CONNECTED` (exit code 3), and the other
commands keep working. When the client disconnects, sctl detaches the tabs it attached, keeps them open, and its own
commands work again; the same address can connect again. `sctl cdp status` shows the addresses, whether a client is
connected and since when, and when the endpoint expires, or says that there is none. `sctl cdp close` closes the
endpoint at once, disconnecting its client, and succeeds when there is none. The endpoint also expires when the daemon
exits, when the browser is forgotten, and after 60 minutes without a connected client. The MCP equivalents are
`cdp_endpoint` and `cdp_close`. Details are in [`docs/protocol.md`](./docs/protocol.md#34-raw-cdp-endpoint) and the
security trade in [`docs/threat-model.md`](./docs/threat-model.md).

Point an existing script at the addresses `sctl cdp endpoint` prints. With Playwright, use the browser's own
context — the user's profile, with its logins — instead of creating one:

```js
const { chromium } = require("playwright");

const browser = await chromium.connectOverCDP("http://127.0.0.1:8643/cdp/<secret>");
const context = browser.contexts()[0];
const page = await context.newPage(); // or pick one of context.pages()
await page.goto("https://example.com");
await browser.close(); // disconnects only: the browser and every tab stay open
```

With Puppeteer, pass `defaultViewport: null` so it does not resize the user's tabs to its default viewport:

```js
const puppeteer = require("puppeteer-core");

const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:8643/cdp/<secret>/devtools/browser/<id>",
defaultViewport: null,
});
const [page] = await browser.pages();
await page.goto("https://example.com");
await browser.disconnect();
```

The client sees every tab Chrome lets a debugger attach to (not `chrome://` pages, extension pages, or the Chrome Web
Store), including tabs the user or a page opens while it is connected; each tab it attaches shows Chrome's
debugging infobar and gets focus emulation, so background tabs behave like the active one. Browser-level commands
are answered by sctl, and commands for a page or its cross-process iframes go to that tab unchanged. Not supported,
and answered with a CDP error saying sctl's CDP endpoint does not support them: new browser contexts (Playwright
`browser.newContext()`, Puppeteer `createBrowserContext()`), granting permissions, window size and position,
ignoring certificate errors, and Service Worker targets. Setting the download behavior (Playwright always does)
succeeds without effect: downloads follow the browser's own settings and the client gets no download events. On
Chrome 125, Chrome itself refuses to read cookies (Playwright `context.cookies()`), and its error comes back as is. A
browser event larger than 4 MiB cannot cross the extension connection and is dropped, so the client never sees it.

## License

GPL-3.0, the same license as ScriptCat. See [LICENSE](./LICENSE).
57 changes: 56 additions & 1 deletion docs/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ Chrome DevTools Protocol 驱动页面时,Chrome 会在浏览器顶部显示"sctl
| `sctl debug network [--url S] [--method M] [--status 404\|4xx] [--type T] [--failed] [--after CURSOR] [--limit N]` | 按开始先后列出标签页的网络请求。 |
| `sctl debug request <ID> [--body]` | 显示一个请求的请求头、请求体、各阶段耗时,加 `--body` 时一并给出响应体。 |
| `sctl debug clear` | 清空标签页的调试记录,不断开调试器。 |
| `sctl cdp send <Method> [--params '<JSON 对象>'] [--tab N] [--timeout D]` | 向标签页的页面发送一条原始 Chrome DevTools Protocol 命令,输出 Chrome 的结果。 |
| `sctl cdp endpoint` / `sctl cdp status` / `sctl cdp close` | 创建或查看浏览器的 CDP 端点地址(给 Playwright `connectOverCDP` 与 Puppeteer `connect`),查看是否有客户端连着、何时失效,或关闭端点。 |

运行 `sctl --help` 或 `sctl <command> --help` 查看用法和参数。写操作会阻塞,直到用户在
ScriptCat 中批准、拒绝或关闭确认流程;浏览器控制命令按设计没有审批步骤、立即执行(参见
[`threat-model.md`](./threat-model.md))。当多个实例同时在线时,`tabs`、`windows`、`groups`、`reading-list`、`bookmarks`、`history`、`browsing-data`、`recent`、`downloads`、`cookies`、`extensions`、`page` 与 `debug` 可用
[`threat-model.md`](./threat-model.md))。当多个实例同时在线时,`tabs`、`windows`、`groups`、`reading-list`、`bookmarks`、`history`、`browsing-data`、`recent`、`downloads`、`cookies`、`extensions`、`page`、`debug` 与 `cdp` 可用
`--browser <name|id>`(或环境变量 `SCTL_BROWSER`)指定目标实例。破坏性的浏览器操作需要显式确认:
`reading-list rm`、`history rm`、`history clear`、`browsing-data clear`,`downloads cancel`、`erase`、`delete-file`,`cookies rm`、`clear`,以及 `extensions disable` 必须加 `--yes`(MCP 传 `confirm: true`),否则什么都不执行,退出码为 3。
`bookmarks rm <id>...` 则需要人工审批:浏览器打开审批窗口,命令一直等待;书签删除后退出码为 0,被拒绝或关闭窗口为 1,
Expand Down Expand Up @@ -228,6 +230,59 @@ sctl 只在自己即将断开调试器时(`sctl page detach`、5 分钟空闲断
Chrome 已不再保留某个体时(之后页面导航离开、请求进行中或失败、没有体、页面没有读取它,或超过 Chrome 约 20 MB 的上限),以及标签页上有未处理的 JS 弹框时,输出写明原因,退出码仍为 0。
ID 不存在或已被丢弃时返回 `NOT_FOUND`(退出码 3)。

`sctl cdp send <Method>` 向标签页的顶层页面发送一条原始 Chrome DevTools Protocol 命令(如 `Page.getNavigationHistory`),
以缩进的 JSON 输出 Chrome 的结果,外加 `tabId` 和 `contentTrust`。`--params` 接受 JSON 对象;`--tab`、`--browser`、`--timeout` 与 `page` 命令相同。
标签页未附加时先附加,同一标签页上与页面、调试命令排队执行;标签页有未处理的 JS 弹框时照常发送(所以可以发 `Page.handleJavaScriptDialog`,
弹框期间 Chrome 会阻塞的命令会等到 `--timeout`)。只发给顶层页面,命令产生的事件不返回。
Chrome 拒绝或不认识这条命令时返回 `INVALID_REQUEST`(退出码 3),带 Chrome 自己的错误信息;结果超过 4 MiB 返回 `PAYLOAD_TOO_LARGE`。
`Page.disable`、`Runtime.disable`、`Network.disable`、`Log.disable`、`Emulation.setFocusEmulationEnabled`、`Target.setAutoAttach`、`Target.detachFromTarget`
会破坏 sctl 自身依赖的状态,直接拒绝、不发给 Chrome。其余命令原样发送,副作用由你负责恢复:
`Emulation.setDeviceMetricsOverride`、`Network.setExtraHTTPHeaders` 这类持续生效的设置会影响之后的页面命令,直到你恢复;
`Fetch.enable` 之后没有人处理被暂停的请求,标签页上的请求都会挂住;`Debugger.enable` 加 `Debugger.pause` 会让页面停住。
恢复办法:发送 `Fetch.disable` 或 `Debugger.resume`,或 `sctl page detach` 后重新附加。和 `page`、`debug` 一样没有人工确认环节。
对应的 MCP 工具是 `cdp_send`(见 [`mcp.md`](./mcp.md))。

`sctl cdp endpoint` 为浏览器创建一个 CDP 端点,已有时给出同一个,并输出两种地址:给 Playwright `chromium.connectOverCDP` 的
`http://<daemon 地址>/cdp/<密钥>`,给 Puppeteer `connect({browserWSEndpoint})` 的 `ws://<daemon 地址>/cdp/<密钥>/devtools/browser/<id>`,
同时给出是否有客户端连着、何时失效。连上的客户端可以不经审批完整控制这个浏览器里 Chrome 允许附加调试器的全部标签页。
地址里带随机密钥,地址本身就是凭据,请像密码一样对待。同一时刻只能有一个客户端;客户端连着时,这个浏览器上的 `sctl page`、`sctl debug`、
`sctl cdp send` 返回 `ENDPOINT_CONNECTED`(退出码 3),其他命令照常执行。客户端断开后,sctl 断开它附加的标签页、保留这些标签页,
自己的命令恢复可用;同一地址可以再次连接。`sctl cdp status` 列出地址、是否有客户端连着及连上的时间、失效时间,没有端点时如实说明。
`sctl cdp close` 让端点立即失效并断开连着的客户端,没有端点时也成功。daemon 退出、浏览器被遗忘、连续 60 分钟没有客户端连着时端点也会失效。
对应的 MCP 工具是 `cdp_endpoint` 与 `cdp_close`。细节见 [`protocol.md`](./protocol.md#34-raw-cdp-endpoint),安全取舍见 [`threat-model.md`](./threat-model.md)。

把已有的脚本指向 `sctl cdp endpoint` 输出的地址即可。Playwright 请使用浏览器自己的上下文(即用户的配置文件与登录状态),不要新建:

```js
const { chromium } = require("playwright");

const browser = await chromium.connectOverCDP("http://127.0.0.1:8643/cdp/<密钥>");
const context = browser.contexts()[0];
const page = await context.newPage(); // 或者从 context.pages() 里选一个
await page.goto("https://example.com");
await browser.close(); // 只断开连接:浏览器和所有标签页都保留
```

Puppeteer 请传 `defaultViewport: null`,否则它会把用户的标签页调整成它的默认视口尺寸:

```js
const puppeteer = require("puppeteer-core");

const browser = await puppeteer.connect({
browserWSEndpoint: "ws://127.0.0.1:8643/cdp/<密钥>/devtools/browser/<id>",
defaultViewport: null,
});
const [page] = await browser.pages();
await page.goto("https://example.com");
await browser.disconnect();
```

客户端能看到 Chrome 允许附加调试器的全部标签页(`chrome://` 页面、扩展页面和 Chrome 应用商店除外),包括连着期间用户或页面新打开的标签页;
它附加的每个标签页都会显示 Chrome 的调试提示条并开启焦点模拟,后台标签页的表现与前台一致。浏览器级命令由 sctl 回答,发给页面或其跨进程 iframe 的命令原样转发到那个标签页。
以下功能不支持,会返回说明 sctl 的 CDP 端点不支持的 CDP 错误:新的浏览器上下文(Playwright `browser.newContext()`、Puppeteer `createBrowserContext()`)、
权限授予、窗口尺寸与位置、忽略证书错误、Service Worker 目标。设置下载行为的请求(Playwright 连接时一定会发)成功但不起作用:下载按浏览器自己的设置进行,客户端收不到下载事件。
Chrome 125 上读取 Cookie 会被 Chrome 自己拒绝(Playwright `context.cookies()`),错误原样返回。超过 4 MiB 的浏览器事件无法经扩展连接传出,会被丢弃,客户端收不到。

## 许可证

GPL-3.0,与 ScriptCat 相同。参见 [LICENSE](../LICENSE)。
Loading
Loading