9d410ec9
docs: ssh's prompts get a mux-painted place
a73x 2026-08-30 10:59
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -240,6 +240,14 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 240 | (`unreachable: <reason>`) and by the entry dial's failure line — and only | 240 | (`unreachable: <reason>`) and by the entry dial's failure line — and only |
| 241 | the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its | 241 | the entry dial (`HandoffTarget.narrate`) relays the bytes onward, to its |
| 242 | own fd 2, and only until the wall takes the screen. | 242 | own fd 2, and only until the wall takes the screen. |
| 243 | - **A wall dial's ssh never touches `/dev/tty`.** `SSH_ASKPASS_REQUIRE=force` | ||
| 244 | plus `SSH_ASKPASS` at this image; `mux askpass` — a mode named by | ||
| 245 | `MUX_ASKPASS_SOCK`, since ssh gives its helper no word — carries the | ||
| 246 | prompt to a popup that eats every byte. `wall_pump.askOn` is the ONE | ||
| 247 | arming point, so the exclusions hold by construction: the entry dial keeps | ||
| 248 | ssh's own tty prompts, polls keep `BatchMode`. A decline SIGTERMs that ssh | ||
| 249 | (a refused askpass is an empty password to OpenSSH, asked three times over) | ||
| 250 | and ends the tile with `[prompt declined]`. | ||
| 243 | - **The layout sidecar is derived convenience, not authored intent.** | 251 | - **The layout sidecar is derived convenience, not authored intent.** |
| 244 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from | 252 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from |
| 245 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall | 253 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall |
README.md
| Old | New | ||
|---|---|---|---|
| @@ -213,6 +213,16 @@ from that same ssh — saying so in a single line, and the session is the | |||
| 213 | same session either way. HOST is one argv word handed to `ssh`, never a | 213 | same session either way. HOST is one argv word handed to `ssh`, never a |
| 214 | shell line: `mux` execs ssh itself, so nothing local expands or splits it. | 214 | shell line: `mux` execs ssh itself, so nothing local expands or splits it. |
| 215 | 215 | ||
| 216 | When a host wants a password, a key passphrase or a host-key `yes`, the | ||
| 217 | wall asks it in a popup: ssh hands the question to `mux` itself, the box | ||
| 218 | paints it over the tiles with the answer starred (the host-key question is | ||
| 219 | not — you are comparing a fingerprint), and Enter sends it back. A security | ||
| 220 | key's "touch now" notice appears the same way and goes away by itself. Esc | ||
| 221 | declines, which ends both that dial and the tile rather than asking again; | ||
| 222 | birth on the host again from `Ctrl-\ s` when you want another go. `mux HOST` | ||
| 223 | typed at a bare prompt still uses ssh's own prompts, and the wall's | ||
| 224 | once-a-second poll never asks anything. | ||
| 225 | |||
| 216 | `--via CMD` accepts any command that exposes the daemon's socket on stdio, | 226 | `--via CMD` accepts any command that exposes the daemon's socket on stdio, |
| 217 | for custom socket paths or transports. CMD is split on whitespace into | 227 | for custom socket paths or transports. CMD is split on whitespace into |
| 218 | argv and exec'd directly — no shell, so no quoting, no variables, no | 228 | argv and exec'd directly — no shell, so no quoting, no variables, no |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -7567,3 +7567,23 @@ body-capable request that declared no length now closes its connection. | |||
| 7567 | `pickerRow` cuts the spelling and never the state. It beats both old failure | 7567 | `pickerRow` cuts the spelling and never the state. It beats both old failure |
| 7568 | lines: `UnterminatedLine` named what mux observed, this names what happened. | 7568 | lines: `UnterminatedLine` named what mux observed, this names what happened. |
| 7569 | - **Deferred.** ssh's PROMPTS (`SSH_ASKPASS`); the dead-host poll pacing. | 7569 | - **Deferred.** ssh's PROMPTS (`SSH_ASKPASS`); the dead-host poll pacing. |
| 7570 | ## 2026-08-29 — ssh's prompts get a mux-painted place: `SSH_ASKPASS` → the wall | ||
| 7571 | |||
| 7572 | - **The rule.** A wall dial's ssh never touches `/dev/tty`: it is spawned | ||
| 7573 | with `SSH_ASKPASS_REQUIRE=force` and `SSH_ASKPASS` at this image, and | ||
| 7574 | `mux askpass` carries the question over a per-client socket in | ||
| 7575 | `$XDG_RUNTIME_DIR` to a popup that eats every byte. The mode is named by | ||
| 7576 | `MUX_ASKPASS_SOCK` and not by a word, because ssh execs its helper with | ||
| 7577 | the prompt as argv[1] and nothing else; a mode word still wins over it. | ||
| 7578 | - **Two exclusions, structural.** `wall_pump.askOn` is the ONE place a | ||
| 7579 | target is armed: the entry dial opens before any listener exists, and a | ||
| 7580 | poll neither passes through it nor could ask under `BatchMode`. | ||
| 7581 | - **A decline KILLS the dial's ssh.** A refused askpass is not a refused | ||
| 7582 | login — OpenSSH turns a non-zero helper into the EMPTY password and asks | ||
| 7583 | again up to `NumberOfPasswordPrompts`; measured against this box's sshd, | ||
| 7584 | one Esc was three prompts. So the wall SIGTERMs that ssh (its own child, | ||
| 7585 | by `dialOwner`) before releasing it, and the ring parks the tile. | ||
| 7586 | - **`SSH_ASKPASS_PROMPT` decides the paint**, not a substring of the text: | ||
| 7587 | `confirm` shows in the clear, `none` is a notice with no input line that | ||
| 7588 | closes when ssh kills its helper, anything else is starred. Both ends fold | ||
| 7589 | control bytes — a keyboard-interactive prompt is the SERVER's wording. | ||
docscheck.budget
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,6 @@ | |||
| 1 | askpass.zig 0 | ||
| 2 | client.zig 0 | ||
| 3 | client_core.zig 0 | ||
| 4 | client_core_wasm_check.zig 0 | 1 | client_core_wasm_check.zig 0 |
| 2 | client_core.zig 0 | ||
| 3 | client.zig 0 | ||
| 5 | cmd.zig 0 | 4 | cmd.zig 0 |
| 6 | delta.zig 0 | 5 | delta.zig 0 |
| 7 | docscheck.zig 0 | 6 | docscheck.zig 0 |
| @@ -13,49 +12,50 @@ interact.zig 0 | |||
| 13 | keymap.zig 0 | 12 | keymap.zig 0 |
| 14 | layout.zig 761 | 13 | layout.zig 761 |
| 15 | main.zig 0 | 14 | main.zig 0 |
| 16 | mux.zig 0 | ||
| 17 | mux_main.zig 0 | ||
| 18 | muxa.zig 0 | 15 | muxa.zig 0 |
| 16 | mux_main.zig 0 | ||
| 19 | paint.zig 0 | 17 | paint.zig 0 |
| 20 | predict.zig 0 | 18 | predict.zig 0 |
| 21 | protocol.zig 0 | 19 | protocol.zig 0 |
| 22 | proxy.zig 0 | 20 | proxy.zig 0 |
| 23 | pty.zig 0 | 21 | pty.zig 0 |
| 24 | quic.zig 0 | ||
| 25 | quic_client.zig 0 | 22 | quic_client.zig 0 |
| 26 | quic_server.zig 0 | 23 | quic_server.zig 0 |
| 24 | quic.zig 0 | ||
| 27 | replica.zig 0 | 25 | replica.zig 0 |
| 28 | select.zig 0 | 26 | select.zig 0 |
| 29 | server.zig 0 | 27 | server.zig 0 |
| 30 | server_agent.zig 0 | ||
| 31 | server_sessions.zig 0 | ||
| 32 | server_test_agent.zig 0 | ||
| 33 | server_test_attach.zig 0 | ||
| 34 | server_test_await.zig 0 | ||
| 35 | server_test_clipboard.zig 0 | ||
| 36 | server_test_deliver.zig 0 | ||
| 37 | server_test_harness.zig 0 | ||
| 38 | server_test_modes.zig 0 | ||
| 39 | server_test_quic.zig 0 | ||
| 40 | server_test_session.zig 0 | ||
| 41 | server_test_upgrade.zig 0 | ||
| 42 | shellint.zig 0 | 28 | shellint.zig 0 |
| 43 | sockpath.zig 0 | 29 | sockpath.zig 0 |
| 44 | spawn.zig 0 | 30 | spawn.zig 0 |
| 45 | testtmp.zig 0 | 31 | testtmp.zig 0 |
| 46 | upgrade.zig 269 | 32 | upgrade.zig 269 |
| 33 | wallview.zig 0 | ||
| 34 | wasm_core.zig 0 | ||
| 35 | webhub_main.zig 0 | ||
| 36 | webhub.zig 0 | ||
| 37 | xdg.zig 0 | ||
| 38 | server_test_harness.zig 0 | ||
| 39 | server_test_attach.zig 0 | ||
| 40 | server_test_deliver.zig 0 | ||
| 41 | server_test_modes.zig 0 | ||
| 42 | server_test_session.zig 0 | ||
| 43 | server_test_quic.zig 0 | ||
| 44 | server_test_await.zig 0 | ||
| 45 | server_test_clipboard.zig 0 | ||
| 46 | server_test_agent.zig 0 | ||
| 47 | server_test_upgrade.zig 0 | ||
| 48 | server_agent.zig 0 | ||
| 49 | server_sessions.zig 0 | ||
| 47 | wall_host.zig 0 | 50 | wall_host.zig 0 |
| 48 | wall_layout.zig 1432 | ||
| 49 | wall_picker.zig 0 | 51 | wall_picker.zig 0 |
| 50 | wall_pump.zig 0 | 52 | wall_pump.zig 0 |
| 53 | wall_layout.zig 1432 | ||
| 51 | wall_test_harness.zig 0 | 54 | wall_test_harness.zig 0 |
| 52 | wall_test_host.zig 0 | 55 | wall_test_host.zig 0 |
| 53 | wall_test_layout.zig 0 | ||
| 54 | wall_test_picker.zig 0 | 56 | wall_test_picker.zig 0 |
| 55 | wall_test_pump.zig 0 | 57 | wall_test_pump.zig 0 |
| 58 | wall_test_layout.zig 0 | ||
| 56 | wall_test_wall.zig 0 | 59 | wall_test_wall.zig 0 |
| 57 | wallview.zig 0 | 60 | mux.zig 0 |
| 58 | wasm_core.zig 0 | 61 | askpass.zig 0 |
| 59 | webhub.zig 0 | ||
| 60 | webhub_main.zig 0 | ||
| 61 | xdg.zig 0 | ||