Cookie import consent: only the person's own click allows; out of screen capture; Computer Use refuses it

Merged
LA
5d ago
+586-78

Summary

The consent panel before cmux browser import-cookies reads another browser (the Allow/Deny panel of the agent cookie import) did not make sure that the person answered it. The independent review of #32983 found it (comment):

  • Screen capture showed the panel, so an agent that reads the screen (cmux Computer Use, any computer-use tool) could see Allow.
  • Any automation could press Allow: an Accessibility press, a programmatic click, or a click that another process posts (CGEventPost). A prompt-injected agent could ask for the import and then click Allow itself. The person's click was the only gate, and an agent could make that click.

This change makes Allow a person-only answer, as macOS privacy prompts and Chrome permission prompts do:

  • Out of screen capture. The panel window has sharingType = .none. A screenshot, a recording, screen sharing or an agent capture does not show it. The person still sees it on screen.

  • Only the person's own click allows. The panel reads the click from its own events (NSWindow.sendEvent), with the strict HID-system check of the app (PersonInputCheck, the same check as the join consent sheet: source pid 0 or the WindowServer, and the HID source state). Allow's button action does nothing. Thus an Accessibility press, performClick, the Space or Return key, a click that another process posts, and DEBUG automation (debug.key) do not allow.

  • No click from before the panel. A click does not count if it starts less than a double-click interval after one of these events (Chrome's InputEventActivationProtector):

    • the panel shows;
    • the panel moves (with its parent, or when an Accessibility client sets its position);
    • the person's own previous mouse-down. Clicks that another process posts do not delay the person.

    Thus a click aimed at the item that was there before the panel does not allow.

  • Palette and Import card runs. A run skips the panel only when two conditions are true. First, it is the person's own run (ActionInvocation.isPersonsOwnRun: the user origin, no caller app, agent or terminal, and not the socket). Second, it starts in the dispatch (InputDispatchContext) of a key or mouse-button event of the person from the last second. Each of these now gets the panel:

    • a click or key that a computer-use tool posts;
    • an Accessibility press, which runs outside any input dispatch;
    • a cmux app's run during the person's gesture in it.
  • Standard dialog buttons. Show Agent Chat, Deny and Allow are CmuxDialogButtonView. Deny has Escape. Allow has the default look and no key.

  • Computer Use refuses the window. The Computer Use helper refuses a target window that keeps itself out of screen capture (kCGWindowSharingState is none), by every target shape and in batches, for each app. (Shipped cmux builds were already refused by bundle; this also covers an allow-listed DEBUG tag and the protected windows of other apps.)

  • Deny, Escape, a closed window and the time limit still deny, by anyone.

The person-click logic of the join consent sheet (JoinConsentAskPanel, HQ#5121) moves into CmuxNextDesign as CmuxDialogPersonClick. Both prompts now use one checked implementation. The join sheet keeps its behavior (no activation delay), and its tests do not change.

Why not CmuxDialogCenter: the center lets automation answer a dialog (press, debug.dialog), and its window-modal overlay would block the window of the person for up to 180 seconds while an agent waits. The panel stays a non-activating child panel and uses the standard dialog buttons.

What still allows automation

  • An Accessibility client can still read the panel text (VoiceOver needs it). It cannot press Allow.
  • A process with the macOS Post Event permission is trusted as much as the person's own input, as in any macOS app: it can already post a real click, so a forged source pid gives it no more access. (Probe on the fleet, cmux21 and cmux24, macOS 27.0: CuaDriver's posted clicks arrived with its own pid, which PersonInputCheck refuses, and a process without the permission got no event through.)
  • Capture exclusion depends on the OS honoring sharingType = .none.
  • A nested tracking run loop (a held mouse button) keeps the outer event in InputDispatchContext. An Accessibility press during that time, within 1 s, could count as the person's run in the palette (review, low, not sure). Possible follow-up: match ActionInvocation.inputTimestamp to the event.
  • A person's click on Import in the web Settings › Import page now gets the panel, because it arrives outside an NSEvent dispatch. This is fail-safe.
  • caller.home_chat (added in #32983, not on this base) is a claim from the caller. Any local process can make the panel show "Chief". This PR does not change that.
  • Known limit, #33504: a VoiceOver user (VO-Space is an Accessibility press) and a person on Screen Sharing, Voice Control or Switch Control cannot answer Allow. They can answer Deny. The join consent sheet has the same limit. An accessible Allow needs a person-proof path: Touch ID or the login password, as macOS privacy prompts use.

Testing

Red commit 44e49ae7322 (tests only, on base 62c09dc65b5). The fix is in 436f2e02e16, with review repairs up to the head 9a08df0b980. Command: cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref SHA --arg=suite --arg=<package> --arg=<suites>.

RunCommitResult
CookieImportConsentPanelTests (red)44e49ae7322failed, 3 issues: sharingType is 1 (readOnly), not none; an Accessibility press / performClick on Allow allowed the import (the panel closed, and the answer was not Deny). Step 5666df11bf9392c963c3fa9b, aws-m4pro-5
CuaTargetGuardTests (red)44e49ae7322failed, 6 issues: only the 6 new capture-excluded cases. The other 20 tests pass. Step ba568dfca531f03d21e95097
CookieImportConsentPanelTests 6, CookieImportCommandTests 10, JoinConsentTests 41, BrowserCookieImportSecurityTests 1 (green)9a08df0b980all 58 Swift Testing tests passed. Step 8b260a40b41c6bcfea8981d1, aws-m4pro-5. The step still exits 1 (see the note below)
Same 3 suites (green)e4bf95f4e50passed, clean step. Step 73cc41dc22146a2d728558e3, aws-m4pro-5
CuaTargetGuardTests (green)e4bf95f4e50 (the helper has not changed since)passed, 26/26. Step ac887796fc49569f920e346b
cmux-ci compile-check --package CmuxNext9a08df0b980passed. Job ef49cdc004617beb3ddee96d
cmux-ci compile-check --package CmuxComputerUseHelpere4bf95f4e50passed. Job 52abe0d009c4630496e30a8e

JoinConsentTests (41, including the two person-click tests of the join sheet) pass with no change to them. This shows that the extraction into CmuxDialogPersonClick keeps the join sheet behavior.

Note on the XCTest-phase exits: after the Swift Testing tests of a suite pass, the empty XCTest phase of swift test sometimes ends with signal 6 (CoreData error 134020, "The model configuration used to open the store is incompatible"). This happened on aws-m4pro-4, -1 and -5, on different suites from run to run. These suites use no CoreData, and the same suites passed clean on aws-m4pro-5 at e4bf95f4e50. This is host state, not this change.

Observed / Not run

  • Observed: the fleet test runs and the compile check above. Tagged build cookie-consent-human-v1 (job e68cc4d1c171d4ff7c9a58e6) at 436f2e02e16, before the review repairs.
  • Not run: a live click on the panel in a tagged build on a GUI Mac (real HID click allows, posted click does not), and a live screenshot that shows the panel is not in it. The join consent sheet's real-click path, which this shares, was checked live in HQ#5121.

Try it (tagged build cookie-consent-human-v2, head 9a08df0b980)

  1. Open the tagged build cookie-consent-human-v2 (the Tag Opener). Keep a cmux window on screen.
  2. In a terminal in a cmux checkout, run:
    CMUX_TAG=cookie-consent-human-v2 scripts/cmux-debug-cli.sh browser import-cookies --from chrome --domain github.com --reason "Try the consent panel"
    
    You should see the consent panel in the center of the cmux window. Your focus does not move, and the Dock icon can bounce. The panel shows the requester, the reason, "Google Chrome", github.com and the target profile. The buttons are Show Terminal, Deny with ⎋, and Allow in the accent color with no key hint.
  3. Take a screenshot while the panel shows (Shift-Command-3, or Shift-Command-5 on the cmux window). The panel is not in the image. The rest of the window is.
  4. Press Return. Nothing happens (Allow has no key).
  5. Optional: run osascript -e 'tell application "System Events" to tell (first process whose name contains "cmux") to click button "Allow" of every window'. It needs the Accessibility permission for your terminal. Nothing happens: an Accessibility press does not allow.
  6. Click Deny (or press Escape). The panel closes, and the command fails with the denied message. If Escape does nothing, report it: a reviewer was not sure that Escape reaches this non-activating panel (the base has the same behavior).
  7. Run the command of step 2 again. Wait a moment, then click Allow with your mouse or trackpad. macOS asks for the "Chrome Safe Storage" key (unless you chose Always Allow before). After that, a toast says how many cookies came in, and the command succeeds.

A click in the first half second after the panel shows does not count (the activation delay). Click again if that happens.

Changelog

Fixed: Only your own click allows an agent's browser cookie import. Screenshots and screen sharing do not show the consent panel, and automation cannot press Allow.

Proof

No capture: the panel is out of screen capture by design (sharingType = .none), so a capture cannot show it. The visible change is the standard dialog buttons (Deny with Escape, Allow in the default look).

Risk: high in substance (consent and credential policy: this gate is all that stops an agent from reading the browser session cookies of the person). risk_tier.py grades it risk:normal, because no high path rule matches these files, and the workflow replaced the risk:high label. The PR has what the high tier asks for anyway: an independent receipt at the exact head (Review (independent-subagent-4): SHIP at 9a08df0b980d9f1c169d2df35a7ab3f00fa56c3f) and a green compile. It is not behind a default-off setting: it closes a hole in a consent gate, and a default-off switch would leave the hole open.