Commit graph

4 commits

Author SHA1 Message Date
5b970f8b48 Hard-deny: PowerShell patterns + drift-proof the label list
Four new compiled-in hard-deny rules covering PowerShell + cmd.exe
catastrophic patterns (mirror of the POSIX 10):

- Remove-Item / del / rd / ri / rm / erase / rmdir targeting C:\
  or user home / appdata
- Format-Volume / Clear-Disk with any flag (= an invocation, not a
  Get-Help lookup)
- iwr | iex pipe form (PowerShell web-to-execute)
- iex (irm ...) parenthesized form

Universal application — no shell-aware scoping yet. PS cmdlet
identifiers are distinctive enough that bash false-positives are
vanishingly unlikely. Shell-aware policy scoping remains a known
follow-up.

Drift-proof the "Always blocked" label list: backend now exposes
hard_deny_rules() via a new mcp_hard_deny_labels Tauri command, and
PolicyTab loads it at mount instead of hardcoding the list. Avoids
the 11→15 manual sync that would have been needed (and that had
already drifted twice this week).

cargo test --lib: 138 passed; 0 failed (118 prior + 20 new fuzz
cases for rules 11-14; hard_deny_rules_count bumped 10 → 14).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:14:42 +01:00
9ebb3e4d2e 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>
2026-05-26 16:04:14 +01:00
bf2810a433 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).
2026-05-26 14:50:06 +01:00
464c576b79 MCP v2 PR-1: policy engine + audit log + Config/Audit/Policy panel tabs
Foundation for Claude-drives-the-workspace writes. Nothing wired
end-to-end yet (App.tsx dispatcher comes next); this lands the
machinery + UI.

mcp_policy.rs (new) — three-tier allow/ask/deny policy with
deny-first precedence and a compiled-in non-overridable hard-deny
list (10 patterns covering rm -rf /, fork bombs, mkfs on device, dd
to raw disk, /etc/passwd overwrite, curl|sh, chmod -R 777 /, etc.).
Shell-operator-aware glob matcher mirroring Claude Code's Bash(*)
syntax. Restrictive default — empty policy means every non-hard-
denied call falls to Ask. Persisted to mcp-policy.json in
app_config_dir. Includes a PolicyClassifier scaffold (no-op) for a
future v2.1 LLM-classifier hook. 1152 lines incl. ~100 unit + fuzz
tests covering the matchers and lookalike negatives.

mcp.rs — TileService now holds AppHandle + Arc<PendingActions>
(oneshot registry keyed by uuid). New async dispatch_action helper
runs the policy check, emits "mcp://request" for the frontend to
handle, awaits a oneshot reply (30s timeout), then emits "mcp://
audit" with the outcome regardless. set_label tool wired through
this path as the demo for PR-1b's dispatcher.

commands.rs / lib.rs — new Tauri commands mcp_action_reply,
mcp_policy_load, mcp_policy_save; PendingActions registered as
managed state.

McpPanel.tsx — refactored into Config / Audit / Policy tabs.
AuditTab listens on mcp://audit, keeps a 200-entry ring with
ok/denied/failed chips. PolicyTab edits the allow/ask/deny buckets
(stacked vertically — three columns overflowed the panel) and shows
the hard-deny rules read-only at the bottom with "Cannot be
disabled" badges. Themed scrollbar on mcp-body to match xterm panes.

Caveat: set_label calls from Claude will currently time out — the
App.tsx side that listens on mcp://request and replies via
mcp_action_reply lands in PR-1b.

Co-authored by Sonnet (policy engine, backend plumbing, panel UI)
and Haiku (hard-deny fuzz test suite); integration + bug fixes here.
2026-05-26 12:05:31 +01:00