lasso-mcp 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.en.md +8 -3
  2. package/README.md +9 -4
  3. package/dist/channels/BrowseChannel.d.ts +33 -2
  4. package/dist/channels/BrowseChannel.d.ts.map +1 -1
  5. package/dist/channels/BrowseChannel.js +163 -17
  6. package/dist/channels/BrowseChannel.js.map +1 -1
  7. package/dist/channels/LoggedInChannel.d.ts.map +1 -1
  8. package/dist/channels/LoggedInChannel.js +17 -0
  9. package/dist/channels/LoggedInChannel.js.map +1 -1
  10. package/dist/config/config.d.ts +20 -0
  11. package/dist/config/config.d.ts.map +1 -1
  12. package/dist/config/config.js +28 -0
  13. package/dist/config/config.js.map +1 -1
  14. package/dist/doctor/chrome-status.d.ts +27 -0
  15. package/dist/doctor/chrome-status.d.ts.map +1 -1
  16. package/dist/doctor/chrome-status.js +56 -1
  17. package/dist/doctor/chrome-status.js.map +1 -1
  18. package/dist/doctor/doctor.d.ts +1 -1
  19. package/dist/doctor/doctor.d.ts.map +1 -1
  20. package/dist/doctor/doctor.js +12 -3
  21. package/dist/doctor/doctor.js.map +1 -1
  22. package/dist/fallback/outcome.d.ts.map +1 -1
  23. package/dist/fallback/outcome.js +6 -0
  24. package/dist/fallback/outcome.js.map +1 -1
  25. package/dist/index.js +45 -8
  26. package/dist/index.js.map +1 -1
  27. package/dist/launcher/chrome-idle-reaper.d.ts +52 -10
  28. package/dist/launcher/chrome-idle-reaper.d.ts.map +1 -1
  29. package/dist/launcher/chrome-idle-reaper.js +69 -16
  30. package/dist/launcher/chrome-idle-reaper.js.map +1 -1
  31. package/dist/launcher/chrome-ledger.d.ts +30 -0
  32. package/dist/launcher/chrome-ledger.d.ts.map +1 -1
  33. package/dist/launcher/chrome-ledger.js +25 -0
  34. package/dist/launcher/chrome-ledger.js.map +1 -1
  35. package/dist/launcher/desired-hide-enforcer.d.ts +19 -2
  36. package/dist/launcher/desired-hide-enforcer.d.ts.map +1 -1
  37. package/dist/launcher/desired-hide-enforcer.js +12 -3
  38. package/dist/launcher/desired-hide-enforcer.js.map +1 -1
  39. package/dist/launcher/launch-chrome.d.ts +11 -0
  40. package/dist/launcher/launch-chrome.d.ts.map +1 -1
  41. package/dist/launcher/launch-chrome.js +8 -0
  42. package/dist/launcher/launch-chrome.js.map +1 -1
  43. package/dist/tools/annotations.d.ts +4 -1
  44. package/dist/tools/annotations.d.ts.map +1 -1
  45. package/dist/tools/annotations.js +4 -1
  46. package/dist/tools/annotations.js.map +1 -1
  47. package/dist/tools/browse.d.ts.map +1 -1
  48. package/dist/tools/browse.js +27 -9
  49. package/dist/tools/browse.js.map +1 -1
  50. package/dist/tools/descriptions.d.ts.map +1 -1
  51. package/dist/tools/descriptions.js +43 -6
  52. package/dist/tools/descriptions.js.map +1 -1
  53. package/dist/tools/screenshot.d.ts +2 -2
  54. package/dist/tools/screenshot.d.ts.map +1 -1
  55. package/dist/tools/screenshot.js +30 -15
  56. package/dist/tools/screenshot.js.map +1 -1
  57. package/doc/bugs/02-chrome-idle-reaper-second-consumer.md +4 -0
  58. package/doc/bugs/03-2026-09-07-hidden/346/241/243/345/212/253/346/214/201/347/224/250/346/210/267Chrome/346/277/200/346/264/273.md +5 -1
  59. package/doc/bugs/06-2026-09-10-/346/227/245/345/270/270/346/241/243idle0/345/271/275/347/201/265/345/270/270/351/251/273-/347/241/254/351/241/266/344/270/216/351/205/215/346/226/271/346/262/273/347/220/206/345/206/263/350/256/256.md +197 -0
  60. package/doc/bugs/06-/346/266/210/350/264/271/346/226/271/351/200/232/347/237/245/350/246/201/347/202/271-cc-control/344/270/216media-gen-mcp.md +27 -0
  61. package/doc/bugs/07-2026-09-10-/346/266/210/350/264/271/346/226/271/345/217/260/350/264/246L4-/346/210/252/345/233/276/345/277/205/347/273/217/351/207/215/345/257/274/350/210/252/346/227/240/346/263/225/346/210/252/345/275/223/345/211/215/351/241/265/351/235/242/347/212/266/346/200/201.md +340 -0
  62. package/doc/testing/01-/345/212/237/350/203/275/346/265/213/350/257/225/346/270/205/345/215/225.md +4 -0
  63. package/doc/usage/01-KEY-GUIDE.md +4 -1
  64. package/doc/usage/02-TROUBLESHOOTING.md +9 -3
  65. package/doc//345/257/271/346/216/245/345/256/236/346/226/275/350/257/264/346/230/216-/346/270/262/346/237/223/346/241/243x-media-gen-mcp.md +4 -0
  66. package/doc//346/270/262/346/237/223/346/241/243/350/256/276/350/256/241/345/206/263/350/256/256.md +4 -0
  67. package/doc//351/234/200/346/261/202-/346/270/262/346/237/223/346/241/243/346/265/217/350/247/210/345/231/250/346/262/273/347/220/206.md +4 -0
  68. package/package.json +1 -1
  69. package/src/channels/BrowseChannel.ts +183 -19
  70. package/src/channels/LoggedInChannel.ts +17 -0
  71. package/src/config/config.ts +35 -0
  72. package/src/doctor/chrome-status.ts +89 -1
  73. package/src/doctor/doctor.ts +12 -3
  74. package/src/fallback/outcome.ts +6 -0
  75. package/src/index.ts +45 -8
  76. package/src/invariants/check-invariants.mjs +318 -1
  77. package/src/launcher/chrome-idle-reaper.ts +90 -16
  78. package/src/launcher/chrome-ledger.ts +34 -0
  79. package/src/launcher/desired-hide-enforcer.ts +22 -3
  80. package/src/launcher/launch-chrome.ts +16 -0
  81. package/src/tools/annotations.ts +4 -1
  82. package/src/tools/browse.ts +35 -17
  83. package/src/tools/descriptions.ts +43 -6
  84. package/src/tools/screenshot.ts +32 -17
package/README.en.md CHANGED
@@ -168,6 +168,8 @@ When you don't need to render a full page, direct HTTP is **~4× faster and ~4×
168
168
 
169
169
  All images and PDFs are **saved to disk and a path is returned** — no giant blob dumped into your chat to waste context. Oversized text output (fetch_url / network, etc.) beyond 48 KiB is also written to disk automatically, returning a preview plus an `@oN` continuation handle — page through it with the `read_text` tool (directly callable over MCP since v1.8). To **choose the screenshot output path yourself** (`options.screenshot.filePath`): rejected by default (no arbitrary-path writes) — set `LASSO_SCREENSHOT_DIR=/your/output/dir` to opt in, and writes are confined to the allowlisted subtrees (auto-created nested dirs included); with no path specified the default `/tmp/lasso-screenshot-*.png` behavior is unchanged.
170
170
 
