Terminal file viewer/editor + per-window app-command lockdown (#60)
Build App / compute-version (push) Successful in 7s
Secret Scan / scan (push) Successful in 8s
Build App / build-macos (push) Successful in 2m53s
Build App / build-linux (push) Successful in 5m12s
Build App / build-windows (push) Successful in 5m15s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 1m5s

Clicking a file path in Claude's terminal output now opens the file in its own window with a CodeMirror 6 editor. The editor highlights the target line, live-reloads while the file changes, and saves explicitly with hash-based conflict detection. The viewer commands are gated by window label.

Every app command is now ACL-gated per window through a Tauri AppManifest. build.rs checks the handler list against the capability files and fails the build on any mismatch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #60.
This commit is contained in:
2026-09-23 17:05:50 +00:00
co-authored by Claude Opus 5.5
parent 3537b234d8
commit 8305c96e20
65 changed files with 15043 additions and 108 deletions
+47 -3
View File
@@ -73,6 +73,16 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
- **`hooks/`** — All Tauri IPC calls are encapsulated in hooks (`useTerminal`, `useProjects`, `useDocker`, `useSettings`)
- **`lib/tauri-commands.ts`** — Typed `invoke()` wrappers; TypeScript types in `lib/types.ts` must match Rust models
- **`components/terminal/TerminalView.tsx`** — xterm.js integration with WebGL rendering, URL detection for OAuth flow
- **`viewer/`** — the terminal file viewer's window (second Vite entry `viewer.html`
`src/viewer/main.tsx`; CodeMirror 6). `lib/filePathLinks.ts` decides what a path is;
`components/terminal/filePathLinkProvider.ts` registers it with xterm. The OSC 8 handler now
runs with `allowNonHttpProtocols` on and dispatches `file:` to the viewer, so every other scheme
must be refused *there*. `viewer.html` must never carry an inline `<style>` — Tauri would add a
style nonce and CodeMirror's injected styles would stop applying. A missing or broken
`viewer.html` Vite entry is not caught by Tauri at build time — both Vite dev and Tauri's asset
lookup silently fall back to `index.html`, so the window just opens the *main app*, full UI and
all, with no error anywhere; `file_viewer::tests::the_viewer_entry_exists_and_is_a_vite_input`
in `file_viewer/mod.rs` is the only thing pinning this.
- **`components/layout/`** — TopBar, MainTabs (the unified tab strip), Sidebar, StatusBar
- **`components/projects/`** — `ProjectRow` (select-only list row), `ProjectList`, `AddProjectDialog`,
and the editors reused by Project Home
@@ -161,6 +171,19 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
Beyond docker/project/settings/terminal: `inspect_commands.rs` (read-only views into a
container — Claude sessions, installed capabilities, scheduler tasks), `auth_bridge_commands.rs`,
`auth_token_commands.rs`.
- **`file_viewer/`** — one window per click (`file-viewer-<n>`), a managed `ViewerRegistry`,
resolution by probing `/workspace/<p>` then `/workspace/<mount>/<p>` in one exec as `claude`,
polling by `sha256sum`, saves staged in `/tmp` and swapped in by a `sh` script as the container
user (spec §5 says why the archive API never writes to the target directory). Commands take
`window: tauri::Window`, gate on the label and act on the caller's own registry entry — no
viewer command accepts a path. Which window may *call* each command is the ACL's job: the
`file-viewer-*` capability grants exactly the five `viewer_*` commands (see `build.rs`).
- **`build.rs` + `src/command_census.rs`** — the build declares a Tauri `AppManifest` from the
`generate_handler!` list and refuses to build unless every command has exactly one bare
`allow-*` grant in the capability file its name says it belongs to. The parser and rules are
in `command_census.rs`, compiled into both the build script and the test build, so they are
unit-tested; `the_generated_app_manifest_matches_the_handler_list` reads back what tauri
embedded. Design: `docs/superpowers/specs/2026-09-22-app-manifest-lockdown-design.md`.
- **`auth_bridge/`** — Host-side loopback bridge so browser logins run *inside* a container can
complete against the host browser. Discovers listeners by parsing `/proc/net/tcp{,6}` (the image
has no `ss`/`netstat`/`lsof`), binds host `127.0.0.1` **only**, and tunnels in over the Docker
@@ -578,9 +601,28 @@ Anthropic and Bedrock deliberately keep Claude Code's own defaults.
- Frontend types in `lib/types.ts` must stay in sync with Rust structs in `models/`
- Tauri commands are registered in `lib.rs` via `.invoke_handler(tauri::generate_handler![...])`
- `capabilities/default.json` grants permissions for **plugin** commands only (`core:`, `dialog:`,
`store:`, `opener:`). Application commands registered through `generate_handler!` do **not**
need an entry there — adding one is not required and none exists for any app command.
- **A new command needs three things:** `#[tauri::command]`, a `generate_handler!` entry in
`lib.rs`, and a bare `allow-<name-with-dashes>` entry in the one capability file for the
window that calls it — `viewer_*` commands in `capabilities/file-viewer.json`, everything else
in `capabilities/default.json`. `build.rs` declares a Tauri `AppManifest` from the handler list
(without one, tauri 2.11 does not apply the ACL to app commands at all) and fails `cargo
check`/`tauri build` on a missing, misspelled, duplicated or misfiled grant, a `deny-*`, or a
hand-written file under `permissions/`. `src/test/capabilities.test.ts` fails if code that runs
in a window imports a `tauri-commands.ts` wrapper that window is not granted. Only `_` becomes
`-` in the identifier; `permissions/autogenerated/` is generated and ignored, and
`gen/schemas/*.json` is regenerated by every build and committed.
- **A new window needs its own top-level `capabilities/*.json`; never `webviews`/`remote`;
never inline.** `build.rs` only vouches for what `src/command_census.rs` reads — a top-level
`capabilities/*.json` file with a `windows` list — so it refuses to build on anything tauri
would load that the census can't check: a capability under a subdirectory or written as
`.toml`/`.json5`, a `webviews` or `remote` key in a capability file (either widens grants past
what `windows` says), `app.security.capabilities` declared inline in `tauri.conf.json`/any
`tauri.<platform>.conf.json`/`TAURI_CONFIG`, or a tauri config in a format it can't parse
(JSON5, TOML). OS/editor junk (`.DS_Store`, `Thumbs.db`, swap files) is recognised and skipped
rather than refused. Each failure names the check that failed, not just "capabilities do not
match generate_handler!". **Known limit:** adding a new `tauri.<platform>.conf.json` to a tree
that has already been built once only takes effect on a clean build or in CI — cargo's
incremental build has no reason to notice a file that did not exist on the previous build.
- The `projects.json` file uses atomic writes (write to `.tmp`, then `rename()`). Corrupted files are backed up to `.bak`.
- **Adding project state that changes the container?** `container_needs_recreation()` is entirely
**label-based** — it does not diff the container's env. If a new setting affects the container's
@@ -601,6 +643,8 @@ Anthropic and Bedrock deliberately keep Claude Code's own defaults.
`#[serde(default)]` on a `bool` yields `false`; follow the `default_full_permissions` pattern in
`models/project.rs` for anything that should default to true.
- Cross-platform paths: Docker socket is `/var/run/docker.sock` on Linux/macOS, `//./pipe/docker_engine` on Windows
- A new local window needs its own capability file (`capabilities/file-viewer.json` is the
model), and `lib.rs`'s `on_window_event` stays guarded on `label() == "main"`.
## Secrets