[CMUX Next] Chief pet: the daemon refuses a reply once its agent left the foreground
The Chief pet's quick reply (#21738) checks with process-info that an agent, not a bare shell, is in the terminal's foreground, and then pastes the reply and presses Return. If the agent exits between that check and the paste, the shell runs the reply as a command. Now the daemon does the check itself, right before it writes.
send gets an optional expect_foreground string, behind the new capability send-expect-foreground-v1. Just before writing, the daemon reads the surface's foreground process-group leader again (foreground_process_name, the same value process-info reports as foreground_executable). If that leader isn't the named executable, or can't be read, the request fails with foreground changed: ..., classified as known-not-delivered, and nothing is written. expect_foreground only works with surface. Combined with terminal_id, the request fails with bad request, because a terminal that's still being created has no foreground yet.
On the App side, SendInputRequest and DaemonConnection.send gain expectForeground, and DaemonConnection.typeToForegroundProgram does the check and the two guarded writes, so the App target shrinks. typeChiefPetReply calls it and keeps its runningProgram check as the first gate, then passes the foregroundExecutable it saw on both writes, the paste and the Return.
Closes the follow-up coord-2 asked for when it reviewed #21738. Stacked on #21738: the base is feat/chief-pet-setting. Retarget to feat-cmux-next once #21738 merges.
Compatibility
The send command enum doesn't deny unknown fields, so an older daemon ignores expect_foreground and writes as before. The App's own check still protects that case, with the old small race. Swift leaves the field off when it's nil, so unguarded sends are byte-for-byte unchanged (tested). The spec and all seven generated bindings carry the new field (generate.py --write; --check, check-spec-inventory.py and check-resource-api-boundary.py pass).
Socket policy analysis
- Can it run commands or open content on local objects? No new capability.
sendalready writes input to a terminal, and this field can only make it refuse a write it would otherwise make. The remote relay still refusessendbefore parsing it (commands.md), so no relay path changes. - Can it change or destroy objects the caller doesn't own? No. It addresses the same
surfacewith the same authority as before. - Does it read local state the caller shouldn't see? No. The comparison yields only match or mismatch, and the error text doesn't name the executable it found. Callers with
sendauthority can already read it throughprocess-info. - Phone lane: the generated
DaemonLanePolicy+Fieldsnow allowsexpect_foregroundonsend. The phone lane already hassendandprocess-info, so this field adds no read, and it can only make the daemon refuse a write. - No command-bearing params, no new method, and no relay allowlist change.
Verification
- Red first: a17e544d and 3707e5c0 add
cli/send_expect_foreground.rs. Hosted run 39179652753 failsa_reply_for_an_exited_program_never_reaches_the_shellon Linux and macOS withthe reply reached the shell: {"data":{},"id":4,"ok":true}. - Green: hosted run 39203646531 on ad26943f passes all three tests on Linux and macOS, plus clippy and fmt. The Rust SDK (
cargo check -p cmux-sdk --all-targets) builds. A re-run on the merge head 8765203d is in progress. - Swift: new
SendInputRequestTests(the encoded wire hasexpect_foreground, and an unguarded send omits it). No Swift toolchain on the authoring box, so these tests and the App change are compiled and run only by this PR's CI. verify-local.py --affected: every selected check passes exceptlocalization, which fails with the base branch's existingneeds_reviewcatalog entries. This PR changes no strings.- No live dogfood: reproducing the race in the app depends on exact timing. The hosted tests drive the real daemon:
sleepexits via Ctrl-C, then a guarded paste is refused and never reaches the shell.
Changelog
- Fixed: A Chief pet quick reply no longer runs as a shell command when its agent exits just before the reply is typed.
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Closes the race where a Chief pet quick reply was typed as a shell command when its agent exited between the app's foreground check and the paste. The daemon now verifies the foreground itself, right before it writes.
send gains an optional expect_foreground field behind the new send-expect-foreground-v1 capability. Just before writing, the daemon re-reads the surface's foreground process-group leader; if it isn't the named executable, or can't be read, the request fails with foreground changed: ..., writes nothing, and is classified as known-not-delivered. The app names the executable its existing process-info check saw on both the paste and the Return writes, and treats the capability as optional so an unguarded reply is still noted when an older daemon lacks it.
Older daemons ignore the field, leaving unguarded sends byte-for-byte unchanged. The field requires surface; combined with terminal_id the request fails as not delivered. The check is best effort: the program can still exit between the read and the write, a window of microseconds instead of a client round trip.
Tests
- Hosted CLI tests drive a real daemon: a guarded paste is refused and never reaches the shell once
sleepexits via Ctrl-C, and the foreground check covers a running program,terminal_id, and an unreadable foreground. - New Swift tests assert the guarded wire payload and an unguarded send carrying no field; all seven SDK bindings carry
expect_foreground.