fix(ssh-controlmaster): handle read-only ControlPath under bind-mounted ~/.ssh

The devbox bind-mounts ~/.ssh read-only. A user ~/.ssh/config with a per-host
ControlPath under it (the CGNAT idiom `ControlPath ~/.ssh/cm/%r@%h:%p`) is
unwritable there, so a plain `ssh <host> pwd` exits 255 trying to bind the
master socket — blocking `pi --ssh <host>` with "Could not resolve remote pwd".
A system default cannot override a user's per-host value (SSH first-value-wins),
so this must be handled in the extension.

- controlPathWritable(): expands ~, tests whether the socket's parent dir is
  writable (or missing but creatable via nearest existing ancestor). Pure fs
  check — OS-agnostic, no host-OS detection.
- negotiateMaster / negotiateMasterWithPassword: reuse the system master only
  when its ControlPath is writable; otherwise start our own /tmp master whose
  command-line `-o ControlPath` overrides the user's unwritable path.
- Remote pwd probe: `-o ControlPath=none -o ControlMaster=no` so a read-only
  system ControlPath cannot make the initial probe fail.

No behaviour change for configs without ControlMaster. Updates README.md +
AGENTS.md to match.
This commit is contained in:
2026-06-18 21:59:00 +02:00
parent 357fcc6eca
commit 6f7dca06e8
3 changed files with 90 additions and 20 deletions
+6 -5
View File
@@ -95,11 +95,12 @@ pi -e ~/src/src_local/pi-extensions/extensions/ssh-controlmaster.ts --ssh user@h
**How it works:**
1. On `session_start`, runs `ssh -G <host>` to read the effective config for that host
2. If `~/.ssh/config` already configures `ControlMaster auto` or `yes` for the host, the existing system socket is reused — no second connection is opened and pi does **not** tear down the master on exit (it was the system's to manage)
3. Otherwise pi establishes its own master: `ssh -fN -o ControlMaster=yes -o ControlPersist=yes -o ControlPath=/tmp/pi-cm-<pid>.sock <remote>` and shuts it down cleanly on exit
4. All tool calls multiplex over the socket with `-o ControlMaster=no -o ControlPath=<socket>` — near-zero per-call overhead
5. The system prompt is patched to tell the LLM it's operating on `<remoteCwd> (via SSH ControlMaster: <remote>)`
6. User `!` shell commands are also routed over SSH
2. If `~/.ssh/config` already configures `ControlMaster auto` or `yes` for the host **and its `ControlPath` directory is writable**, the existing system socket is reused — no second connection is opened and pi does **not** tear down the master on exit (it was the system's to manage)
3. Otherwise (no system master, **or** its `ControlPath` is on a read-only mount — e.g. `~/.ssh/cm` when `~/.ssh` is bind-mounted read-only) pi establishes its own master: `ssh -fN -o ControlMaster=yes -o ControlPersist=yes -o ControlPath=/tmp/pi-cm-<pid>.sock <remote>` and shuts it down cleanly on exit. The command-line `-o ControlPath` overrides the user's unwritable path.
4. The remote `pwd` is resolved with a direct connection (`-o ControlPath=none -o ControlMaster=no`) so a read-only system `ControlPath` can't make the initial probe fail
5. All tool calls multiplex over the socket with `-o ControlMaster=no -o ControlPath=<socket>` — near-zero per-call overhead
6. The system prompt is patched to tell the LLM it's operating on `<remoteCwd> (via SSH ControlMaster: <remote>)`
7. User `!` shell commands are also routed over SSH
The status bar shows `⚡ own master` or `⚡ system master` so you can see which path was taken.