MCP v2 PR-4: add_host + delete_host + extraArgs sanitiser + third SSH safeguard

Final v2 PR. All 11 planned write tools live. add_host/delete_host let
Claude mutate the saved-hosts list; both gated by a new allowAddHost
switch (default off) — symmetric with the allowOpenSsh gate from PR-3.5.

add_host's extraArgs are sanitised against CVE-2023-51385-class
local-RCE primitives: ProxyCommand, LocalCommand, KnownHostsCommand,
PermitLocalCommand=yes are refused server-side. Recognises both -o KEY=VAL
and -oKEY=VAL, case-insensitive on the key. The manual host manager UI
stays unrestricted (user has full agency over their own hosts).

Also fixes a pre-existing compile bug: mcp_policy.rs's policy_with test
helper was missing the ssh_safeguards field added in PR-3.5, silently
breaking the entire policy test module since then. Re-enabling those
tests is the prereq for the hard-deny rework that follows in the next
commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-26 16:04:14 +01:00
parent 71f330e934
commit 9ebb3e4d2e
8 changed files with 513 additions and 5 deletions

View file

@ -48,9 +48,43 @@ Durable memory for this project. Read at session start, update before session en
- **Notifications**: `notify(message)` for status updates Claude wants to surface.
- Authentication: bind to localhost only; consider a per-session token written to the app config dir that the MCP client must present. Treat the MCP socket as trusted only to processes the user explicitly points at it — anyone with access to the user's account could read commands and stream PTY output. Surface this caveat in the help overlay.
- Tauri integration: Rust-side MCP server using a published crate (or hand-rolled JSON-RPC); reuses the existing `PtyManager` + `hosts.json` + workspace state. Frontend gets read-only events when the MCP causes a layout change so the UI reflects it without races. Big — milestone-scale work; needs a design doc before code.
- **Status:** v1 (read-only, 2026-05-25) + v2 (write surface, 2026-05-26 across PRs 14) shipped. All 11 originally-planned write tools are live: set_label, close_pane, swap_panes, promote_pane, apply_preset, spawn_pane, connect_host, write_pane, add_host, delete_host. Open polish items live in the per-session-log "follow-ups" sections.
## Session log
### 2026-05-26 — MCP v2 PR-4: `add_host` + `delete_host` + extraArgs sanitiser + third SSH safeguard
Final v2 PR. All 11 planned MCP write tools now live. Mechanically the same dispatcher shape as the other tree-shape tools; the novel bits are the **extraArgs sanitiser** and the **third SSH-safeguard switch**.
**Sanitiser (`hosts::sanitize_extra_args`).** Rejects four `-o KEY=...` keys that are local-RCE primitives at ssh-invocation time, before the connection is even attempted:
1. `ProxyCommand=…` — runs a shell command on connect.
2. `LocalCommand=…` — runs a shell command on connect (when `PermitLocalCommand=yes`).
3. `KnownHostsCommand=…` — runs a shell command at handshake (CVE-2023-51385 class).
4. `PermitLocalCommand=yes` — unlocks LocalCommand even if not set in this snippet. (`=no` and unset are fine.)
Recognises both two-arg form (`-o KEY=VAL`) and joined form (`-oKEY=VAL`), case-insensitive on the key, equals-or-space between key and value. Returns `Err(reason)` with the offending arg + a human-readable why. 19 fuzz tests cover positive + lookalike-negative cases (e.g. `-o ServerAliveInterval=30` passes; `-o proxycommand=evil` fails; bad arg in the middle of a long list fails). **Only the MCP `add_host` path runs this** — manual host management via the titlebar 🔑 picker stays unrestricted, matching the "user has full agency" stance.
**Third SSH safeguard: `allowAddHost`** (default off). Gates both `add_host` and `delete_host` with the same `add-host-disabled` server-side error pattern as the existing `allowOpenSsh` gate. Bundled both tools under one switch for simplicity — `delete_host` is destructive but it's the natural symmetric companion to `add_host`. UI is a third checkbox in the SSH safeguards section; unlike `autoAllowSpawnedSsh`, this one isn't disabled-when-X (you can let Claude manage hosts without letting it open them, or vice versa).
**Both tools are thin dispatcher wrappers**, following the PR-2/PR-3 pattern exactly: arg struct → safeguard gate → in-process validation → `dispatch_action` with stable `args_repr` → frontend `runMcpHandler` case + `buildConfirmInfo` case. `add_host` runs `pty::validate_ssh_token` on hostname/user/jumpHost (made `pub` for cross-module use; same logic ssh-spawn would do anyway, just rejected earlier with a clearer error) plus the sanitiser on extraArgs. `delete_host` looks the host up in `state.mirror.hosts` so Claude can't probe arbitrary ids, and relies on `save_ssh_hosts`' existing orphan-credential sweep to clean up the keyring entry.
**Backend host_id is generated frontend-side** in the handler (via the same `newId()` helper HostManager uses → `crypto.randomUUID()` shape). Backend doesn't pre-generate one because the dispatcher contract is "MCP call → emit request → frontend mutates + resolves" — generating the id on whichever side actually performs the mutation keeps responsibility clean.
**Pre-existing bug fixed as a prerequisite:** `mcp_policy.rs`'s `policy_with` test helper was constructing `McpPolicy` without the `ssh_safeguards` field (added in PR-3.5). That made the entire `tests` mod fail to compile, silently breaking all 30+ policy unit tests since 2026-05-26 morning. Added `ssh_safeguards: SshSafeguards::default()` as one-liner; tests should compile again.
**Module headers + `with_instructions` updated** to call out the new 11-tool surface, `add_host`'s extraArgs sanitiser, and the `add-host-disabled` error string Claude needs to recognise. Always keep these in sync when adding tools — Claude reads `with_instructions` for routing decisions.
Open follow-ups specific to this session:
- **Verify on Windows.** PR-4 was authored in WSL; `pnpm check` is clean but Rust build/tests live on the Windows host. User to `cd D:\dev\tiletopia && cargo test -p tiletopia_lib` (or the equivalent) before merging, especially to confirm the 19 new sanitiser tests + the policy_with fix.
- **End-to-end test with Claude.** Suggested smoke: toggle the new `allowAddHost` switch on; ask Claude to `add_host` with hostname `example.com`, then `connect_host` to it (which still needs `allowOpenSsh`), then `delete_host`. With all three switches off, `add_host` should refuse cleanly with `add-host-disabled`.
- **Race in concurrent `add_host` calls.** Frontend reads `hosts` from the closure, builds `next = [...hosts, newHost]`, calls `setHosts(next)` (non-functional updater). If Claude burst-fires two add_host calls and the second runs before React commits the first, the second's `next` drops the first. Pre-existing pattern (`saveHosts` in App.tsx:466 does the same), and in practice the confirm-modal queue serialises calls — but `Always allow add_host` users would race. Convert to `setHosts(prev => …)` + extract the saved snapshot if it ever bites.
- **Sanitiser scope expansions to consider:** `-F <path>` lets the user point ssh at a custom config file that could contain ProxyCommand. Currently allowed. Tightening this means rejecting any caller-controlled config file. Out of scope for v2 — `add_host` doesn't expose a flag for it, and saved hosts are user-edited.
- **PowerShell hard-deny patterns** still POSIX-only (carried over from PR-3 list).
- **Per-leaf-shellKind policy scoping** still wanted (carried over).
- **CLAUDE.md still says Svelte 5** (still not fixed; called out in 4 session logs now).
### 2026-05-26 — MCP v2 PR-3 + PR-3.5: powerful writes + SSH safeguards + host-manager Connect button
Commits `bf2810a` (PR-3 + PR-3.5) and `6da7523` (polish bundle). 8 of 9 planned v2 tools are now live — only `add_host` (PR-4) remains.