Repository navigation
cmux-next: agents can screenshot the app again; browser pages are blacked out - #23983
Conversation
…ling) Leo-ask: ask-49f4ca2104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
…nted black 45fe246 made window, pane and tab screenshots person-only because the app's window capture cannot reproduce the browser host's secret mask. That left an agent with no way to see a terminal or a chat (Leo: "cmux only lets a person run the window screenshot action. doesnt seem right"). An automation run now gets the image with every browser tab area, every web view the app does not own and every Chromium page window painted opaque black (ScreenshotPrivacyMask), and without the overlay panels and other windows. A browser tab, or a pane showing one, is refused and points at the browser REPL. A person's run is unchanged. Leo-ask: ask-49f4ca2104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: |
|
Owned by cmux-mcp-surface on cosmic-gray · session |
|
Found 16 test failures on Blacksmith runners: Failures
...and 6 more test failures. View all on Blacksmith: run 39711313842, run 39711313898
|
|
|
…stays masked (failing) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
…ure, and nested page windows An app's run carries origin user during the person's gesture in it, so the mask now applies to every run but a person's own (no callerApp, agent chat or terminal). The mask walks visibleChildWindows, the same windows the capture includes, and is measured before the capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
…ws instead of sending the unmasked image Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
|
Review (subagent, correctness first): SHIP at 91e026a
|
|
Bead |
|
Red/green on the fleet (
Observed / Not run |
Conflicts resolved toward the base where it now owns the behavior: - Agent screenshots: the base's policy (#23983, browser pages blacked out, captures off main) replaces this branch's pane/tab screenshot changes. - Opening a session's chat in a pane: the base's agent.openSessionTab (#23492) replaces this branch's pane/workspace arguments on agent.openSessionWorkspace; agents chat new --pane|--workspace opens its tab through agent.openSessionTab, which now answers tab, pane, workspace and workspace_key. - cmux docs: both sets of guides kept; the chat guide drops the keywords the base's setup guides use (prompt, harness). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nshotPrivacyMask #23983 landed the same feature (agents screenshot the app, browser pages blacked out) five hours after this PR opened, so base now has a second, weaker mask. Resolved by keeping this PR's ScreenshotMask, which does everything ScreenshotPrivacyMask does and more, and deleting the duplicate: - the mask comes from the window's view tree, so a page a pane holds while its next tab paints, a sleeping page's picture and a page under no chrome view are covered; chats, Home views and every app page not in `shownAppPages` too; - it samples each run loop turn across the capture, retries an unsettled window and refuses rather than return pixels it could not mask; - `viaControlSocket` marks every socket run, so a caller claiming `origin: "user"` no longer gets the raw image (base's `applies(to:)` let it through). Kept from #23983, which this PR's `isPersonsRun` did not cover: a run that names a caller (`callerApp`, `callerAgentSession`, `callerTerminal`) is masked too, even carrying `user` from the person's gesture inside it. Takes back #23983's refusal of pane/tab screenshots over the socket when the pane or tab shows a browser: masking the page area whole keeps that guarantee without the refusal, which is this PR's point. `handlers.screenshot.browserNeedsREPL` went with it. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
… what an agent's capture shows now Review (claude subagent, resolution delta) found three things in the hand resolution: - The package split moved a `!isHidden` guard from the chat launch image onto whatever `Rules.area` returned, which includes a browser chrome's page area. Before the split the page area was added unconditionally. That is fail-open in exactly the race the type exists for: hide the page area, capture inside the stale frame, and its pixels ship unmasked. Inert today (nothing assigns `contentContainer.isHidden`), fixed anyway: the rule decides, and only the launch image is gated. - skills/cmux/SKILL.md and skills/cmux-browser/SKILL.md still described #23983's behaviour: "painted black" and "a pane showing a browser fails". Both are false here (gray, and masked rather than refused), and neither mentioned chats, Home views, app pages or the `masked` count. Rewritten to this head. - Base's `theAppsOwnPageViewIsNotMasked` had no replacement, and it covers the branch the split made indirect. `aListedAppPageAndItsWebViewAreNotCovered` takes it: a listed page is not covered, an unlisted one is, and a listed page's own web view is not masked for being a web view. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
Both independent reviews (Codex gpt-6-astra xhigh, and a Claude subagent on the resolution delta) called this blocking, and they are right. #23983 masked the whole BrowserChromeView; an earlier head of this PR masked only `chrome.pageArea`, on the reasoning that an agent can read the URL through the browser REPL anyway. That reasoning was never verified, and the address bar carries a URL that can hold a token or a one-time link, plus the page's title and badges. Weakening a landed gate needs a better argument than utility. `Rules.app.area` returns the chrome itself, not its page area. It stays an `area` rather than a `covers` so `read` still descends into the chrome and still marks the mask unsettled when a page's pixels have moved out from under it (aStrayWebViewIsMaskedAndAMovingPageIsUnsettled covers that). The two tests that named the page rect now name the chrome rect. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
…glass, and waits behind a busy main actor (#24240) * test: a window snapshot waits behind a busy main actor; a background toggle reaches the Home (red) debug.window_snapshot is now built by DebugWindowSnapshot.controlMethod (no behavior change) so a test can register it. Both tests fail on this commit: the snapshot times out at the control-plane deadline while the main actor is busy, and with no key window a child window in front takes the Chief Settings toggle instead of its window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: a window snapshot waits behind a busy main actor; a background toggle reaches the Home debug.window_snapshot keeps its own 30 s deadline instead of the 2 s control-plane one. On a capture mini that also runs builds and tests (cua-5 at load 30 to 44 on 14 cores) the wait for the main actor passed 2 s, the snapshot timed out, and pr-capture refused the capture for want of a still. The capture itself takes about 50 ms. With no key window, home.toggleChiefSettings resolves the frontmost visible window to the window it belongs to. Outside an active app AppKit lists Chromium page child window first, so a toggle from the CLI with cmux in the background (every capture slot) reached no Home and the popover never opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Composite a window with its popovers and menus in one image A window snapshot composited the window, then its overlay panels alone, then the menus over it. A glass popover composited alone blurs nothing and came out grey. The window, its child windows and the app's windows over it now go to the window server as one composite, so the glass blurs the window under it as on screen. Where a Chromium page image is painted over that, the overlays come back on top through a coverage mask. Only this app's windows are composited, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: a window screenshot awaits its fallback outside ?? Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the capture tool change to debug.window_snapshot Two open PRs (#23972, #23983) rework cmux window screenshot in AppWindowImage and WindowImageLayer. Leave both files as on base: the overlay cut is an NSWindow helper painted as an ordinary window layer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * debug.window_snapshot: a translucent overlay is not faded twice; cut overlays before the page awaits Where the overlays alone are translucent (a shadow, a fade, an edge), the cut now takes their own pixels instead of the composite times their alpha, which painted over a page image faded them twice. The overlays are cut right after the composite, before the page images' awaits, so a popover that opens meanwhile is not cut from a composite it is not in. The deadline test waits for the request to start before it blocks the main actor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a1603a-6462-72f0-a4b0-6937eb58e4c3&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…es come back masked (#23972) * cmux NEXT: agents can screenshot windows, panes and tabs; browser pages come back masked Socket runs of window.screenshot, pane.screenshot and tab.screenshot are no longer person-only. Any run the control socket carries (whatever origin it claims) gets the window without child or floating windows, with every browser page area, agent chat and conversation filled with an opaque mask read from the view tree on every run loop turn around the capture. A window that keeps animating or resizing, or a mask that cannot be painted, refuses the capture. A browser tab no window shows stays refused to automation. Design mode and the raw page captures stay person-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16 * screenshots: mask every app page not checked to be safe, Home and debug.toast runs Review findings on the masked socket capture: - App pages are masked unless listed in ScreenshotMask.shownAppPages (keys, changelog, icon picker, apps, diff, markdown, editor, files, debug settings), so Settings, Passwords, History, Home Channels, Agent Activity, the Chief pages and any new page are masked by default. - The Home list, dock, draft and transcript views are masked too (the list shows each conversation's latest message). - debug.toast's runs are socket runs: a screenshot it starts stays masked. - The handler's raw-or-masked choice is tested through ScreenshotHandlers.shown with real invocations, pane crop included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16 * screenshots: keep the settle timer alive with a synchronous lifetime fence withExtendedLifetime with an async body does not compile on CI's SDK. The header now names everything a socket capture masks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16 * test(screenshots): no nested #require; no shadowed locals in the pixel reader Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16 * screenshots: the mask engine moves to CmuxNextScreenshots; the app supplies the view rules The app-size guardrail refuses new logic in CmuxNextApp, and refuses appending it to an existing App file too. ScreenshotMask could not move whole: it names BrowserChromeView, AgentPaneView, the Home views, PageWebView and PaneContentView, which the App owns until the Workbench rows of the refactor plan move. So the geometry and the timing (the regions, the settle wait, the attempts, the run-loop Sampler, whether a view's pixels are where its frame says) go to CmuxNextScreenshots beside WindowImageLayer, and the view types stay in the App as ScreenshotMask.Rules.app in AppWindowImage.swift: covers, area, isAppPage, coversContents. CmuxNextScreenshots gains one dependency, CmuxNextWakeups, for the DemandTimer the settle wait uses. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue * screenshots: import CmuxNextScreenshots where ScreenshotMask is named The fleet compile at 018aac1 found the two files the move left without it: ScreenshotHandlers' `catch let failure as ScreenshotMask.Failure` and ScreenshotStrings' `reason(_ failure: ScreenshotMask.Failure)`. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue * screenshots: the page area is masked even when hidden; the skills say what an agent's capture shows now Review (claude subagent, resolution delta) found three things in the hand resolution: - The package split moved a `!isHidden` guard from the chat launch image onto whatever `Rules.area` returned, which includes a browser chrome's page area. Before the split the page area was added unconditionally. That is fail-open in exactly the race the type exists for: hide the page area, capture inside the stale frame, and its pixels ship unmasked. Inert today (nothing assigns `contentContainer.isHidden`), fixed anyway: the rule decides, and only the launch image is gated. - skills/cmux/SKILL.md and skills/cmux-browser/SKILL.md still described #23983's behaviour: "painted black" and "a pane showing a browser fails". Both are false here (gray, and masked rather than refused), and neither mentioned chats, Home views, app pages or the `masked` count. Rewritten to this head. - Base's `theAppsOwnPageViewIsNotMasked` had no replacement, and it covers the branch the split made indirect. `aListedAppPageAndItsWebViewAreNotCovered` takes it: a listed page is not covered, an unlisted one is, and a listed page's own web view is not masked for being a web view. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue * screenshots: a browser pane is masked whole again, its toolbar included Both independent reviews (Codex gpt-6-astra xhigh, and a Claude subagent on the resolution delta) called this blocking, and they are right. #23983 masked the whole BrowserChromeView; an earlier head of this PR masked only `chrome.pageArea`, on the reasoning that an agent can read the URL through the browser REPL anyway. That reasoning was never verified, and the address bar carries a URL that can hold a token or a one-time link, plus the page's title and badges. Weakening a landed gate needs a better argument than utility. `Rules.app.area` returns the chrome itself, not its page area. It stays an `area` rather than a `covers` so `read` still descends into the chrome and still marks the mask unsettled when a page's pixels have moved out from under it (aStrayWebViewIsMaskedAndAMovingPageIsUnsettled covers that). The two tests that named the page rect now name the chrome rect. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue * test(screenshots): the fixture builds its WebKitEngine the way the other app tests do `WebKitEngine()` has no such initializer: it takes `fileInput` and `stores`. The CI test-target build at 77e0ee3 (`cmux-next generated files`) found it; compile-check --app links no test target, so neither fleet compile could. `WebKitEngine(fileInput: NoFileInput(), stores: .inMemory())` is what NewTabHoverPreviewTests and AgentPageStaysInWindowTests already use. Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue * cmux NEXT screenshots: say why a hidden view adds no mask area The comments said a hidden browser chrome stays masked, which read() never did. The real guarantee is the sampler's: its first reading comes settle (100 ms) before the capture, so a view hidden before it is off the screen, and one hidden after it was in an earlier reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 * ci: list MaskedScreenshotTests among the display suites Its two pixel tests carry a display skip trait; display_suites.py --check requires the suite in display-suites.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 * fix(screenshots): mask browser tab strips Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e * fix(screenshots): retain launch masks alongside tab strips Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e * test(screenshots): verify saved frames remain masked in both crops Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e * fix(screenshots): mask unchecked native page tabs; refuse a capture whose areas move An independent review at 7d7ec32 blocked on two fail-open paths: - a native page tab (InternalPageView without a PageWebView: Phone and Mac Screen frames, Passwords, Stashed, a new page) was not masked, against the rule that an app page is masked until someone checks it. It is now covered whole unless its page id is in shownNativePages. - the sampler kept every reading's areas but never noticed an area that moved between readings, whose pixels can sit between two frames. Such a move now makes the mask unsettled, so the capture retries or refuses. Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(defaults): record this PR's screenshot defaults in the snapshot The generated-files check found schemas/defaults/cmux-next.json stale at 5d6d95a. Only this PR's 24 keys (the screenshot mask, handlers and strings, DebugToast.run) are updated, from the shared build's regenerated file; keys that are equally stale on the base are left for the base's own fix. Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo-ask: ask-49f4ca2104
Takes back part of 45fe246 ("fix(security): refuse unguarded app screenshots", lawrencecchen, 2026-10-20): it made
window.screenshot,pane.screenshotandtab.screenshotperson-only, so an agent had no way to see a terminal or a chat window. Leo: "cmux only lets a person run the window screenshot action. doesnt seem right". This PR keeps that fix's guarantee: no unmasked browser pixels reach automation.Summary
cmux window|pane|tab screenshotruns again. A person's run (palette, menu) is unchanged.ScreenshotPrivacyMask): these areas are painted opaque black, because the app can't reproduce the browser host's secret mask (vault, TOTP, typed secrets):WKWebViewnot held by the app's own agent chat (AgentPaneView) or page tabs (InternalPageView), so an unlisted web view fails closed;page.screenshot(), which masks secrets.cmuxandcmux-browserskills now say what an agent's screenshot shows.Risk
risk:high: this changes a security policy (a teammate's fail-closed screenshot gate). It takes back part of a commit pushed straight to feat-cmux-next, which has no PR number.
Changelog
Fixed: Agents can screenshot cmux windows, panes and terminal tabs again; browser pages in the image are blacked out.
Testing
Test first:
ActionOriginTests/appScreenshotsRunForEveryCaller: CLI, MCP, script and no-origin runs reach the handler as non-person runs.ScreenshotPrivacyMaskTests:Observed / Not run
user-origin run (during a person's gesture) was unmasked;A re-review of 238d980 passed (SHIP). A review of the follow-up 91e026a, where a masked capture whose masks cannot be painted now throws instead of sending the unmasked image, also passed (SHIP).
package-test-lane ActionOriginTests,ScreenshotPrivacyMaskTestson scratch refs (the red tests, then the fix, each with ci: the agent pane page ships the Inspector's ContextSection.css (base red since #23789) #23935's build-script patch for the baseContextSection.cssred). Queued on the fleet; results to be posted as a comment.cmux-next crash-class rules(an objc-selector probe onCEFFileDragRelay.swift, which cmux NEXT browser: files dragged in from Finder drop into the Chromium page #23790 added) and theChecks job structurestep ofcmux-next checks. This PR touches neither.contentView.Agent session: cosmic-gray
🤖 Generated with Claude Code