README: refresh for today's shipped features (MCP v2, SSH, hard-deny)

Several sections were stale:

- Top feature list missing SSH host support, PowerShell, MCP, drag-to-swap.
- MCP section still claimed "v1 is read-only" — actually shipped 10 write
  tools (PR-1 through PR-4 today) plus three-tier policy engine, audit log,
  SSH safeguards, extraArgs sanitiser, and 14 compiled-in hard-deny patterns
  (with the rule-set rework that fixed the 9-of-10-rules-don't-actually-fire
  bug). Added a per-tool table.
- Test counts were 43 vitest cases; actual is 72 (frontend) + 138 (Rust).
  Added the `cargo test --lib` recipe and pointer to scripts/pr4-verify.mjs.
- Architecture section now covers hosts.rs / creds.rs / mcp.rs / mcp_policy.rs
  alongside pty.rs and the layout tree.

Marker block (auto-generated from shortcuts.ts) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-26 18:42:49 +01:00
parent 35194cd60c
commit 139730259a

View file

@ -2,11 +2,13 @@
A Windows desktop app for running and arranging many WSL terminals at once. Built primarily for managing multiple `claude` sessions across projects in parallel; works for any multi-shell workflow.
- Tiling layout — recursive splits, draggable dividers, preset layouts (single / 2-col / 3-col / 2-row / 2×2)
- Per-pane distro + cwd + label, persisted across restarts
- Tiling layout — recursive splits, draggable dividers, drag-to-swap pane headers, preset layouts (single / 2-col / 3-col / 2-row / 2×2)
- Three shell kinds per pane: WSL distros, PowerShell, saved SSH hosts (with optional Windows Credential Managerstored passwords for auto-typing at the prompt)
- Per-pane distro + cwd + label + font-size + broadcast state, persisted across restarts
- Broadcast input to a group of panes (per-pane 📡 chip, or global toggle in the titlebar)
- Idle-detection toasts when a pane goes quiet
- Idle indicator (red border + titlebar "N idle" badge) when a pane goes quiet
- Ctrl+K palette to fuzzy-jump between panes
- **MCP server** so a Claude session — Claude Desktop, Claude Code, or one running inside a tiletopia pane itself — can read scrollback, send keystrokes, spawn / close / swap panes, change layout, and manage SSH hosts. Tiered policy with confirm modals + a compiled-in hard-deny list (`rm -rf /`, fork bomb, `iwr | iex`, etc.) you can't disable.
## Install
@ -107,13 +109,28 @@ Layout + per-pane settings auto-save to `%APPDATA%\com.megaproxy.tiletopia\works
### MCP server (Claude can drive the workspace)
The titlebar 🤖 button opens a small panel that starts an MCP (Model Context Protocol) server. A Claude session — running anywhere reachable from the host, including inside one of tiletopia's own panes — can connect to it, read scrollback, wait for commands to settle, and inspect the layout. v1 is **read-only**: no spawning, no keystroke injection, no host editing.
The titlebar 🤖 button opens a small panel that starts an MCP (Model Context Protocol) server. A Claude session — Claude Desktop, Claude Code, or one running inside a tiletopia pane itself — connects to it, reads scrollback, waits for commands to settle, and (with your permission) drives the workspace: sends keystrokes, spawns / closes / swaps / reshapes panes, manages SSH hosts.
- **Off by default.** Click the button, hit **Server: ON** to start. The panel shows the URL + bearer token and a ready-to-paste Claude config snippet. Both port and token persist across restarts (saved to `%APPDATA%\com.megaproxy.tiletopia\mcp.json`); use **Regenerate** in the panel if the token leaks.
- **Off by default.** Click the button, hit **Server: ON** to start. The panel shows the URL + bearer token and a ready-to-paste config snippet. Both port and token persist across restarts (saved to `%APPDATA%\com.megaproxy.tiletopia\mcp.json`); use **Regenerate** if the token leaks.
- **Default-deny per pane.** Toggle the 🤖 chip in any pane's toolbar to allow MCP to see it. Panes without the chip on are invisible to the server.
- **Three-tier policy** (allow / ask / deny) with a confirm modal on every Ask, configurable in the panel's **Policy** tab. The default is "ask on everything"; add bare tool names like `set_label` to **allow** to skip the prompts, or globs like `write_pane(rm *)` to **deny** outright.
- **Compiled-in hard-deny list** of 14 patterns the user can NOT disable: `rm -rf /`, fork bomb, `dd of=/dev/sd...`, `curl | sh`, `Remove-Item -Recurse -Force C:\`, `iwr | iex`, etc. Checked against every `write_pane` text before policy. Best-effort accident prevention, not a sandbox — alias and quoting tricks bypass it.
- **SSH safeguards.** Three switches in the Policy tab, all off by default: `allow_open_ssh` (gates `connect_host` / `spawn_pane(ssh)`), `auto_allow_spawned_ssh` (gates whether spawned-by-Claude SSH panes start MCP-allowed), `allow_add_host` (gates `add_host` / `delete_host` saved-list edits). `add_host`'s `extraArgs` are also sanitised — `ProxyCommand` / `LocalCommand` / `KnownHostsCommand` / `PermitLocalCommand=yes` (CVE-2023-51385 class) are refused.
- **Audit log** in the panel's **Audit** tab — last 200 tool calls with arg summary, outcome, duration. Ephemeral (cleared on restart).
- **Saved SSH passwords are never exposed** through the MCP surface.
- **Bound to all interfaces** (`0.0.0.0`). The bearer token is the only auth — don't enable the server on an untrusted network.
**Tools currently exposed:**
| Tool | What it does |
|---|---|
| `read_pane(leaf_id, last_lines?, after_seq?)` | Read a pane's scrollback. Returns text + `__seq__=N` marker for incremental polling. |
| `wait_for_idle(leaf_id, idle_ms?, timeout_ms?)` | Block until a pane is quiet — useful for command-completion sync. |
| `write_pane(leaf_id, text)` | Send keystrokes. Rate-limited (30 calls / 10s per pane). Hard-deny + user policy apply. |
| `set_label`, `close_pane`, `swap_panes`, `promote_pane`, `apply_preset` | Tree-shape and metadata operations. |
| `spawn_pane`, `connect_host` | Open new local / SSH panes. SSH gated by `allow_open_ssh` safeguard. |
| `add_host`, `delete_host` | Manage the saved SSH hosts list. Gated by `allow_add_host`; `extraArgs` sanitised. |
#### Claude Desktop setup (one-click via `.mcpb` bundle — recommended)
The MCP panel has a **Download .mcpb** button that fetches a packaged Claude Desktop extension (an `.mcpb` file). Drag it into Claude Desktop's *Settings → Extensions* pane and Claude will auto-discover tiletopia — no config editing, no copy-pasting tokens.
@ -187,19 +204,27 @@ pnpm tauri build # NSIS installer at src-tauri\target\release\bundle\nsis
### Run the tests
```sh
pnpm test # vitest, 43 cases on the layout tree
pnpm test # vitest — currently 72 cases (layout tree)
pnpm test:watch
pnpm check # tsc --noEmit (strict TypeScript pass)
pnpm build # tsc -b && vite build — full production frontend bundle
```
The test suite covers the pure helpers in `src/lib/layout/tree.ts`. UI behavior, broadcast routing, and Tauri integration are manually tested.
```powershell
cd src-tauri
cargo test --lib # 138+ Rust unit tests (mostly hard-deny pattern fuzzing + extraArgs sanitiser)
```
The test suites cover pure helpers (`src/lib/layout/tree.ts`) on the frontend and the hard-deny regex set + SSH `extraArgs` sanitiser + MCP policy evaluator on the backend. UI behaviour, broadcast routing, MCP end-to-end, and Tauri IPC are manually verified — see `scripts/pr4-verify.mjs` for a Node-driven MCP smoke test you can run against the dev app.
## Architecture
- **Backend** (`src-tauri/src/pty.rs`): `PtyManager` holding `Mutex<HashMap<PaneId, PaneHandle>>` of `portable-pty` children. Each spawned pane gets a background reader thread that emits `pane://{id}/data` events (base64 byte chunks). Counterparts: `write_to_pane` / `resize_pane` / `kill_pane`. Workspace persistence via `save_workspace` / `load_workspace` writes to `app.path().app_config_dir()` with atomic tmp + rename.
- **Layout** (`src/lib/layout/tree.ts`): binary tree of splits. `HSplit | VSplit` internal nodes with a ratio, `Leaf` at the bottom — same model as i3 / tmux / Zellij. Adaptive resize falls out of mutating one parent ratio. Pure helpers (`splitLeaf`, `closeLeaf`, `changeDistro`, `setAllBroadcast`, etc.) live in `tree.ts` with 43 vitest cases; the rendering chain (`Pane.tsx``SplitNode.tsx` / `LeafPane.tsx`) is thin.
- **Orchestration** — broadcast routing, idle detection, palette, active-pane focus all live in `App.tsx`. Shared state and operations reach descendants through a React Context (`src/lib/layout/orchestration.tsx`), so each LeafPane reads `activeLeafId`, `distros`, and the tree-mutation methods directly via `useOrchestration()` — no prop drilling through the recursive Pane chain.
- **PTY backend** (`src-tauri/src/pty.rs`): `PtyManager` holding `Mutex<HashMap<PaneId, PaneHandle>>` of `portable-pty` children. Spawns `wsl.exe` / `powershell.exe` / `ssh.exe` depending on the leaf's `shellKind`. Each spawn gets a background reader thread that emits `pane://{id}/data` events (base64 byte chunks) AND mirrors bytes into a per-pane 256 KB scrollback ring that the MCP server reads from. Counterparts: `write_to_pane` / `resize_pane` / `kill_pane`. Workspace + hosts + MCP config persisted via `save_*` / `load_*` to `app.path().app_config_dir()` with atomic tmp+rename.
- **SSH hosts** (`src-tauri/src/hosts.rs`, `src-tauri/src/creds.rs`): `SshHost` shape with optional user / port / identity / jump host / extra ssh args. Passwords stored in Windows Credential Manager via `keyring-core` 1.0 + `windows-native-keyring-store` — never on disk, never in IPC events, never in the MCP surface. Reader thread autotypes the password at the first `password:` / `passphrase` prompt within 30 s of spawn, then disarms.
- **MCP server** (`src-tauri/src/mcp.rs`): embedded `rmcp` Streamable HTTP server on 127.0.0.1 with bearer-token auth, default port 47821 (overridable; falls back to OS-picked if taken). 12 tools — 2 read (`read_pane`, `wait_for_idle`) + 10 write (`set_label`, `close_pane`, `swap_panes`, `promote_pane`, `apply_preset`, `spawn_pane`, `connect_host`, `write_pane`, `add_host`, `delete_host`). Write tools dispatch through the event/reply pattern in [src-tauri/src/mcp.rs](src-tauri/src/mcp.rs) — frontend owns tree authority, backend emits a request event, frontend resolves via `mcp_action_reply`. Per-leaf `mcpAllow` gate (default-deny) filters what the server can see.
- **MCP policy** (`src-tauri/src/mcp_policy.rs`): three-tier `allow / ask / deny` precedence (deny-first), glob matcher (`*` only, no regex), shell-operator-aware subcommand splitting on `&& || ; | |& & \n`. **Hard-deny pass** runs against both the whole input and each subcommand to catch patterns that span operators (fork bomb, `curl | sh`). Hard-deny list is 14 patterns compiled in, non-overridable. Plus `SshSafeguards` switches for SSH-spawn / SSH-auto-allow / saved-host edits.
- **Layout** (`src/lib/layout/tree.ts`): binary tree of splits. `HSplit | VSplit` internal nodes with a ratio, `Leaf` at the bottom — same model as i3 / tmux / Zellij. Adaptive resize falls out of mutating one parent ratio. Pure helpers (`splitLeaf`, `closeLeaf`, `setLeafShell`, `swapLeaves`, `promoteLeaf`, etc.) live in `tree.ts` with 72 vitest cases; the rendering chain (`Pane.tsx``SplitNode.tsx` / `LeafPane.tsx`) is thin.
- **Orchestration** — broadcast routing, idle detection, palette, active-pane focus, MCP request dispatcher all live in `App.tsx`. Shared state and operations reach descendants through a React Context (`src/lib/layout/orchestration.tsx`), so each LeafPane reads `activeLeafId`, `distros`, and the tree-mutation methods directly via `useOrchestration()` — no prop drilling through the recursive Pane chain.
## License