Agent pane: no system call on the key path to judge who posted an event (P-MAIN S5)
Summary
Risk tier: normal, as scripts/ci/risk_tier.py pr --number 28065 grades it (lead decision 2026-11-01 18:03Z: the label follows the repo's risk tool). The change guards permission grants and join consent, so it still has what high would need: an independent SHIP at the exact head and a green fleet compile. risk_tier.py does not list these files as high; that classifier gap goes to cmux-ad.
P-MAIN S5 (found by the S2 stall agent, 2026-10-30). AgentPanePersonInput.isPerson (the agent pane's gesture monitor: every key-down and mouse-down) and PersonInputCheck.isPerson (PersonInputLedger: every key and mouse event of the app) called proc_pidpath on the main thread for each event that another process posted (source pid not 0). That is a system call on the key path.
Why the path is read: only the person may grant in an agent pane (permission Allow, cx-ocdz / cx-1l61) or back a join consent. The HID system's events (pid 0) are the person's. Of the posted events, only the Apple services a person drives the Mac through count (Screen Sharing, Universal Control, Accessibility Keyboard, named by their path under SIP-protected /System), and for the strict join consent check only the WindowServer. A computer-use agent or a script that posts a click must not count. The event itself carries only the pid, not the executable, so the decision cannot come from the event alone (option (a) does not apply).
Fix (option (b)): EventSourceProcesses, one table per app (built in AppServices, injected; no singleton).
- The key path reads the table under its lock and makes no system call.
// perf-hot:marks the three functions. - A pid not in the table takes the safe default at once: not the person's. A wrong "no" costs one gesture (the person clicks again); a wrong "yes" would let an automated click grant. The person's own HID input (pid 0) never reaches the table, so local typing and clicks are never affected.
- The process is read once, on the table's serial queue
com.cmuxterm.next.event-source-processes: start time (sysctl(KERN_PROC_PID)p_starttime), path (proc_pidpath), start time again (so both are one process). Neither read has a same-user check, so the WindowServer, launchd and the root Screen Sharing daemon are read as this user (62fc9e9212e; before it,proc_pidinfo(PROC_PIDTBSDINFO)refused them with EPERM). A live process that is still unreadable keeps the safe default until its exit watch fires; it is not read again for each event. - An exit watch (a kqueue
EVFILT_PROCwithNOTE_EXIT | NOTE_EXEC, registered withEV_RECEIPTand read by a dispatch read source on the same serial queue) is armed before the read. It removes the entry when that process ends or runs a new program (exec: same pid and start time, new image). A pid the kernel gives to a new process, or a new image in the same process, is therefore read again. No timer decides anything (R5). One entry per live posting process, no constant cap (R9).
Lead review, second round:
- Priming.
PersonInputLedgeralso watches posted mouse moves, scrolls, other drags and modifier changes (primingTypes). For an unknown pid, it queues the table read (EventSourceProcesses.prime: a table lookup, no system call). A Screen Sharing session moves the mouse before it clicks, so its first click already counts. - SECURITY, exit watch. The watch is a kqueue
EVFILT_PROC/NOTE_EXITregistered withEV_RECEIPT, so a refusal is seen. When the kernel refuses the watch, nothing is kept: no verdict can outlive its process or pass to a process that reuses the pid. What the person sees then: a click or key that Screen Sharing, Universal Control or the Accessibility Keyboard posts never counts as their gesture. A permission Allow through Screen Sharing stays unanswered and they must use the Mac's own keyboard or mouse. Their own local input (HID, pid 0) is never affected. Each new event reads the process again, off the main thread. - Fleet probe (SSH diagnostic, uid 501,
select.kqueuewithEV_RECEIPT): on cmux17, cmux19, cmux21 and cmux23 (macOS 27),NOTE_EXITattached to launchd (pid 1, root) and to the WindowServer (_windowserver). screensharingd starts only on demand and ran on no host; it runs as root like launchd.theKernelExitWatchAttachesAsThisUserprints the same lines from the fleet test step. plans/cmux-next/arch-ratchet.json: unchanged (feat's file). The PR adds no ratchet exception and no// blocking-allow:escape: the start-timesysctlandproc_pidpathrun only on the table's serial queue.
Commits: the seam (no behavior change), red test 1, fix 1, red test 2 (priming, no watch), fix 2; after the base merge, 4cc0a03d904 moves the wiring into CmuxNextAgentPane (AgentPaneShared, PersonInputLedger.sources) so CmuxNextApp lines do not rise, and drops the exception.
Performance
Main-thread time per event, measured in EventSourceProcessesTests.perEventMainThreadCost (mean over 20,000 events, one fleet Mac, not an exclusive host).
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| main-thread time per posted event (person-input check) | 4333 ns (proc_pidpath per event) | 323 ns (table read) | -4010 ns | perEventMainThreadCost, fleet job 5e30898cbc1d8b0a1826e976 at ff83b4e1f5d |
| main-thread time per mouse move of the person (pid 0), new ledger work | 0 ns (not watched) | 208 ns | +208 ns | perEventMainThreadCost, fleet job 5e30898cbc1d8b0a1826e976 at ff83b4e1f5d |
No capture: no visible change; the person-input check gives feat's verdicts for every posting process, including the WindowServer and root Screen Sharing (see the BLOCK at 072f126 and its fix 62fc9e9212e), only off the main thread; an unread or unreadable posting process is refused until it is read.
Review
Review (claude-opus-5.5-merge-review-subagent): SHIP at 97fd6798bb060116e342d07bdf05a5a535085450 (receipt: https://github.com/manaflow-ai/cmux/pull/28065#issuecomment-6520347203; merge of origin/feat-cmux-next, compile c17fa3d30e8e96886973f04c). Before the merge: SHIP at ff83b4e1f5d (https://github.com/manaflow-ai/cmux/pull/28065#issuecomment-6519845847). Earlier round: FIX at 3f13e1d3bec, all findings fixed. Follow-up (non-blocking): drop the default eventSources: argument of AgentPaneView.init and AgentTabStore.init, so that a new caller cannot get an unprimed private table.
Changelog
none
Review round 1 (claude-opus-review-subagent, FIX at 3f13e1d3bec): fixed the exec case (NOTE_EXEC), fail closed unless exactly one EV_RECEIPT receipt has no error, a kernel-wait test that could hang now fails after 10 s, and perEventMainThreadCost prints the main-thread ns per event (R10). The reviewer's finding that the blocking-allow exception is not needed is wrong for the installed hook: its raw-blocking-ext rule (proc_pid*) refused the push without it.
Testing
Regression: EventSourceProcessesTests (CmuxNextAgentPaneTests), run by scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext EventSourceProcessesTests.
| Step | Ref | Job | Result |
|---|---|---|---|
| compile | 3f13e1d3bec | bfa3612a880006dfa9828091 | passed (CmuxNext 275 s, app 50 s, cmux-tui 27 s) |
| red 1 (per-event read on main) | b89a7aaa105 | 18c989cb925e7f1104c5446e | failed as expected: theKeyPathReadsOnlyTheTable (101 reads, on main), aReusedPidIsReadAgain, aPidWithNoProcessIsNotKept, theStrictCheckReadsOnlyTheTable |
| red 2 (no priming, kept without a watch) | d9c4ed99a67 | abffd290d5e174c34c5dcebe | failed as expected: only aMouseMoveFillsTheTableBeforeTheFirstClick and aProcessWithNoExitWatchIsNeverKept |
| green | 3f13e1d3bec | 98d9a0eafbca030eb8d0fa80 | 7/7 passed on aws-m4pro-6; exit-watch-proof: launchd pid 1 as uid 501: attached, WindowServer pid 164 as uid 501: attached, screensharingd not running |
| compile | ff83b4e1f5d | ddbe9951d28d3c8897f4a6b0 | passed (CmuxNext 255 s, app 76 s, cmux-tui 39 s) |
| green at the head | ff83b4e1f5d | 5e30898cbc1d8b0a1826e976 | 9/9 passed; theKernelWatchFiresOnExec passed; per event on main: person's mouse move 208 ns (new), posted event from the table 288-323 ns, base proc_pidpath 4333 ns |
| red 3 (no exec notice) | 38ce555794a | ebac231666cc7f1a9e7489d6 | NO verdict: worker lease lost during compile, 0 test lines |
| red 3 infra-rerun | 38ce555794a | a74dba670ac66b514e5e0355 | failed as expected on cmux11s-Mac-mini: only theKernelWatchFiresOnExec (Expectation failed: await Self.fires(fired.stream) after 10.029 s); the other 8 passed |
Observed / Not run
Observed: the jobs above; an SSH kqueue probe as uid 501 on cmux17, cmux19, cmux21, cmux23 (macOS 27) attached NOTE_EXIT to launchd (root) and WindowServer (_windowserver).
Not run: the typing bench (perf claim: none); screensharingd ran on no host (it starts on demand; it runs as root like launchd); no local Mac build or test (repo rule).