Repository navigation
fix(chief memory): the launch-time memory offer never lists ~/Downloads or ~/Desktop (restores the #28811 test) - #28820
Conversation
…loads or ~/Desktop Root cause of the launch prompt '"cmux DEV …" would like to access files in your Downloads folder' (Lawrence, 2026-10-30): HomeHostView's first-run memory offer runs the bundled `optchat-chief memory import-from detect` on every launch until the offer is clicked, and detect listed ~/Downloads and ~/Desktop for claude.ai / ChatGPT export zips. macOS attributes the child's read to cmux and prompts. `detect` now reads only the agents' own folders; `detect --exports` (detect_with_exports) also lists the export zips, and only the Import Memories action the person runs passes it. The offer still finds Claude Code and Codex memory (LAUNCH-NO-TCC-PROMPTS).
|
Review (claude-opus independent reviewer): SHIP at 3c94857 Read-only review of the two commits (ee4ed73 red test, 3c94857 fix). I did not build or run anything. Correctness
Findings (none block)
Other unasked paths: what I searched and what I foundSearched (all inside this checkout):
Found outside Chief memory. These are separate follow-ups if anything, not this PR's scope:
|
|
Non-green checks at head 3c94857, classified (none come from this PR's 6 files):
|
|
Swift proof for this PR (merged head 3c94857): The step ran at proof-only commit d064fc0, which is this head plus only the regenerated Follow-up from the review: #28882 (a scripted Import Memories run never lists ~/Downloads or ~/Desktop). |
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Not verified: macOS jobs did not run: compile and app tests were skipped on CI passes on Written by Shipped in:
|
…ds ~/Desktop until the person shows it (#28907) * test(chief memory): a scripted Import Memories run never lists ~/Downloads or ~/Desktop (red) chief.importMemories with no source runs detect --exports for any caller: a script, an agent or the CLI through the control socket makes cmux list ~/Downloads and ~/Desktop (and raise the macOS folder prompt) before the dialog shows. Only the person's own run in the app may ask for the export zips. This commit moves the choice of the detect arguments into ChiefMemoryImportHandlers.detectArguments(muxHome:invocation:) and offerDetectArguments(muxHome:) with no change in behavior, and adds ChiefMemoryImportExportsTests. onlyThePersonsOwnRunListsTheExportZips fails here: every origin still gets --exports. The next commit fixes it. * fix(chief memory): only the person's own Import Memories run lists ~/Downloads or ~/Desktop A chief.importMemories run that names no source asked optchat-chief for detect --exports whatever its origin, so a script, an agent or the CLI through the control socket made cmux list ~/Downloads and ~/Desktop, the folders macOS guards, with no action of the person. detectArguments now passes --exports only for ActionInvocation.isPersonsOwnRun: origin user, not carried by the control socket, and naming no caller app, agent chat or terminal. Any other run lists only the agents' own folders; a script still imports a zip by naming it (source zip:/path). The predicate moves from ScreenshotHandlers.isPersonsRun into CmuxNextActions, so the raw-pixel rule and this rule are one rule. Reported by the independent review of #28820. * test(screenshots): a Screenshots model not started reads and watches no folder (red) ~/Desktop is macOS's default screenshot folder, which macOS guards. The Screenshots page reads and watches its folders as soon as the page is made, and an agent or a script makes it with no action of the person (cmux browser on its tab, the DevTools relay, screenshots.open with focus: true). This commit adds ScreenshotsModel(listsAtOnce:), isListing and startListing() with no change in behavior (every model still lists at once), and ScreenshotsModelListsOnlyWhenStartedTests, which fails here: a model made with listsAtOnce: false lists and watches ~/Desktop. The next commit fixes it and makes the app's page start listing only when it is on screen. * fix(screenshots): a Screenshots page reads ~/Desktop only once it is on screen, and only the person's run puts it there The Screenshots page read and watched its folders, ~/Desktop (macOS's default screenshot folder, which macOS guards) among them, as soon as the page was made. An agent or a script makes it with no action of the person: cmux browser on its tab and the DevTools relay make the page off screen, and screenshots.open with focus: true put the tab in front. ScreenshotsModel(listsAtOnce: false) now reads and watches nothing until startListing(). The app's page model is made that way and starts listing when the page is first on screen (setContentVisible(true), which only the pane that shows the tab calls). screenshots.open selects the tab only for ActionInvocation.isPersonsOwnRun (the rule of #28882); any other run opens it in the background. A tab the person left on screen is restored on screen and lists at once: that is the person's own view, as Chrome reloads a restored tab. * test(screenshots): a restored or agent-shown Screenshots page lists nothing until the person clicks (red) The review of #28907 found that showing the page is not the person's choice: an agent can show a background Screenshots tab (cmux tab focus, a browser mirror, the Home mirror), and a tab restored on screen reads ~/Desktop at launch, which raises the macOS folder prompt after a privacy reset or on a new Mac (LAUNCH-NO-TCC-PROMPTS). This commit adds the seams ScreenshotsView.showsPrompt and showScreenshots(), and ScreenshotsOpen (the decision of a screenshots.open run) with the old behavior, and the tests: - ScreenshotsPageListsOnlyAfterTheClickTests: a page made by the app and shown (restored on screen, or shown by an agent) lists nothing and shows the prompt until the person clicks; it fails here (showing it lists). - screenshotsOpenFromAScriptWithFocusSaysItOpenedInTheBackground (the handler, on a real window): a scripted run with focus says that it opened the tab in the background; it fails here (it says nothing). - ScreenshotsOpenDecisionTests: only the person's run selects and lists at once, and every other run gets a note; it fails here. * fix(screenshots): a Screenshots page lists nothing until the person clicks Show Screenshots in it The review of #28907 (FIX) found that showing the page is not the person's choice. An agent can show a background Screenshots tab (cmux tab focus, a browser mirror, the Home mirror), and a tab restored on screen read ~/Desktop at launch, which raises the macOS folder prompt after a privacy reset or on a new Mac (LAUNCH-NO-TCC-PROMPTS). Chrome's reload of a restored tab is no precedent here: Chrome passes no OS privacy gate. - ScreenshotsView shows a Show Screenshots prompt (title, one line on why, one button) until the person clicks it; only that click starts the model listing. Shown, focused or restored, the page reads and watches nothing. The prompt goes in one frame, with no animation; its fonts come from the app's Typography. - The app's page model is made with listsAtOnce: false, and setContentVisible no longer starts listing. - screenshots.open: only the person's own run (isPersonsOwnRun) brings the tab to the front; any other run, with --focus too, opens it in the background and says so (result opened: background, and a note), never a silent background open. The decision is ScreenshotsOpen. - ScreenshotsModel.apply does nothing before listing starts. - Four new strings in all 21 locales of the Screenshots catalog. * test(screenshots): the click-gate test checks plain values, so a red run never reflects the view In the red run (step c6a93fd37237b33f8c12418c) the test failed on its own assertion, then the test process crashed while Swift Testing described the next failed expectation: it reflected ScreenshotsView and hit a null NSPasteboard. The handler test after it never ran. Each expectation now checks a Bool that is read first. * test(screenshots): no detection before the click, the click remembered per tab, and screenshots.open says what it did (red) The review of 791f3d7 (FIX) found: - folder detection ran before the click (makePage); it reads other apps' containers and checks folders under ~/Desktop, which can raise a macOS prompt at launch for a restored tab; - the keyboard could not reach Show Screenshots (the hidden grid took focus); - the click was kept on the page, so a page made again (hibernation wake, engine switch) asked again; - ScreenshotsModel listed at once by default; - screenshots.open told a script "opened in the background" for a tab already open and listing; the unselected tab and the no-window refusal had no test. This commit adds the seams ScreenshotsGate (unlocked tabs by key), ScreenshotsView.onShowScreenshots and focusTarget, and an injectable ScreenshotEnvironment for ScreenshotsPageService, all with the old behavior, and the tests ScreenshotsGateTests and ScreenshotsGateWiringTests, which fail here. * fix(screenshots): nothing is detected before the click, the click is kept per tab, and screenshots.open says what it did The fixes for the review of 791f3d7 (FIX): - Folder detection (which reads other apps' containers and checks folders under ~/Desktop) runs only after the person's first Show Screenshots click (ScreenshotsGate.mayDetect); before it, a restored or agent-made page reads nothing at all. - The click is kept per tab key (ScreenshotsGate), so a page made again after a hibernation wake or an engine switch lists with no second click. - While the prompt is up, focusing the tab focuses the Show Screenshots button (Space presses it; no Return key equivalent), not the hidden grid. - ScreenshotsModel lists only when its caller says so (listsAtOnce defaults to false). - The prompt's secondary text uses the palette (Palette.textSecondary). - screenshots.open from a script on a tab already open answers opened: existing with listing true or false (and a note while it waits for the click), not "opened in the background". - SystemScreenshotsWorkspace (the open and reveal adapter) moves from CmuxNextApp into CmuxNextScreenshots. - A What's New highlight, and one new string in all 21 locales. * test(screenshots): the answer for a Screenshots tab already open is tested on ScreenshotsOpen The red run at d5198fd (step bfd0f9bd9fc2d72fa22f0be0) showed that screenshotsOpenSaysWhatItDid passed there: its existing-tab part ran only when the new tab was in the pane, and in a test with no daemon the tab never arrives, so that part was skipped. That part moves to ScreenshotsOpenDecisionTests, on ScreenshotsOpen.result: a new tab opened behind, the tab already open and listing, and the tab already open and waiting for the click. The handler test keeps the background answer. * test(screenshots): Space shows the screenshots without Full Keyboard Access, and the wiring test never reads the test Mac's Desktop (red) The third review (FIX at bf28c6a) found that the Show Screenshots button takes the keyboard only with Full Keyboard Access on, so with the default setting Space did nothing; and that one wiring test ran the real detector after its click, which lists the test Mac's own Desktop. spacePressesShowScreenshotsWhileThePromptIsUp: while the prompt is up the pane itself takes the keyboard and Space shows the screenshots; it fails here (the focus target is the button, and Space does nothing). The click test now uses a fake detection environment. The unused ScreenshotsGate.forget leaves the gate test. * fix(screenshots): Space shows the screenshots with the default keyboard setting; the title is themed; a script with no pane is refused; the highlight validates The fixes for the third review (FIX at bf28c6a): - B: while the prompt is up the pane itself takes the keyboard (acceptsFirstResponder) and Space shows the screenshots: a plain button takes the keyboard only with Full Keyboard Access on. After the click the keyboard goes on to the grid. - A: the What's New highlight has its action line (screenshots.open), so the highlight validator passes. - E: the prompt title takes Palette.textPrimary. - F: screenshots.open with no focused pane is refused (No pane is focused), never a silent success. - D: the unused ScreenshotsGate.forget is gone; a click stays for the life of the process (tab keys are durable). - G: the click test uses a fake detection environment. * test(screenshots): the Space test checks plain values, so a red run never reflects the view Its red run (step 7dd4a41d0e08065a142e97fc) crashed while Swift Testing described a failed expectation on two views (a null NSPasteboard in the reflection), as the click test did before. It now compares Bools. * chore(next): PR autofix The regenerated files from autofix run 41198445289 at e04a9b9 (its pr-autofix-patch artifact): ci-target-graph.json (CmuxNextScreenshots now depends on CmuxNextCompat), action-surfaces.json and actions.md (screenshots.open is an effect binding), the CLI and palette reference docs, and schemas/defaults/cmux-next.json. The run's push job did not run: it needs a non-pull_request event, and pull_request is the only trigger. * chore(next): this PR carries no generated output (take the base's copies) The security of the merge (review FIX at cef95c1): the autofix patch was applied with git apply, not with pr-autofix-apply.sh, so it pushed outputs this pull request does not own (bot-only schemas/defaults, the CLI and palette references, and export rows regenerated against an older base; merged with a newer base, action-surfaces.json and actions.md removed 155 of the base's rows, the #21946 problem). generated_outputs.py drift also says ci-target-graph.json is written by the base autofix only. All six now equal origin/feat-cmux-next; the base autofix regenerates them after this pull request merges. The commit message of cef95c1 was also wrong: the export change there was the base's automations.show, not screenshots.open. --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…oads or ~/Desktop (#28882) * test(chief memory): a scripted Import Memories run never lists ~/Downloads or ~/Desktop (red) chief.importMemories with no source runs detect --exports for any caller: a script, an agent or the CLI through the control socket makes cmux list ~/Downloads and ~/Desktop (and raise the macOS folder prompt) before the dialog shows. Only the person's own run in the app may ask for the export zips. This commit moves the choice of the detect arguments into ChiefMemoryImportHandlers.detectArguments(muxHome:invocation:) and offerDetectArguments(muxHome:) with no change in behavior, and adds ChiefMemoryImportExportsTests. onlyThePersonsOwnRunListsTheExportZips fails here: every origin still gets --exports. The next commit fixes it. * fix(chief memory): only the person's own Import Memories run lists ~/Downloads or ~/Desktop A chief.importMemories run that names no source asked optchat-chief for detect --exports whatever its origin, so a script, an agent or the CLI through the control socket made cmux list ~/Downloads and ~/Desktop, the folders macOS guards, with no action of the person. detectArguments now passes --exports only for ActionInvocation.isPersonsOwnRun: origin user, not carried by the control socket, and naming no caller app, agent chat or terminal. Any other run lists only the agents' own folders; a script still imports a zip by naming it (source zip:/path). The predicate moves from ScreenshotHandlers.isPersonsRun into CmuxNextActions, so the raw-pixel rule and this rule are one rule. Reported by the independent review of #28820. --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
Summary
Privacy bug, live on
feat-cmux-next: Home's first-run memory offer (HomeHostView->ChiefMemoryImportHandlers.detectForOffer) runs the bundledoptchat-chief memory import-from detectat every launch until the person clicks the offer. The person did not ask for it.detectlisted~/Downloadsand~/Desktopto look for claude.ai and ChatGPT export zips. macOS charges that read to cmux and shows the prompt "cmux would like to access files in your Downloads folder".This PR:
unasked_detect_never_lists_downloads_or_desktop(Native/OptChat/optchat-chief/src/memory_sync_state.rs, first added in aec7054) by reverting base red: revert the unasked-detect red test (aec7054d44b) until its fix lands #28811.a-lazy-permissions, cherry-picked with no conflicts and no changes):detectnow reads only the agents' own folders (Claude Code, Claude memory, Codex).detect_with_exports(CLIdetect --exports) also lists the export zips.--exports(MemoryImportCommand.detect(exports: true)). The launch-time offer callsdetect()and does not pass it.Takes back #28811: it removed the red test only until this fix lands.
Risk:
risk:high, because it changes which user data the app reads.Testing
Focused command (fleet, macOS):
6ff896f6fa46488400b42f66(aws-m4pro-8):panicked at src/memory_sync_state.rs:461:9: detect listed a protected folder: [... zip:.../Downloads/data-2026.zip ..., zip:.../Desktop/claude-export.zip ...].--arg optchat-chiefwith no filter: all optchat-chief tests, clippy-D warningsand fmt): step52751e3eef0eaaade0c87d33(aws-m4pro-4): lib tests 154 passed, 0 failed, includingunasked_detect_never_lists_downloads_or_desktop ... okanddetect_with_exports_finds_claude_code_notes_codex_and_export_zips ... ok; clippy and fmt pass. The step is red on 3 integration tests in files this PR does not touch:tests/acpmux_peer_moved.rs:187(CMS signature status 4),tests/eval_upsell.rs:37(skill lacks the billing checkout line),tests/toolpack_mcp.rs:219(unwrap on None). A base run of the same mode at dff3196 (step791a2c4095b9f21b801a1d9b) is queued to confirm them as base reds.ChiefMemoryImportTestsat the head (scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=ChiefMemoryImportTests): step004f0362b68ecae05f93dc15at the head stopped before any test ran:generated file is stale ... Native/OptChat/optchat-chief/inspector/index.html(a base red; this PR does not touch that file; open fix cmux NEXT chief inspector: regenerate the committed page after the cursor-default change (base red) #28817). Rerun on proof-only branchproof/cmdp-swift-with-28817(d064fc0: this head plus only the cmux NEXT chief inspector: regenerate the committed page after the cursor-default change (base red) #28817 page) is queued as step3eb65e3eb41152fcc129469f; base check stepb9079fb1b1db3bf15738764dqueued.cmux-ci compile-check --package CmuxNext --app --recipe-version 2): passed, job3be590d91044f7623578aa4c: CmuxNext 291 s, app 75 s, cmux-tui 37 s.Observed / Not run
Observed:
git revert 74b40b70e02applied cleanly. The restored test is the same as the one in aec7054, line for line.git cherry-pick bc8b414147aapplied cleanly. Its patch-id is the same as the original commit's, so nothing changed in the patch.detect(exports: Bool = false)is a nonisolated method on aSendablestruct.Runner.detect()is@MainActorand already awaits the off-mainrunhelper, so isolation does not change.--exportsis changed to--exports=1beforeFlags::parse, the same as--stop-host. No other optchat-chief command uses--exports.scripts/cmux-next/prepush-gate.py: no new violations.Not run:
chief-evals.ymlor other suites calldetectand expect zips. A search found no other Rust caller ofdetect.Proof
No capture: the offer looks the same; this change only stops a folder read at launch, which no still can show.
Changelog