MCP v2 PR-3: write_pane, spawn_pane, connect_host + SSH safeguards

Three of the highest-power v2 tools, plus a defense-in-depth pass
on SSH-specific risk.

write_pane sends keystrokes (or any bytes) to a pane's PTY. The
policy engine matches against the text content directly so rules
like write_pane(npm test*) match by what would run, and the
compiled-in hard-deny catches rm -rf /, fork bombs, etc. regardless
of policy. Per-pane token-bucket rate limiter (30 calls / 10s,
3/sec refill) prevents a runaway loop from spamming the user with
confirm modals or burning audit-log capacity. The frontend handler
truncates the text in modal/audit summaries to ~60 chars + escapes
control characters so secrets pasted into write_pane don't echo
verbatim into the UI.

spawn_pane mirrors the existing SpawnSpec enum (WSL distro,
PowerShell, SSH) as the tool schema. New splitLeafWith helper
inserts a caller-built LeafNode (with a pre-generated id) so the
handler can await waitForPaneRegistration on that exact leaf before
replying with the resulting {leafId, paneId}. 15s spawn timeout
covers cold-start WSL distros; 30s for connect_host covers SSH
handshake + auth. Outer dispatch timeout bumped 30s → 60s. SSH
spawns without a saved hostId are refused — LeafNode only persists
sshHostId, no inline params, so use connect_host.

connect_host is a thin wrapper that looks up a saved SSH host by
id and routes through the same spawn machinery.

McpConfirm.tsx gains an optional ssh context — when the call
targets or spawns an SSH pane, a red warning banner renders
explaining that pattern matching is best-effort on the bytes we
send (remote shell expands aliases/subshells before executing).
buildConfirmSummary became buildConfirmInfo and returns the SSH
context alongside the summary string.

PR-3.5 — SSH safeguards. Two new switches in the Policy tab,
both off by default, both gated by mcp_policy::SshSafeguards:

  allowOpenSsh: when off, connect_host and spawn_pane(kind=ssh)
    refuse server-side with a clear "ssh-disabled" message pointing
    at the Policy tab. User must open SSH manually via the titlebar
    🔑 picker and toggle 🤖 on to grant Claude access.

  autoAllowSpawnedSsh: when off, an SSH pane Claude spawns starts
    with mcpAllow=false. User must explicitly toggle 🤖 before
    Claude can read scrollback or send keystrokes. The second switch
    is disabled in the UI when the first is off.

The safe-by-default design means a fresh install gives Claude no
ability to autonomously touch SSH — full safety with one click per
level to enable when consciously wanted. Both switches read fresh
per call so policy edits take effect without a server restart.

ErrorBoundary.tsx — last-resort guard against React render
exceptions. Wraps the App root + each MCP panel tab independently
so a bug in one tab doesn't blank the entire app. Shows a small
red error card with the exception message and a "Try again"
button. Caught a serde rename_all bug during PR-3.5 testing where
PolicyTab read policy.sshSafeguards but Rust serialized
ssh_safeguards (snake_case); without the boundary the whole window
went black.

newId() now exported from tree.ts for the splitLeafWith path.
McpPolicy struct gained #[serde(rename_all = "camelCase")] so
sshSafeguards survives the IPC round-trip cleanly; older policy
files without the field still load (serde defaults to safe).
This commit is contained in:
megaproxy 2026-05-26 14:50:06 +01:00
parent 3acad63fb7
commit bf2810a433
12 changed files with 844 additions and 41 deletions

View file

@ -710,3 +710,68 @@
color: #ccd;
border-color: #4488cc;
}
.mcp-confirm-ssh-warn {
background: #2a1a1a;
border: 1px solid #a04040;
border-radius: 4px;
padding: 8px 10px;
margin: 0 0 10px;
color: #e0a0a0;
font-size: 11px;
line-height: 1.5;
}
.mcp-confirm-ssh-warn strong { color: #ff8080; }
.mcp-confirm-ssh-warn code {
background: #0c0c0c;
padding: 1px 4px;
border-radius: 2px;
color: #ffcccc;
}
.mcp-confirm-ssh-warn em { color: #ffd0a0; font-style: normal; }
/* ---- SSH safeguards section ------------------------------------------- */
.policy-ssh-safeguards {
background: #1a1410;
border: 1px solid #4a2a1a;
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 12px;
}
.policy-ssh-safeguards .policy-bucket-header {
color: #d8a040;
border-bottom-color: #3a2a1a;
margin-bottom: 8px;
}
.policy-toggle-row {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
cursor: pointer;
border-top: 1px solid #2a1a10;
}
.policy-toggle-row:first-of-type { border-top: none; }
.policy-toggle-row input[type="checkbox"] {
margin-top: 3px;
accent-color: #d8a040;
flex-shrink: 0;
}
.policy-toggle-text {
font-size: 11px;
color: #b8a890;
line-height: 1.45;
}
.policy-toggle-text strong { color: #d8a040; display: block; margin-bottom: 2px; }
.policy-toggle-text code {
background: #0c0c0c;
padding: 1px 4px;
border-radius: 2px;
font-family: inherit;
color: #ffcc80;
}
.policy-toggle-row input:disabled + .policy-toggle-text {
opacity: 0.5;
}