171
+ > **Shoot "the page's current state"** (mid-frame animation forensics): `action=screenshot` on `browse_headless` / `browse_logged_in` **with url omitted** shoots the page that channel is already managing — **zero re-navigation**, so page state built by earlier evaluate calls (e.g. a CSS animation frozen at 35% via `document.getAnimations().forEach(a=>a.pause())`) survives into the PNG. Recommended loop: any url-bearing action to open the session → `evaluate` to freeze/inject → `{action:"screenshot"}` (no url) for the current frame → `console` for forensics — four channels, one session, one tool. The standalone `screenshot` tool with url omitted does the same (bound to the headless session; for a logged_in session's current frame use `browse_logged_in`). With no active session (cold channel / page closed / rotated by self-heal) it fails explicitly (`didnt` + a rebuild hint) — it **never silently opens a browser and never shoots a blank page fabricating state**; url-bearing usage is byte-for-byte unchanged.
172
+
171
173
  ### See What a Page Loads
172
174
 
173
175
  > You: "What third-party trackers did this page load?" → resource list with tracker-domain counts
@@ -210,7 +212,7 @@ Goes to the Internet Archive (Wayback Machine) to find the last archived copy of
210
212
 
211
213
  ## Install
212
214
 
213
- **Current version v1.23.0** (changelog in the collapsed block at the end of this section).
215
+ **Current version v1.25.0** (changelog in the collapsed block at the end of this section).
214
216
 
215
217
  Prerequisites: Node.js ≥ 20 + Claude Code (or any MCP-capable client).
216
218
 
@@ -227,6 +229,9 @@ Restart Claude Code → `/mcp` → `lasso ✓ Connected`. **That's the one line
227
229
  <details>
228
230
  <summary>📋 Changelog (v1.8 → v1.18.6 — click to see what each version changed)</summary>
229
231
 
232
+ - **v1.24.0 (unreleased, merged in this working tree)**: 🔴 **behavior change (BUG-06 — root fix for the 12-hour ghost-resident incident, doc/bugs/06)** — ① `--idle-ms 0` / `LASSO_LAUNCH_IDLE_MS=0` semantics change from "never reap" to "**hard-cap reap after 24 hours of no activity**" (counted from the last activity signal — lasso browse now writes the touch file automatically, external consumers keep it alive with `touch ~/.cache/lasso/chrome-touch-<port>`, so **active use never triggers it**; new key `LASSO_LAUNCH_HARD_CAP_MS` tunes or disables it); truly never reap = the two-flag deliberate form `--idle-ms 0 --no-hard-cap`, or deployment-level `LASSO_LAUNCH_HARD_CAP_MS=0`. visible / user-claimed (chrome-show) exemptions unchanged (r2: a claim landing between the reap decision and the kill is honored too — the reaper's default stop now re-evaluates userTakenAt at kill time, closing the adversarial-review P2 TOCTOU); the render tier is untouched (`LASSO_RENDER_IDLE_MS` stays independent, pinned by the INV-94 tripwire). **On upgrade, pre-existing over-age idle-0 ledger records are reaped by the first reaper tick (≤15s)** — this is intended (such instances are exactly the incident's ghost form); nohup recipes relying on 0=infinite without touching should migrate (see TROUBLESHOOTING §9.3 / KEY-GUIDE). ② cross-process activity source of truth (A-7): `lasso browse` activity now lands in the touch file on disk — visible to the enforcer reaper host (previously it only touched in-server process memory, leaving a 24h/30min mis-reap blind spot whenever both hosts coexist, fixed in the same stroke); ③ over-age observability (B): chrome-status/doctor continuously emit an advisory warn block for capped records past 50% of the cap (expected reap time + touch keep-alive recipe + the user's own chrome-stop exit) with zero new kill/directive surface; ④ with global `LASSO_LAUNCH_IDLE_MS=0` the reaper now runs in cap-only mode (D2 — disabling idle reaping no longer also dismantles the hard-cap backstop). Adversarial round r1 adopted both P1 findings (N1 cross-process blind spot / N2 default flipped to 0 and sunk into the assembly layers); round r2 closed the reaper TOCTOU (P2 — the default reap exit now re-evaluates userTakenAt at kill time); new invariant INV-94.
233
+ - **v1.25.0**: current-page screenshot batch (BUG-07, novel-engine animation-forensics driven) — `action=screenshot` **with url omitted now screenshots the current managed page directly** (the standalone `screenshot` tool gains the same semantics): previously both screenshot paths re-navigated unconditionally, wiping whatever page state evaluate had established (a CSS animation paused mid-frame, injected temporary DOM) — state-sensitive screenshots were unreachable and the consumer had to detour through a second browser-automation stack. Now the same session closes the loop across four channels: open with url → `evaluate` freeze/inject → screenshot without url → `console` forensics. With no active session (cold channel / page closed / healed-over), it errors explicitly with guidance — **never silently opening a browser, never faking state with a blank page**; the old url-bearing behavior is unchanged (pinned by regression anchors); three clean adversarial rounds — 6/6 mutations red + session-drift attacks on live dual channels with zero penetration (concurrent races refused / popup vectors pinned to the managed page / silent page-swap invalidated at the commit point). Upstream research verdict: the industry-universal screenshot semantic IS "capture the current state" — this version realigns lasso with the standard.
234
+ - **v1.24.0**: ghost-instance root-fix batch (BUG-06, driven by a live 12-hour instance) — ① 🔴 **behavior change**: `launch-chrome --idle-ms 0` changes from "never reap" to "**no self-reaping, but a 24-hour hard cap as a safety net**" — a windowless hidden instance can no longer linger indefinitely as an "invisible, uncloseable ghost" (this batch's trigger: a 12-hour instance invisible in the Dock and menu bar with no way to close it). True unlimited now requires the additional explicit `--no-hard-cap` (**double intent** — prevents scripts passing 0 casually); deployments can tune or disable via `LASSO_LAUNCH_HARD_CAP_MS` (`=0` disables entirely); failure direction is always safe (invalid values fall back to capped). Old scripts relying on "0 = forever" should add `--no-hard-cap`. ② **cross-process liveness truth**: before hard-cap reaping, the touch-file heartbeat is consulted (any session / any consumer still using it → no wrongful kill); userTakenAt / visible exemptions carry over natively (re-evaluated at kill time, the race window sealed). ③ **over-age observability**: `doctor` / `chrome-status` warn early on instances past half the hard cap (no new kill surface added). Six live-machine acceptance checks all PASS + two clean adversarial rounds (three mutations all red / wrongful-kill panorama replayed with live stand-in processes, zero casualties / render tier zero cross-contamination).
230
235
  - **v1.23.0**: local-file & debugging batch (BUG-05, novel-engine field driven) — ① **`LASSO_ALLOW_FILE_FROM` file:// directory allowlist**: testing local single-file HTML (a staple for offline-artifact acceptance) used to be flat-blocked by the SSRF guard — now, with `LASSO_ALLOW_FILE_FROM=/your/project/dir`, `browse_*` can navigate/snapshot/extract/screenshot local files **inside the designated directory subtree** (still denied by default; zero-config = zero behavior change; `../` traversal, symlink escapes, prefix forgery and trailing-symlink write escapes all sealed — 18 read-side + 10 write-side attack variants, zero escapes); ② new **`console` action**: read the current page's console messages since the last navigation (severity filter + most-recent-N) — no more window.onerror injection detours when debugging rendering defects; ③ **`ignored_options` honesty contract**: options keys you passed that the current action doesn't consume are listed verbatim in the response (never silently dropped — one root cause of the "worked but no screenshot" trap the consumer hit); and to be explicit: navigate-then-screenshot in one step **is already what `action=screenshot` does**; ④ **`LASSO_SCREENSHOT_DIR` screenshot write-root constraint**: custom screenshot output paths are denied by default (arbitrary-write protection), opt-in and confined to the designated subtree. Two clean adversarial rounds: 6/6 mutations killed + proof the core SSRF guard is unweakened + consumer scenario (real single-file HTML over both http and file://) fully verified on a live machine.
231
236
  - **v1.22.1**: audit & stability batch — ① six consumer-deep-inspection callback items closed: the cc-control hook sample **fixed for three defects and now ships in-repo with 17 vector tests** (samples must live in-repo, docs only point — deliverable-quality rule established), doctor port-probe detail wording made honest across four outcome shapes, version-alignment tests generalized (assertions read package.json as the single source of truth), `npm run gate` gate hardening (explicit count assertions — pipes can no longer swallow exit codes); ② full review per the 03 audit checklist (four-track six-dimension + Gauntlet verification + clean double-blind adversarial); ③ three timing flakes fixed (wall-clock boundary → fixed-clock injection, sleep-for-disk → polling, threshold aligned to the discrimination surface) — verified by five consecutive full-suite green runs + gate.
232
237
  - **v1.22.0**: ownership-verdict & channel-self-heal batch (BUG-04, consumer-field driven) — ① new subcommand **`chrome-status [--port N]`**: takes "who occupies this port and may I touch it" out of the AI's hands — lasso classifies it directly in ten categories (ledger zombie / stale / suspected user asset …) and outputs a human decision message + machine-readable directives (`allowed_commands` / `must_report` — on a "suspected user asset" verdict the AI is explicitly told it has **zero allowed actions and must report to you**) + a paste-ready report pack; companion `chrome-stop --zombie-gate` is the only machine kill exit (in-ledger + fingerprint match + ledger re-read at kill time — triple gate). Existing commands unchanged. ② fixes the `browse_logged_in` **selected-page wedge** (P1, channel-level): root cause is a structural flaw in the upstream engine — once the selected page closes, every later navigate/evaluate/snapshot fails forever until restart; the wedge signature is now detected and healed by quietly taking over with a background page of our own, with an honest prefix when healing fails. ③ `evaluate` accepts the `(async () => {...})()` form (previously "fn is not a function"); ④ your own bad JS no longer trips the fallback chain (script errors vs channel errors classified); ⑤ doctor port-probe wording made honest ("HTTP 404" → four real outcome shapes). Two clean adversarial rounds: 5/5 mutations killed + live-machine sovereignty checks against a real user Chrome + report-pack weaponization tests. For heavy automation users: a PreToolUse hook sample (hard-blocking AI killing browsers via Bash) is in doc/bugs/04 appendix E.
@@ -308,7 +313,7 @@ lasso launch-chrome
308
313
  The first time, log in to your accounts in that window (handle 2FA yourself) — **the login state is reused from then on**. Afterwards, just say "open my logged-in Jira" to Claude.
309
314
 
310
315
  - Works **silently with zero window** by default — never steals focus, always muted; add `--mode visible` to watch it work
311
- - CLI-launched Chrome **auto-collects after 30 min of no activity by default** (BUG-03 A1 — "stays alive while in use, auto-collected when idle"; external tools keep it alive with one `touch ~/.cache/lasso/chrome-touch-<port>`; long sessions opt out with `--idle-ms 0`); `lasso chrome-stop` closes it manually at any time
316
+ - CLI-launched Chrome **auto-collects after 30 min of no activity by default** (BUG-03 A1 — "stays alive while in use, auto-collected when idle"; external tools keep it alive with one `touch ~/.cache/lasso/chrome-touch-<port>`; long sessions opt out of idle reaping with `--idle-ms 0` — which now carries a **24-hour no-activity hard cap** (BUG-06); truly never reap requires the `--idle-ms 0 --no-hard-cap` two-flag form); `lasso chrome-stop` closes it manually at any time
312
317
  - Tabs it opens in your Chrome are restored to the original list when you say `admin {action:"tab_restore", reason:"done"}` after a task (the same happens automatically on server exit)
313
318
 
314
319
  > 🔴 **Red line**: 2FA / verification codes / CAPTCHA — Lasso doesn't solve these for you; pass them manually once in the window.
@@ -319,7 +324,7 @@ The first time, log in to your accounts in that window (handle 2FA yourself) —
319
324
  - Since v1.8, a dedicated Lasso profile is used by default (Chrome 136+ forbids opening a debug port on the default profile — the old way would exit instantly); to reuse an existing profile, use `lasso launch-chrome --profile <dir>`.
320
325
  - After launch, the debug port is probed automatically; if Chrome didn't come up or the port is taken, it fails loudly instead of faking success.
321
326
  - Auto-close threshold: `LASSO_LAUNCH_IDLE_MS` (default 60000; `300000` reverts to 5 minutes; `0` disables). To let a single long task through: `--idle-ms 3600000`. Since BUG-03 an **explicit CLI launch defaults to `--idle-ms 1800000` (30 min)** — a server exit never guilt-by-associates CLI-launched Chromes (owner filtering), and the enforcer process (alive even without a server) collects on expiry; explicit env / config.json still wins.
322
- - **Keep-alive for external CDP consumers** (v1.18.5; wording revised in BUG-03): when other tools (chrome-devtools-mcp, automation scripts, …) connect straight to this Chrome's debug port, a single `touch ~/.cache/lasso/chrome-touch-<port>` counts as "just used" (the promise is now **stays alive while in use** — only silent expiry collects); no resident Chrome is needed (hidden-tier cold start reaches a usable CDP in ~1.5 s measured); for a whole working session launch with `--idle-ms 0` (explicit opt-out of auto-collect) or touch periodically.
327
+ - **Keep-alive for external CDP consumers** (v1.18.5; wording revised in BUG-03; as of BUG-06 lasso browse also lands in the same touch file automatically — the keep-alive signal is visible across reaper hosts): when other tools (chrome-devtools-mcp, automation scripts, …) connect straight to this Chrome's debug port, a single `touch ~/.cache/lasso/chrome-touch-<port>` counts as "just used" (the promise is now **stays alive while in use** — only silent expiry collects); no resident Chrome is needed (hidden-tier cold start reaches a usable CDP in ~1.5 s measured); for a whole working session launch with `--idle-ms 3600000` (auto-collects on expiry) or touch periodically — `--idle-ms 0` now carries the 24h hard cap (BUG-06) and is no longer an unbounded resident.
323
328
  - The headless browser auto-recycles after 5 minutes idle (`LASSO_HEADLESS_IDLE_MS` to tune or disable).
324
329
  - Honest boundary: running `lasso launch-chrome` standalone (outside the server) has no idle auto-close — the exit is `chrome-stop`; `browse_logged_in` connecting to **your own visible Chrome** is "low-disturbance, not zero-disturbance" (a macOS platform-level limitation — occasional operations may steal focus once) — for pure silence use the hidden tier or `browse_headless`; `desktop` simulates a real human's keyboard and mouse, so it occupies the physical keyboard/mouse by design — there is no silent form.
325
330
  - chrome-stop only closes the Chromes Lasso itself launched, with verified ownership — it never touches your manually-opened browsers.
package/README.md CHANGED
@@ -169,6 +169,8 @@ key 怎么申请、免费额度多少 → [**Key 配置指南**](./doc/usage/01-
169
169
 
170
170
  所有图片和 PDF 都**存到本地、返回路径**,不会把一大坨图片数据塞进对话浪费上下文。超大文本输出(fetch_url / network 等)超过 48 KiB 也会自动落盘,返回预览 + `@oN` 续页句柄——用 `read_text` 工具按页续读(v1.8 起经 MCP 可直接调用)。要**自定截图落盘位置**(`options.screenshot.filePath`):默认拒(防任意路径写盘)——设 `LASSO_SCREENSHOT_DIR=/你的输出目录` 开放,且只允许写进指定目录子树(嵌套新目录自动创建也限在内);不指定路径时默认落 `/tmp/lasso-screenshot-*.png`,行为不变。
171
171
 
172
+ > **截「当前页面状态」**(动画中间帧取证):`browse_headless` / `browse_logged_in` 的 `action=screenshot` **省略 url** 即对当前受管页面直接截屏——**零重导航**,前面 evaluate 建立的页面状态(如 `document.getAnimations().forEach(a=>a.pause())` 冻结在 35% 的 CSS 动画)原样保留。推荐循环:带 url 的 action 建会话 → `evaluate` 冻结/注入 → `{action:"screenshot"}`(无 url)截当前帧 → `console` 取证——同会话四通道闭环。独立 `screenshot` 工具省略 url 同义(绑 headless 会话;logged_in 会话的当前帧用 `browse_logged_in`)。无活动会话(冷通道/页面已关/被自愈换页)会显式报错(`didnt` + 下一步指引),**绝不静默新开浏览器、绝不截空白页伪造状态**;带 url 的旧用法行为零变化。
173
+
172
174
  ### 看一个页面加载了什么
173
175
 
174
176
  > 你:「这页加载了哪些第三方跟踪?」 → 资源列表 + 跟踪域名计数
@@ -219,7 +221,7 @@ macOS 上能控 Finder / Mail / Safari / Notes / 系统设置等任何原生 app
219
221
 
220
222
  ## 安装
221
223
 
222
- **当前版本 v1.23.0**(更新日志见本节末尾折叠块)。
224
+ **当前版本 v1.25.0**(更新日志见本节末尾折叠块)。
223
225
 
224
226
  前提:Node.js ≥ 20 + Claude Code(或任何支持 MCP 的客户端)。
225
227
 
@@ -234,8 +236,11 @@ claude mcp add lasso -- npx -y lasso-mcp
234
236
  **给其它 AI 工具当确定性渲染浏览器(渲染档)**:如果你在用 media-gen-mcp 这类要求「同一输入必须产出同一像素」的渲染工具,装好 lasso 后只需一行 `npx -y lasso-mcp render-chrome --ensure`——它会在 9224 端口拉起一个确定性 headless Chrome(冻结旗标快照 + 独立临时 profile),输出消费方直连用的 `wsEndpoint`;**消费方进程被强杀浏览器也照常存活**(生命周期归属 lasso),空闲 10 分钟自动回收并连带清理 profile(`LASSO_RENDER_IDLE_MS` 可调)。配套:`render-chrome --status` 自省 / `--stop` 收尾(认 `LASSO_RENDER_PORT`:显式设 port 只收该 port,未设=全部 render 记录,非法值 exit 1;全局收口仍可用 `chrome-stop --modes render`)/ `render-chrome doctor [--clean]` 孤儿与陈年 profile 清理(默认只报告;认 `LASSO_RENDER_PORT`,同 `--stop`:显式设 port 只判该 port,未设=全扫,非法值 exit 1;touch 心跳 10 分钟内的在用实例豁免不判孤儿)。**同机多 agent 并行验收**(多个 agent 各自清场会互杀对方实例)需配三 env 契命名空间隔离:`LASSO_RENDER_PORT` + `LASSO_LAUNCHED_CHROMES_PATH` + `LASSO_RENDER_GUARDIAN_PID_PATH`,配齐后并行互不可见,配方与清场纪律见 [doc/渲染档-并行验收隔离配方.md](doc/渲染档-并行验收隔离配方.md)。
235
237
 
236
238
  <details>
237
- <summary>📋 更新日志(v1.8 → v1.18.6,点开看每版改了什么)</summary>
239
+ <summary>📋 更新日志(v1.8 → v1.23.0,点开看每版改了什么)</summary>
238
240
 
241
+ - **v1.24.0(未发版,本批已合入工作树)**:🔴 **行为变更(BUG-06,12h 幽灵常驻事故根治,doc/bugs/06)**——① `--idle-ms 0` / `LASSO_LAUNCH_IDLE_MS=0` 语义从「永不回收」改为「**无活动 24 小时硬顶回收**」(自最近活动起算——lasso browse 自动落盘 touch 文件或外部 `touch ~/.cache/lasso/chrome-touch-<端口>` 续命,**在用永不触发**;`LASSO_LAUNCH_HARD_CAP_MS` 新键可调/禁);真·无限常驻 = `--idle-ms 0 --no-hard-cap` 双旗,或部署级 `LASSO_LAUNCH_HARD_CAP_MS=0`。visible/用户认领(chrome-show)档豁免不变(r2:收割判定与执行间隙落下的认领同样豁免——reaper 默认收割出口在 kill 时刻重估 userTakenAt,对抗复审 P2 TOCTOU 闭口);渲染档零变化(`LASSO_RENDER_IDLE_MS` 独立,INV-94 tripwire 钉死)。**升级瞬间存量超龄 idle-0 记录将被首个收割 tick(≤15s)回收**——属预期(该类实例即事故幽灵形态);依赖 0=无限且不 touch 的 nohup 配方请迁移(TROUBLESHOOTING §9.3 / KEY-GUIDE)。② 跨进程活动真源(A-7):`lasso browse` 活动现自动落盘 touch 文件——执守收割宿主可见(此前只写 server 进程内存,双宿主并时有 24h/30min 误杀盲区,同型同修);③ 超龄可观测(B):chrome-status/doctor 对硬顶过半(>50%)的在案实例持续给出 warn 咨询块(预期回收时刻 + touch 续命法 + 用户 chrome-stop 出口),零新增 kill/指令面;④ 全局 `LASSO_LAUNCH_IDLE_MS=0` 下 reaper 以 cap-only 模式运行(D2——不再连硬顶兜底一起拆)。对抗复审 r1 两项 P1 采纳(N1 跨进程盲区 / N2 缺省反转 0 下沉装配层);r2 闭 reaper TOCTOU(P2——默认收割出口 kill 时刻对 userTakenAt 重估);新不变量 INV-94。
242
+ - **v1.25.0**:当前页截图批(BUG-07,novel-engine 动画取证驱动)——`action=screenshot` **省略 url 即对当前受管页面直接截屏**(独立 `screenshot` 工具同义):此前两条截图路径都必经重导航,把 evaluate 建立的页面现场(冻结的 CSS 动画中间帧、注入的临时 DOM)全部洗掉——状态敏感截图不可达,消费方被迫借道第二套浏览器栈。现在同会话四通道闭环:带 url 建会话 → `evaluate` 冻结/注入 → 无 url 截当前帧 → `console` 取证。无活动会话(冷通道/页面已关/被自愈换页)显式报错带指引,**绝不静默新开、绝不截空白页伪造状态**;带 url 的旧行为零变化(回归锚钉死);对抗三轮 clean——6 变异全红 + 会话漂移攻击真机双通道零穿透(并发竞态拒/弹窗钉受管页/静默换页 invalidate 封口)。上游调研定谳:截图的业界普适语义就是「截当前状态」——本版把 lasso 对齐回行业标准。
243
+ - **v1.24.0**:幽灵常驻根治批(BUG-06,用户 12 小时实例实锤驱动)——① 🔴 **行为变更**:`launch-chrome --idle-ms 0` 的语义从「永不回收」改为「**不自收,但 24 小时硬顶保命**」——无窗口的隐藏实例从此不可能无限常驻成「看不见也关不掉的幽灵」(本轮用户实锤:一个 12 小时实例,Dock/菜单栏不可见无从关闭)。要真正无限需再显式 `--no-hard-cap`(**双重意图**,防脚本顺手传 0);部署级可用 `LASSO_LAUNCH_HARD_CAP_MS` 调整或 `=0` 全禁;失效方向恒安全(非法值回退有顶)。旧脚本若依赖「0=无限」请加 `--no-hard-cap`。② **跨进程活动真源**:硬顶收割前看 touch 文件心跳(任何会话/任何消费方还在用 → 不误杀);userTakenAt / visible 豁免天然继承(kill 时刻二次重估,竞态窗口封口)。③ **超龄可观测**:`doctor` / `chrome-status` 对过半硬顶的实例提前 warn(不新增任何 kill 面)。真机验收六项全 PASS + 对抗两轮 clean(三变异全红 / 误杀全景真活替身重放零误伤 / 渲染档零交叉)。
239
244
  - **v1.23.0**:本地文件与排障能力批(BUG-05,novel-engine 实测驱动)——① **`LASSO_ALLOW_FILE_FROM` file:// 目录白名单**:测本地单文件 HTML(离线产物验收的标配场景)此前被 SSRF 守卫一刀切拦——现在设 `LASSO_ALLOW_FILE_FROM=/你的项目目录` 后 `browse_*` 可导航/快照/抽取/截图**指定目录子树内**的本地文件(默认仍拒、零配置零行为变化;`../` 穿越、symlink 逃逸、目录前缀伪造、末段 symlink 写逃逸全封口——读面 18+写面 10 攻击变体零逃逸);② 新 **`console` action**:直接读当前页自上次导航以来的 console 消息(按严重度过滤 + 最近 N 条)——渲染类缺陷排查不再需要注入 window.onerror 绕路;③ **`ignored_options` 诚实契约**:你传了但当前 action 用不上的 options 键会在响应里如实列出(不再静默丢弃——novel-engine 实测踩到的「worked 但截图没生成」根因之一);顺带明确:navigate+截图一步走**本就是 `action=screenshot` 的行为**(先导航再截图);④ **`LASSO_SCREENSHOT_DIR` 截图写根约束**:自定义截图落盘路径默认拒(防任意写盘),opt-in 且限指定目录子树内。对抗两轮 clean:6 变异全红+SSRF 守卫本体零削弱证明+消费方场景(真实单文件 HTML http/file:// 双路径)真机全通。
240
245
  - **v1.22.1**:审查与稳定性批——① 消费方深检回告六项收口:给 cc-control 的 hook 硬拦样例**回修三处缺陷并落仓自带 17 向量测试**(样例必落仓、文档只指路——交付物质量制度成型)、doctor 端口探测 detail 文案四形态如实化、版本对齐测试泛化(断言读 package.json 单一真源,不再逐版手改)、`npm run gate` 门禁固化(显式计数断言防管道吞退出码);② 03 审查测试清单全面审查(四轨六维+Gauntlet 验证+双盲对抗 clean);③ 三个时序 flake 收口(墙钟边界改固定时钟注入/sleep 赌落盘改轮询/阈值贴判别面)——全量连跑 5 轮 + gate 全绿实证。
241
246
  - **v1.22.0**:归属鉴定与通道自愈批(BUG-04,消费方实战驱动)——① 新子命令 **`chrome-status [--port N]`**:把「这个端口被谁占着、我能不能动它」的鉴定权从 AI 手里收走——lasso 直接十分类(账本僵尸/陈留/疑似用户资产…),输出人类决策文案 + 机器可读指令(`allowed_commands` / `must_report`——AI 拿到「疑似用户资产」分类时被明确告知**零允许操作、必须上报你**)+ 可直接粘贴的上报包;配套 `chrome-stop --zombie-gate` 唯一机器代杀出口(台账在案+指纹对+kill 时刻重读台账三重门)。旧命令行为零变化。② 修复 `browse_logged_in` **选中页死锁**(P1 通道级):根因是上游引擎的结构性缺陷——选中页一旦被关闭,后续 navigate/evaluate/snapshot 永久报错只能重启;现在自动检测楔死签名并后台自建页接管自愈,修不掉时如实前缀上报。③ `evaluate` 兼容 `(async () => {...})()` 形态(此前报 fn is not a function);④ 调用方自己写错 JS 不再拉响备用通道(脚本错误与通道错误分类);⑤ doctor 端口探测措辞如实化(「HTTP 404」→ 四形态真实归因)。对抗两轮 clean:5 变异全杀 + 真实用户 Chrome 主权实机验证 + 上报包武器化测试不破防。给重度自动化用户的配套:PreToolUse hook 硬拦样例(拦 AI 经 Bash kill 浏览器)见 doc/bugs/04-附录E。
@@ -328,7 +333,7 @@ lasso launch-chrome
328
333
  以后对 Claude 说「打开我已登录的 Jira」就行。想再看窗口随时 `lasso chrome-show`(可逆,登录态不动)。
329
334
 
330
335
  - 默认**零窗口静默**干活、不抢焦点、永远静音;想看着它干加 `--mode visible`
331
- - 命令行拉起的 Chrome **默认 30 分钟无活动自动收**(BUG-03 A1——「有活动就活,无消费者到期自动收」;外部工具 `touch ~/.cache/lasso/chrome-touch-<端口>` 一行续命;长会话显式 `--idle-ms 0` 永不自动收),手动关随时 `lasso chrome-stop`
336
+ - 命令行拉起的 Chrome **默认 30 分钟无活动自动收**(BUG-03 A1——「有活动就活,无消费者到期自动收」;外部工具 `touch ~/.cache/lasso/chrome-touch-<端口>` 一行续命;长会话显式 `--idle-ms 0` 不做 idle 回收、但**无活动 24 小时硬顶回收**(BUG-06),真·无限须 `--idle-ms 0 --no-hard-cap` 双旗),手动关随时 `lasso chrome-stop`
332
337
  - 它在你 Chrome 里开的 tab,任务后说 `admin {action:"tab_restore", reason:"完成"}` 恢复原列表(server 退出也会自动做)
333
338
 
334
339
  > 🔴 **红线**:2FA / 验证码 / CAPTCHA——Lasso 不替你解,你在窗口里手动过一次。
@@ -339,7 +344,7 @@ lasso launch-chrome
339
344
  - v1.8 起默认用 Lasso 独立 profile(Chrome 136+ 禁止对默认 profile 开调试端口,老办法会秒退);复用已有 profile 用 `lasso launch-chrome --profile <目录>`。
340
345
  - 启动后自动探活调试端口,Chrome 没起来 / 端口被占会明确报错,不假报成功。
341
346
  - 自动关阈值:`LASSO_LAUNCH_IDLE_MS`(默认 60000;`300000` 回退 5 分钟;`0` 禁用)。单次长任务放行:`--idle-ms 3600000`。BUG-03 轮起**命令行显式拉起默认 `--idle-ms 1800000`(30 分钟)**——server 退出不再连坐任何 CLI 拉起的 Chrome(归属过滤),执守进程(server 不在也活着)负责到期收割;显式配置 env / config.json 仍最高优先。
342
- - **外部 CDP 消费者续命**(v1.18.5,BUG-03 口径修订):其他工具(chrome-devtools-mcp、自动化脚本等)直连这条 Chrome 的调试端口时,一行 `touch ~/.cache/lasso/chrome-touch-<端口>` 即视为「刚用过」(承诺口径 = **在用就一直活**,无信号才到期收);不需要常驻(hidden 档冷启动实测 1.5s 内 CDP 可用),一整段会话要用就拉起时配 `--idle-ms 0`(显式退出自动收)或定期 touch。
347
+ - **外部 CDP 消费者续命**(v1.18.5,BUG-03 口径修订;BUG-06 起 lasso browse 自动落盘同一 touch 文件——续命信号跨收割宿主可见):其他工具(chrome-devtools-mcp、自动化脚本等)直连这条 Chrome 的调试端口时,一行 `touch ~/.cache/lasso/chrome-touch-<端口>` 即视为「刚用过」(承诺口径 = **在用就一直活**,无信号才到期收);不需要常驻(hidden 档冷启动实测 1.5s 内 CDP 可用),一整段会话要用就拉起时配 `--idle-ms 3600000`(到点自动收)或定期 touch——`--idle-ms 0` 现带 24h 硬顶(BUG-06),不再是无上界常驻。
343
348
  - 登录后自动收窗:`LASSO_AUTO_HIDE_AFTER_LOGIN`(默认 false;开启后只在「见过登录页 → 登录页消失 → 等 10 秒 → Claude 没在用」四关全过时才收,收错方向保守——拿不准就不收)。等待时长 `LASSO_AUTO_HIDE_AFTER_LOGIN_DELAY_MS`(默认 10000)。只在 server 会话运行期间生效(CLI 单独 `launch-chrome` 没有调度器,仍走手动 `chrome-hide`)。
344
349
  - 无头浏览器空闲 5 分钟自动回收(`LASSO_HEADLESS_IDLE_MS` 可调/禁用)。
345
350
  - 诚实边界:单独跑 `lasso launch-chrome`(不经 server)没有 idle 自动关,出口是 `chrome-stop`;它起的 Chrome 在 Dock / 任务栏会多一个图标(浏览器有头进程的注册行为,lasso 控制不了),要零图标用 `browse_headless`;`browse_logged_in` 连**你自己开的可见 Chrome** 时会在你的 Chrome 里临时开一个后台 tab 干活(不抢焦点不发声,结束自动关,v1.17.2 起你的 tab 一律不被改写)——但你的 Chrome 本身不静音(lasso 不改写你的浏览器参数),浏览到自动播放页面会真出声;`desktop` 模拟真人键鼠,设计上就占用物理键鼠,没有静默形态。
@@ -75,7 +75,13 @@ export declare abstract class BrowseChannel extends UiChannel {
75
75
  * INV-12:两条路径都经 withOperation() ALS 包裹(请求级隔离 + StateStore.epoch 派生)。
76
76
  * 永不抛异常——所有失败路径走 InteractResult。
77
77
  */
78
- browse(url: string, action: string, options: BrowseOptions): Promise<InteractResult<BrowseResult>>;
78
+ browse(url: string | undefined, action: string, options: BrowseOptions): Promise<InteractResult<BrowseResult>>;
79
+ /**
80
+ * BUG-07 决议 A⁺(§5.2②):url 省略 + 非 current-page action(含 steps 链)的
81
+ * 显式拒(与 unknown_action 同族——「策略确定性拒」early-return,不 throw:
82
+ * throw 形态会被 classifyBrowseError 落 unknown 触发熔断污染级联)。
83
+ */
84
+ private urlRequiredResult;
79
85
  /**
80
86
  * v0.2 单 action 路径(原 browse() 实装,零行为变更;仅迁出便于 browse() 入口分流)。
81
87
  */
@@ -100,6 +106,10 @@ export declare abstract class BrowseChannel extends UiChannel {
100
106
  * = 上游新开空白页,L3 实证:此时单发 extract/snapshot 返 about:blank 空内容
101
107
  * + outcome=worked)+ url 非占位 about:blank(forest 调度器缺 subtitle 时的
102
108
  * 兜底值,无可导航目标)→ 返 true 需先导航。
109
+ *
110
+ * BUG-07 决议 A⁺(§5.2③):签名放宽 url: string | undefined 并前置
111
+ * url !== undefined 守卫——防 undefined 走 `url !== "about:blank"` 求值出
112
+ * true 的隐性边(对抗复审自查项)。
103
113
  */
104
114
  private needsFreshPageNav;
105
115
  /**
@@ -108,6 +118,23 @@ export declare abstract class BrowseChannel extends UiChannel {
108
118
  * 不构成共享 mutable state 耦合面。
109
119
  */
110
120
  private readonly navSeenClients;
121
+ /**
122
+ * BUG-07 决议 A⁺(§5.3):current-page 模式的会话真源——最后一次成功导航的
123
+ * client(可空强引用)。与 navSeenClients **同两行写点、单写者**
124
+ * (dispatchAction 现有两处 add),只增只读判定(R-INT-07 教训:不构成
125
+ * 多消费者 mutable state 耦合面)。唯一例外写点 = 自愈成功路径的
126
+ * invalidateCurrentPageSession()(三处调用点,LoggedInChannel;写者仍是
127
+ * 通道自身 protected 方法)。守卫语义 =「client 身份 ∧ 未被 heal 换页」。
128
+ */
129
+ private lastNavigatedClient;
130
+ /**
131
+ * BUG-07 决议 A⁺(§5.3 r1 铁则 (b)):current-page 会话失效通知。
132
+ * 自愈成功路径(楔死 heal 层 1 new_page / 层 2 respawn / P6 恢复)都会把
133
+ * 「当前受管页面」换成新空白页——client 身份可能不变(层 1 返回原 client),
134
+ * 必须显式置 null 才能让 current-page 截图的守卫拒绝(绝不静默截 about:blank
135
+ * 伪造状态)。navSeenClients(FRESH_PAGE_NAV golden)不动。
136
+ */
137
+ protected invalidateCurrentPageSession(): void;
111
138
  /**
112
139
  * P6(v1.18.1,得到实战问题集 P6):"No page selected" 零页面自愈钩子。
113
140
  * 默认 false(HeadlessChannel 上游自管浏览器无 0-page 形态,行为零变化);
@@ -255,6 +282,10 @@ export declare function truncatePreviewKeepingRefs(s: string): string;
255
282
  /**
256
283
  * BUG-05 决议 D1:计算「传入但该 action 未消费」的 options 键(导出供测试)。
257
284
  * 只对 worked 出口调用(didnt/unknown 路径不标——错误已自解释)。
285
+ *
286
+ * BUG-07 决议 A⁺(§5.2③):mode="current_page"(url 省略的 screenshot)时
287
+ * 从消费集派生剔除 NAV_ONLY_OPTION_KEYS——该模式下无导航,no_cache 只经
288
+ * doNavigate 消费 → 传入必须出现在 ignored_options(INV-91 精神:禁死键豁免)。
258
289
  */
259
- export declare function computeIgnoredOptions(action: string, options: BrowseOptions): string[];
290
+ export declare function computeIgnoredOptions(action: string, options: BrowseOptions, mode?: "current_page"): string[];
260
291
  //# sourceMappingURL=BrowseChannel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"BrowseChannel.d.ts","sourceRoot":"","sources":["../../src/channels/BrowseChannel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BrowseChannel.d.ts","sourceRoot":"","sources":["../../src/channels/BrowseChannel.ts"],"names":[],"mappings":"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"}
@@ -189,24 +189,41 @@ export class BrowseChannel extends UiChannel {
189
189
  * 永不抛异常——所有失败路径走 InteractResult。
190
190
  */
191
191
  async browse(url, action, options) {
192
+ // --------------------------------------------------------------
193
+ // BUG-07 决议 A⁺(doc/bugs/07 §5.2②,2026-09-10):url 省略分发语义门。
194
+ // browse() 是唯一入口(tools / forest dispatcher / serp 全经此)——steps
195
+ // 分流**之前**拒绝,防 steps 链在隐式 current-page 上跑出未定义语义。
196
+ // 有 url → 全部现状路径字节级不变(§5.7 向后兼容证明义务 2)。
197
+ // --------------------------------------------------------------
198
+ if (url === undefined && !CURRENT_PAGE_ACTIONS.has(action)) {
199
+ return this.urlRequiredResult(action);
200
+ }
201
+ if (url === undefined &&
202
+ Array.isArray(options.steps) &&
203
+ options.steps.length > 0) {
204
+ return this.urlRequiredResult(action);
205
+ }
192
206
  // --------------------------------------------------------------
193
207
  // v0.3 入口分流:options.steps 非空 → StepEngine.runChain
194
208
  // --------------------------------------------------------------
195
209
  if (Array.isArray(options.steps) && options.steps.length > 0) {
210
+ // 入口门已拒「url 省略 + steps」(上方第二个 guard)——steps 分支内 url
211
+ // 不变量为 string;TS 无法跨复合条件收窄,显式收紧一次。
212
+ const stepsUrl = url;
196
213
  // resourceKey:channel 全名 + url(粗粒度隔离;StepEngine 内每个 step 独立 stateId)
197
- const resourceId = `${this.name}:${url}`;
214
+ const resourceId = `${this.name}:${stepsUrl}`;
198
215
  // epoch = 0(v0.3 不接 ResourceScheduler;parse3 §4.3 推迟到 v0.5+)
199
216
  return withOperation(resourceId, 0, async () => {
200
217
  // W1-DEF-2b(v1.8):action="navigate" + steps 时**先导航再跑链**
201
218
  // (此前 steps 分支丢弃 action,链跑在 about:blank 上——expectPoll 在
202
219
  // 空白页 30s 全 false,wave2 smoke 实证)。链内首 step 为 navigate 的
203
220
  // 旧范式(U-03-1)不受影响(StepEngine 自行处理)。
204
- if (action === "navigate" && url) {
221
+ if (action === "navigate" && stepsUrl) {
205
222
  const nav = this.actionDispatch.get("navigate");
206
223
  if (nav) {
207
224
  try {
208
225
  const c = await this.getMcpClient();
209
- await nav(c, url, options);
226
+ await nav(c, stepsUrl, options);
210
227
  }
211
228
  catch (e) {
212
229
  // 导航失败(404 / DNS / 落盘类 didnt)→ 整链诚实终止
@@ -222,7 +239,7 @@ export class BrowseChannel extends UiChannel {
222
239
  }
223
240
  }
224
241
  }
225
- const chain = await this.runChain(url, options.steps,
242
+ const chain = await this.runChain(stepsUrl, options.steps,
226
243
  // v1.18.2(doc/governance/10 F3+Y1):budget_ms 显式放宽(钳制 600s;缺省 DEFAULT 120s)
227
244
  clampChainBudgetMs(options.budget_ms));
228
245
  return this.wrapChainResult(chain);
@@ -231,9 +248,25 @@ export class BrowseChannel extends UiChannel {
231
248
  // --------------------------------------------------------------
232
249
  // v0.2 单 action 路径(保留;INV-12 包裹)
233
250
  // --------------------------------------------------------------
234
- const resourceId = `${this.name}:${url}`;
251
+ const resourceId = `${this.name}:${url ?? CURRENT_PAGE_URL_LITERAL}`;
235
252
  return withOperation(resourceId, 0, async () => this.browseSingle(url, action, options));
236
253
  }
254
+ /**
255
+ * BUG-07 决议 A⁺(§5.2②):url 省略 + 非 current-page action(含 steps 链)的
256
+ * 显式拒(与 unknown_action 同族——「策略确定性拒」early-return,不 throw:
257
+ * throw 形态会被 classifyBrowseError 落 unknown 触发熔断污染级联)。
258
+ */
259
+ urlRequiredResult(action) {
260
+ return {
261
+ outcome: "didnt",
262
+ data: null,
263
+ served_by: this.name,
264
+ fallback_used: false,
265
+ retrieval_method: this.retrievalMethod(),
266
+ error: `url_required_for_action:${action}`,
267
+ hint: "url is optional only for action=screenshot (current-page mode); pass an explicit http(s) url",
268
+ };
269
+ }
237
270
  /**
238
271
  * v0.2 单 action 路径(原 browse() 实装,零行为变更;仅迁出便于 browse() 入口分流)。
239
272
  */
@@ -253,20 +286,27 @@ export class BrowseChannel extends UiChannel {
253
286
  * act(@pN) 经 InteractDispatcher 也走本路径,同享此保证。
254
287
  */
255
288
  async dispatchAction(c, action, url, options, handler) {
256
- if (NAV_FIRST_ACTIONS.has(action) ||
257
- this.needsFreshPageNav(c, action, url)) {
289
+ // BUG-07 决议 A⁺(§5.2③):加法守卫——url defined 时与旧代码求值全等
290
+ //(current-page 已在 browseSingle 早退,url === undefined 永不进导航分支)。
291
+ if (url !== undefined &&
292
+ (NAV_FIRST_ACTIONS.has(action) ||
293
+ this.needsFreshPageNav(c, action, url))) {
258
294
  const nav = this.actionDispatch.get("navigate");
259
295
  let navFinalUrl;
260
296
  if (nav)
261
297
  navFinalUrl = (await nav(c, url, options)).final_url;
262
298
  this.navSeenClients.add(c);
299
+ this.lastNavigatedClient = c; // BUG-07 A⁺:与 navSeenClients 同两行写点(单写者)
263
300
  const partial = await handler(c, url, options);
264
301
  return { ...partial, final_url: partial.final_url ?? navFinalUrl };
265
302
  }
266
- const partial = await handler(c, url, options);
303
+ // current-page 模式:ActionHandler 契约收 string——传字面量(handler 均不读)
304
+ const partial = await handler(c, url ?? CURRENT_PAGE_URL_LITERAL, options);
267
305
  // navigate 自身成功后才标记会话已导航(失败不标——下次 snapshot/extract 仍门控导航)
268
- if (action === "navigate")
306
+ if (action === "navigate") {
269
307
  this.navSeenClients.add(c);
308
+ this.lastNavigatedClient = c; // BUG-07 A⁺:同上,第二写点
309
+ }
270
310
  return partial;
271
311
  }
272
312
  /**
@@ -274,9 +314,14 @@ export class BrowseChannel extends UiChannel {
274
314
  * = 上游新开空白页,L3 实证:此时单发 extract/snapshot 返 about:blank 空内容
275
315
  * + outcome=worked)+ url 非占位 about:blank(forest 调度器缺 subtitle 时的
276
316
  * 兜底值,无可导航目标)→ 返 true 需先导航。
317
+ *
318
+ * BUG-07 决议 A⁺(§5.2③):签名放宽 url: string | undefined 并前置
319
+ * url !== undefined 守卫——防 undefined 走 `url !== "about:blank"` 求值出
320
+ * true 的隐性边(对抗复审自查项)。
277
321
  */
278
322
  needsFreshPageNav(c, action, url) {
279
- return (FRESH_PAGE_NAV_ACTIONS.has(action) &&
323
+ return (url !== undefined &&
324
+ FRESH_PAGE_NAV_ACTIONS.has(action) &&
280
325
  url !== "about:blank" &&
281
326
  !this.navSeenClients.has(c));
282
327
  }
@@ -286,6 +331,25 @@ export class BrowseChannel extends UiChannel {
286
331
  * 不构成共享 mutable state 耦合面。
287
332
  */
288
333
  navSeenClients = new WeakSet();
334
+ /**
335
+ * BUG-07 决议 A⁺(§5.3):current-page 模式的会话真源——最后一次成功导航的
336
+ * client(可空强引用)。与 navSeenClients **同两行写点、单写者**
337
+ * (dispatchAction 现有两处 add),只增只读判定(R-INT-07 教训:不构成
338
+ * 多消费者 mutable state 耦合面)。唯一例外写点 = 自愈成功路径的
339
+ * invalidateCurrentPageSession()(三处调用点,LoggedInChannel;写者仍是
340
+ * 通道自身 protected 方法)。守卫语义 =「client 身份 ∧ 未被 heal 换页」。
341
+ */
342
+ lastNavigatedClient = null;
343
+ /**
344
+ * BUG-07 决议 A⁺(§5.3 r1 铁则 (b)):current-page 会话失效通知。
345
+ * 自愈成功路径(楔死 heal 层 1 new_page / 层 2 respawn / P6 恢复)都会把
346
+ * 「当前受管页面」换成新空白页——client 身份可能不变(层 1 返回原 client),
347
+ * 必须显式置 null 才能让 current-page 截图的守卫拒绝(绝不静默截 about:blank
348
+ * 伪造状态)。navSeenClients(FRESH_PAGE_NAV golden)不动。
349
+ */
350
+ invalidateCurrentPageSession() {
351
+ this.lastNavigatedClient = null;
352
+ }
289
353
  /**
290
354
  * P6(v1.18.1,得到实战问题集 P6):"No page selected" 零页面自愈钩子。
291
355
  * 默认 false(HeadlessChannel 上游自管浏览器无 0-page 形态,行为零变化);
@@ -345,8 +409,39 @@ export class BrowseChannel extends UiChannel {
345
409
  error: `unknown_action:${action}`,
346
410
  };
347
411
  }
412
+ // ==============================================================
413
+ // BUG-07 决议 A⁺(doc/bugs/07 §5.2-5.3,2026-09-10):current-page
414
+ // 守卫块——与 unknown_action / upstream_unsupported 同位(dispatchAction
415
+ // 之前、try 之外)。错误契约必须是本函数的 early-return(完整
416
+ // InteractResult 原样直达调用方),**禁止以 dispatchAction throw 形态
417
+ // 落地**(throw → classifyBrowseError 无规则 → unknown → 熔断污染级联
418
+ // + fallback_exhausted 吞 hint——§5.3 r1 发现 ② 的三级劣化)。
419
+ // ==============================================================
420
+ /** no-session 完整 InteractResult(level-1 / level-2 / 自愈禁令共用形状)。 */
421
+ const noSessionResult = () => ({
422
+ outcome: "didnt",
423
+ data: null,
424
+ served_by: this.name,
425
+ fallback_used: false,
426
+ retrieval_method: "current_page_no_session",
427
+ error: "no_active_session:current_page_screenshot",
428
+ hint: "current-page screenshot requires an active session on this channel: run any url-bearing action (navigate / snapshot / evaluate / ...) first, or pass url for the one-step navigate+shoot form. Refusing to shoot a blank page — the previous page is gone (closed / rotated by self-heal / cold channel), which would fabricate state.",
429
+ });
430
+ // level-1 pre-check(不获取 client):冷通道**不 spawn 浏览器子进程**就完成
431
+ // 拒绝——「不静默新开」落到实处(headless 不拉 Chromium;logged_in 不拉 npx)。
432
+ if (url === undefined && this.lastNavigatedClient === null) {
433
+ return noSessionResult();
434
+ }
348
435
  try {
349
436
  const c = await this.getMcpClient();
437
+ // level-2 复核(获取 client 后):`lastNavigatedClient === c && navSeenClients
438
+ // .has(c)` 不满足 → 同错误契约。封 respawn 边(上游子进程重启换 McpClient
439
+ // 实例=新浏览器空白页)和 heal 换页边(invalidation 置 null——同 client 但
440
+ // 选中页已被自愈换成新空白页,§5.3 r1 铁则 (b) 的穿透口全落这里)。
441
+ if (url === undefined &&
442
+ (this.lastNavigatedClient !== c || !this.navSeenClients.has(c))) {
443
+ return noSessionResult();
444
+ }
350
445
  this.touchKeepalive(); // v1.9:action dispatch 后保活(防 idle watchdog 误杀)
351
446
  // P10(v1.18.1,得到实战问题集 P10):上游工具缺失前置门(导航**之前**)。
352
447
  // 锁定的 chrome-devtools-mcp@1.7.0 实测不暴露 `pdf` 工具——此前单 action pdf
@@ -380,7 +475,16 @@ export class BrowseChannel extends UiChannel {
380
475
  // 失败方向:heal 失败 → `upstream_wedge_selected_page_closed` 透明前缀
381
476
  // (unknown);heal 成功但重试仍失败 → `upstream_wedge_unhealed`(同
382
477
  // unknown——fallback 到 headless 语义正确)。
478
+ //
479
+ // BUG-07 决议 A⁺(§5.3 r1 铁则 (a)):current-page 模式(url === undefined)
480
+ // **禁两条自愈重试**——选中页已死 = 受管状态定义性消亡,heal 出的新页
481
+ // (无论同 client 还是 respawn)对状态敏感截图毫无意义,重试的唯一可能
482
+ // 产出就是空白页伪造。铁则:仅在两条自愈分支内前置判定(不做成 catch
483
+ // 顶部的裸 if——那会把无关错误误报成 no-session,制造新的错误归因
484
+ // 伪造);url-present 路径的自愈重试逐字节不动(INV-89(c) 锚不破坏)。
383
485
  if (isUpstreamWedgeError(String(e))) {
486
+ if (url === undefined)
487
+ return noSessionResult(); // 铁则 (a):禁楔死自愈
384
488
  const healed = await this.healUpstreamWedge(c);
385
489
  if (healed === null) {
386
490
  throw new Error(`upstream_wedge_selected_page_closed:${String(e).slice(0, 200)}`);
@@ -399,8 +503,14 @@ export class BrowseChannel extends UiChannel {
399
503
  // silent bail)下所有页级调用抛 "No page selected"(getSelectedMcpPage)。
400
504
  // 自愈钩子(LoggedInChannel:CDP 预建 background tab + select_page)成功 →
401
505
  // 原样重试一次;失败/不支持 → 原错误路径(classify 落 unknown)。
402
- NO_PAGE_SELECTED_RE.test(String(e)) &&
403
- (await this.recoverNoPageSelected(c))) {
506
+ NO_PAGE_SELECTED_RE.test(String(e))) {
507
+ // BUG-07 铁则 (a) 同款:current-page 模式禁 P6 自愈(r1 发现 ③ 的
508
+ // 第三条同型穿透口——LoggedInChannel override 新建 about:blank tab +
509
+ // select_page 后同 client 重试,对 current-page 同样空白页伪造)。
510
+ if (url === undefined)
511
+ return noSessionResult(); // 铁则 (a):禁 P6 换页恢复
512
+ if (!(await this.recoverNoPageSelected(c)))
513
+ throw e;
404
514
  partial = await this.dispatchAction(c, action, url, options, handler);
405
515
  }
406
516
  else {
@@ -408,25 +518,30 @@ export class BrowseChannel extends UiChannel {
408
518
  }
409
519
  }
410
520
  // 写盘 + 短指针(v0.1 简化版;v0.3 升 StateStore LRU + stateId 反查)
521
+ // BUG-07 A⁺(§5.2③):current-page 模式 data.url = "current-page" 字面量
522
+ //(诚实+可 grep,不伪造 URL;解析真实页面 URL = 显式非目标,决议 §5.10 记档)
523
+ const effUrl = url ?? CURRENT_PAGE_URL_LITERAL;
411
524
  const stateId = randomUUID();
412
525
  const contentPath = await writeState(this.name, stateId, {
413
- url,
526
+ url: effUrl,
414
527
  action,
415
528
  ...partial,
416
529
  });
417
530
  // BUG-05 决议 D1(INV-91):传入但该 action 未消费的 options 键诚实标注
418
531
  //(空省略 = byte-identical;消费表单一真源 CONSUMED_OPTIONS)
419
- const ignored = computeIgnoredOptions(action, options);
532
+ // BUG-07 A⁺(§5.2③):模式感知——current-page 模式无导航,仅经导航消费的
533
+ // 键(no_cache)从消费集派生剔除(调用方传 no_cache 必须出现在标注里)。
534
+ const ignored = computeIgnoredOptions(action, options, url === undefined ? "current_page" : undefined);
420
535
  return {
421
536
  outcome: "worked",
422
537
  data: {
423
- url,
538
+ url: effUrl,
424
539
  action,
425
540
  state_id: stateId,
426
541
  content_path: contentPath,
427
542
  preview: truncatePreviewKeepingRefs(partial.preview ?? ""),
428
543
  title: partial.title,
429
- final_url: partial.final_url ?? url,
544
+ final_url: partial.final_url ?? effUrl,
430
545
  // v1.1(parse12 §3.3.1):markdown 档元数据透传(仅 extract_mode=markdown* 时 partial 含这些字段)
431
546
  ...(partial.byline ? { byline: partial.byline } : {}),
432
547
  ...(partial.citations ? { citations: partial.citations } : {}),
@@ -1441,6 +1556,15 @@ export function truncatePreviewKeepingRefs(s) {
1441
1556
  // browseSingle 先 navigate 再 dispatch(工具注释自 v0.5 起就承诺「URL → navigate + X」,
1442
1557
  // v1.8.1 补上真实导航)。wait/click 等保持原语义(作用于当前页)。
1443
1558
  const NAV_FIRST_ACTIONS = new Set(["network", "screenshot", "pdf"]);
1559
+ // BUG-07 决议 A⁺(doc/bugs/07 §5.0/§5.2,2026-09-10):url 省略时合法的
1560
+ // current-page action 集(对当前受管页面直接执行,零导航)。house pattern 同
1561
+ // NAV_FIRST_ACTIONS(顶级 const 预留增项位,未来 network/pdf 的 current-page
1562
+ // 形态=加一行,决议 §5.10 记档本期不开)。有 url → NAV_FIRST 照旧字节级不变。
1563
+ const CURRENT_PAGE_ACTIONS = new Set(["screenshot"]);
1564
+ // BUG-07 决议 A⁺(§5.2③):current-page 模式的 data.url / final_url 字面量
1565
+ //(诚实+可 grep,不伪造 URL;上游 take_screenshot 本就无 url 参数——本决议
1566
+ // 删除的是 lasso 自创组合形态里的导航,§5.5 零上游变更)。
1567
+ const CURRENT_PAGE_URL_LITERAL = "current-page";
1444
1568
  // review-r3 F3:URL 驱动但**会话语义敏感**的采集 action——只在会话页仍空白时
1445
1569
  // 先导航(needsFreshPageNav 门控),已导航会话作用于当前页。理由:
1446
1570
  // - snapshot/extract 是工具默认 action + descriptions 承诺 URL 定向——空白会话
@@ -1500,15 +1624,29 @@ const CONSUMED_OPTIONS = Object.freeze({
1500
1624
  /** network_include_bodies / network_timeout_ms 死键不入表(决议 r1) */
1501
1625
  network: ["network_filter", "no_cache"],
1502
1626
  });
1627
+ /**
1628
+ * BUG-07 决议 A⁺(§5.2③):仅经导航消费的 options 键(doNavigate 读 no_cache;
1629
+ * 无导航即死键)。current-page 模式下运行时从 CONSUMED_OPTIONS 表派生剔除
1630
+ * (消费表仍是单一真源,模式变体不落表)。
1631
+ */
1632
+ const NAV_ONLY_OPTION_KEYS = new Set(["no_cache"]);
1503
1633
  /**
1504
1634
  * BUG-05 决议 D1:计算「传入但该 action 未消费」的 options 键(导出供测试)。
1505
1635
  * 只对 worked 出口调用(didnt/unknown 路径不标——错误已自解释)。
1636
+ *
1637
+ * BUG-07 决议 A⁺(§5.2③):mode="current_page"(url 省略的 screenshot)时
1638
+ * 从消费集派生剔除 NAV_ONLY_OPTION_KEYS——该模式下无导航,no_cache 只经
1639
+ * doNavigate 消费 → 传入必须出现在 ignored_options(INV-91 精神:禁死键豁免)。
1506
1640
  */
1507
- export function computeIgnoredOptions(action, options) {
1641
+ export function computeIgnoredOptions(action, options, mode) {
1508
1642
  const consumed = CONSUMED_OPTIONS[action];
1509
1643
  if (!consumed)
1510
1644
  return Object.keys(options); // 未注册 action 由 unknown_action 早退拦下,此处防御
1511
1645
  const set = new Set(consumed);
1646
+ if (mode === "current_page") {
1647
+ for (const k of NAV_ONLY_OPTION_KEYS)
1648
+ set.delete(k);
1649
+ }
1512
1650
  return Object.keys(options).filter((k) => !set.has(k));
1513
1651
  }
1514
1652
  function classifyBrowseError(msg, _action) {
@@ -1533,6 +1671,14 @@ function classifyBrowseError(msg, _action) {
1533
1671
  // 不同下一步(前者改参数/配 env 可解,后者重试无益)。
1534
1672
  if (m.includes("screenshot_path_not_allowed"))
1535
1673
  return "didnt";
1674
+ // BUG-07 决议 A⁺(doc/bugs/07 §5.3 belt-and-braces):current-page 无会话 /
1675
+ // url-required 是**策略确定性拒** → didnt。铁则本使 no-session 以 browseSingle
1676
+ // early-return 落地(不经 throw);本规则防未来重构回退——若残余 throw 路径
1677
+ // 产出含该码的 unknown,此处兜底归 didnt(免熔断污染 + fallback_exhausted 吞
1678
+ // hint 的三级劣化)。**必须置于 upstream_wedge 规则之前**(防复合串
1679
+ // `upstream_wedge_unhealed:no_active_session:x` 被截胡落 unknown)。
1680
+ if (m.includes("no_active_session") || m.includes("url_required_for_action"))
1681
+ return "didnt";
1536
1682
  // E④(BUG-03 决议 E④):会话轮换("No page selected" 类)是**瞬态可重试**信号
1537
1683
  //(页面被轮换/清空,重 snapshot 即恢复)——显式归 unknown(可重试档)+
1538
1684
  // session_rotated 前缀供 agent 透明识别下一步(重 snapshot),不再落泛 unknown