Terminal file viewer/editor + per-window app-command lockdown #60

Merged
jknapp merged 37 commits from feat/terminal-file-viewer into main 2026-09-23 17:05:50 +00:00
Owner

Two pieces of work in one PR, so the preview build gives a single testable artifact.

1. Terminal file viewer/editor

File locations in terminal output (src/foo.ts:42, path#L40-L50, OSC 8 file:// links) become clickable. Each click opens the file in its own OS window, a CodeMirror 6 editor that scrolls to and highlights the line.

  • Path resolution: relative paths are resolved by probing /workspace, then /workspace/<mount>. Several matches bring up a picker; no match brings up a "not found" window.
  • Editing: live reload every 2 s while you have no unsaved edits. Explicit save (Ctrl/Cmd+S) checks the file's hash first, so a concurrent change shows a "Changed on disk" banner instead of being overwritten. Saves keep CRLF line endings, a BOM and the file's mode, and refuse files that are read-only for the container user. Closing with unsaved edits asks for confirmation.
  • Hardening: each Rust command checks which window called it, and the Rust side keeps track of which file each window may touch; viewer windows never send a path. Existing http(s) and OSC 8 link behaviour is unchanged, and the "Shift+click" hint on file paths is enforced, not just shown.
  • Docs: spec docs/superpowers/specs/2026-09-22-terminal-file-viewer-design.md, plan docs/superpowers/plans/2026-09-22-terminal-file-viewer.md.

2. Per-window app-command lockdown (AppManifest)

  • build.rs declares a Tauri AppManifest, so every app command is now ACL-gated. main is granted 110 commands; file-viewer-* gets exactly the 5 viewer_* commands.
  • Build check (command_census.rs): the build fails if the command list and the capability files disagree. It also fails on a bare deny-*, a stray or inline capability source, and webviews/remote keys.
  • Tests:
    • A Rust test checks the generated acl-manifests.json against generate_handler!.
    • A vitest AST scan checks that each window's code only imports the wrappers that window is granted.
  • Docs: the threat model (capabilities/default.json) and CLAUDE.md are updated. Spec docs/superpowers/specs/2026-09-22-app-manifest-lockdown-design.md.

Verification

  • Automated: tsc is clean, vitest passes 975/975, npm run build succeeds, and cargo test passes 675 + 3, with no new clippy warnings. Each task was reviewed, and the whole branch got a final review.
  • Manual (not run yet): nothing has been exercised in a real Tauri window. Please run the Manual verification checklist at the end of the viewer spec on the preview artifact.
  • CI: CI runs neither cargo test nor vitest, so the build-time census check is the only automated guard on merge. Consider adding both test steps to build-app-preview.yml.

🤖 Generated with Claude Code

Two pieces of work in one PR, so the preview build gives a single testable artifact. ## 1. Terminal file viewer/editor File locations in terminal output (`src/foo.ts:42`, `path#L40-L50`, OSC 8 `file://` links) become clickable. Each click opens the file in its own OS window, a CodeMirror 6 editor that scrolls to and highlights the line. - **Path resolution:** relative paths are resolved by probing `/workspace`, then `/workspace/<mount>`. Several matches bring up a picker; no match brings up a "not found" window. - **Editing:** live reload every 2 s while you have no unsaved edits. Explicit save (Ctrl/Cmd+S) checks the file's hash first, so a concurrent change shows a "Changed on disk" banner instead of being overwritten. Saves keep CRLF line endings, a BOM and the file's mode, and refuse files that are read-only for the container user. Closing with unsaved edits asks for confirmation. - **Hardening:** each Rust command checks which window called it, and the Rust side keeps track of which file each window may touch; viewer windows never send a path. Existing http(s) and OSC 8 link behaviour is unchanged, and the "Shift+click" hint on file paths is enforced, not just shown. - **Docs:** spec `docs/superpowers/specs/2026-09-22-terminal-file-viewer-design.md`, plan `docs/superpowers/plans/2026-09-22-terminal-file-viewer.md`. ## 2. Per-window app-command lockdown (AppManifest) - `build.rs` declares a Tauri AppManifest, so every app command is now ACL-gated. `main` is granted 110 commands; `file-viewer-*` gets exactly the 5 `viewer_*` commands. - **Build check (`command_census.rs`):** the build fails if the command list and the capability files disagree. It also fails on a bare `deny-*`, a stray or inline capability source, and `webviews`/`remote` keys. - **Tests:** - A Rust test checks the generated `acl-manifests.json` against `generate_handler!`. - A vitest AST scan checks that each window's code only imports the wrappers that window is granted. - **Docs:** the threat model (`capabilities/default.json`) and CLAUDE.md are updated. Spec `docs/superpowers/specs/2026-09-22-app-manifest-lockdown-design.md`. ## Verification - **Automated:** `tsc` is clean, vitest passes 975/975, `npm run build` succeeds, and `cargo test` passes 675 + 3, with no new clippy warnings. Each task was reviewed, and the whole branch got a final review. - **Manual (not run yet):** nothing has been exercised in a real Tauri window. Please run the **Manual verification checklist** at the end of the viewer spec on the preview artifact. - **CI:** CI runs neither `cargo test` nor vitest, so the build-time census check is the only automated guard on merge. Consider adding both test steps to `build-app-preview.yml`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jknapp added 37 commits 2026-09-23 13:50:05 +00:00
Follow-up to the terminal file viewer spec. Verified against tauri 2.11.0,
tauri-build 2.6.0 and tauri-utils 2.9.0 sources: with no app manifest the
ACL skips app commands entirely; with one, every command must be granted
to the calling window, unlisted commands are denied, and deny-* is global.
The spec derives the manifest from generate_handler! in build.rs and fails
the build when a command is missing from, or duplicated across, the
capability files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review the approved design against tauri 2.11.0, xterm 5.5.0, Vite 6.4.1
and the tree: record the verified multi-window, capability, CSP and exec
facts, choose the save mechanism (stage in /tmp, swap in as the container
user), make polling a sha256sum exec, store realpath-resolved targets, and
pin the exact viewer capability set. Add the task-by-task plan with its
parallel groups and the verification commands that work here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Task 0 of the terminal file viewer plan: the shared interfaces that seven
later tasks build against in parallel. Adds ViewerLocation/ViewerTargetState/
ViewerState/ViewerFile/ViewerPoll to types.ts and their invoke() wrappers to
tauri-commands.ts, creates the file_viewer Rust module (mod.rs with
MAX_VIEWER_WINDOWS/VIEWER_LABEL_PREFIX/is_viewer_label, plus placeholder
registry/resolve/poll/write/window submodules), wires it into lib.rs, and
loosens visibility on the file_commands.rs helpers the viewer commands will
reuse (MAX_READ_BYTES, validate_container_path, validate_container_write_path,
FetchedFile, fetch_container_file, require_running, clip_container_text).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds joinWrappedRows/offsetToCell for reassembling a wrapped xterm
line into its full text plus offset->cell mapping, so link matching
can find paths that xterm has soft-wrapped across rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 2 of the terminal file viewer plan: ViewerRegistry tracks which
file-viewer-<n> window is looking at which container path. reserve()
takes the cap check and label allocation atomically under one lock so
two concurrent open requests cannot both slip past the 20-window cap;
find_open() only matches windows in the Resolved state, so a window
still choosing a candidate or reporting not-found is never treated as
"open on" a path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Implements resolve.rs: candidate_paths() turns a raw path Claude printed
into an ordered, capped, deduped list of container paths (absolute paths
pass through as-is; relative paths probe /workspace then each project
mount, in order), PROBE_SCRIPT + parse_probe_output() run one exec that
prints realpath -e of every candidate that is a regular file, and
probe_candidates() wires the two together over exec_oneshot_streams_as.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds findFilePathLinks, a pure matcher that finds file paths (with
optional :line, :line:col, :start-end and #L42/#L40-L50 suffixes) in
a line of terminal text, skipping anything inside a URL and stripping
markdown/quote wrapping from the matched span.

Per pre-flight ruling P7, a slash-having token still requires the
basename to carry a real extension (any extension) or be a known
extensionless basename -- a bare and/or or src/components is no
longer treated as a path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- viewerState.ts: pure reducer for the clean/dirty, same/changed/gone,
  container-down and overwrite-on-save states (spec §5), plus the
  pollEffect/canSave helpers EditorPane will drive off.
- editability.ts: classifies a fetched file as text/image/binary and
  decides whether it is editable, deferring to Rust's readonly_reason
  when it refuses.

Per preflight P1, languages.ts/.test.ts move to Task 10 (needs the
CodeMirror packages Task 6 installs; out of scope for this task's
worktree). Per P3, the "reloaded" action now carries `truncated` and
`polledHash` so a poll-driven reload of a truncated (prefix-hash-only)
file adopts the polled full-file hash instead of re-triggering a
reload on every subsequent poll -- with a reducer test covering it.
Per P13, tightened the poll_failed/canSave test to start from a dirty
doc so it actually exercises containerDown rather than passing only
because the doc was clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 6 of the terminal file viewer plan: app/viewer.html plus
src/viewer/{main,ViewerApp}.tsx (placeholder ViewerApp for Task 11 to
replace), registers viewer.html as a second Rollup input in
vite.config.ts, adds the file-viewer capability restricted to
file-viewer-* windows (allow-listen/unlisten for the goto event,
allow-destroy for the close-button/prevent_close interaction,
allow-internal-toggle-devtools to match default.json's dev
convenience), installs the CodeMirror packages Task 10 builds
languages.ts on top of (P1), and pins the viewer entry/capability
with a Rust fallback-trap test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
poll.rs: one exec per tick that tests existence then hashes+stats the
file (sha256sum/stat), so the 2 s poll costs one exec instead of
re-downloading up to 1 MiB of archive per window per tick.

write.rs: sha256_hex/is_sha256_hex, MAX_WRITE_BYTES, and the save
script. Saving stages the payload in /tmp via the existing
write_file_to_container (owned by the container user, since the
Docker archive API writes as root), then an exec as `claude` checks
the base hash, swaps the file in with a same-directory rename when
the directory is writable (falling back to an in-place `cat >` when
it is not), and always cleans up the staged temp file via `trap`.
classify_write maps exit 0/3/4 to Saved/Conflict/Gone.

Applies three pre-flight rulings against the brief's literal text:
- P8: pulled the write script's argv shape and the size/hash checks
  into pure `write_command`/`check_write_input` helpers with their
  own unit tests, since both lived only inside the untested async
  `write_file` otherwise.
- P9: the brief's manual Docker smoke-test invocation
  (`sh w.sh save target tmp hash`) makes `$1` become "save" instead
  of the target, which is not what the script or the Rust caller
  expect. Verified in a throwaway container that invoking the file
  directly without the dummy "save" arg reproduces the Rust
  convention's `$1/$2/$3` correctly: exit 0 with the new hash and a
  removed payload on a clean save, exit 3 with the file untouched on
  a stale base hash, and exit 4 when the target is gone.
- P15: GNU sha256sum prefixes its output line with `\` when the
  path contains a backslash or newline. Without a fix that turns
  into a permanent false conflict (write.rs) and a blinded poll
  (poll.rs, hash: null forever). Both parsers now strip a leading
  `\`, and the script itself strips it from $actual before comparing
  to $expect. Verified against real sha256sum output in a container
  with a backslash-named file: the save no longer false-conflicts
  and the reported hash matches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Implements Task 10 of the terminal file viewer plan: highlightLine.ts
(line-range StateField + setHighlight effect), viewerTheme.ts (EditorView
theme + syntax HighlightStyle), and CodeEditor.tsx (the React wrapper with
getDoc/setDoc/goTo/focus).

Also implements languages.ts + languages.test.ts, moved here from Task 7
per preflight ruling P1 (they need @codemirror/* packages, which only
Task 6 installs).

Preflight rulings applied:
- P1: languages.ts/.test.ts live here, not in Task 7.
- P12: syntax colours are `--syntax-*` custom properties in index.css,
  not hard-coded hex.
- P13: languageFor tests assert `.not.toBeNull()` for mapped extensions
  and move README to the "returns null" case, instead of the vacuous
  `.resolves.toBeDefined()`.
- P14(c): readOnly extension array factored into readOnlyExt(readOnly)
  instead of being duplicated in CodeEditor.
- P16: no custom Mod-g binding; searchKeymap's Mod-Alt-g covers "go to
  line" already.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Registers a file-path ILinkProvider (after WebLinksAddon) that opens the
file viewer at the matched line, and turns on allowNonHttpProtocols so
OSC 8 file: targets reach createOsc8LinkHandler, which now parses every
target and refuses anything but file: and http(s):. The hover card gains
an "Open in viewer" variant, exposed as showFileCard(rawPath) so relative
paths are shown as printed (preflight P6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
showFileCard now records modifierPromised like the OSC 8 hover, and the
file-path provider's gate goes through the handler's new opensFileLink,
so a "Shift+click to open" card cannot be answered by a bare click after
the container drops mouse tracking. A click before the session's project
is known now toasts instead of doing nothing. Refusal-card tests assert
the card is present; a misplaced test comment is back on its test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fix round 1 for Task 3, addressing task-3-review.md's I1-I3 (reproduced
under dash) plus M3 and M10 from the same review.

- I1: WRITE_SCRIPT read the target's hash through `sha256sum | cut … ||
  exit 1`. POSIX sh has no pipefail, so that `|| exit 1` tested only
  cut's exit status — an unreadable target (EACCES, EIO) left $actual
  empty, which never equals $expect, so the script silently reported
  exit 3 (conflict) instead of a real error. The user got a misleading
  "changed on disk" banner whose "Overwrite on save" could never
  succeed, since the next poll hit the same read error. Fixed by
  reading the hash from a plain command substitution
  (`actual=$(sha256sum -- "$target") || exit 1`) and splitting out the
  hash field in shell instead of piping into `cut`.

- I2 (+ M3): a failed `cp` into the staged file (ENOSPC, quota, EFBIG,
  EIO) left a partial `.<name>.triple-c-<pid>` behind in the user's
  own directory — the EXIT trap only ever removed $tmp. Fixed by
  creating the staged file with `mktemp` (M3: exclusive, unpredictable
  name, so it can't be planted or follow an existing symlink) and
  adding it to the trap as soon as it's assigned
  (`trap 'rm -f -- "$tmp" ${staged:+"$staged"}' EXIT`), so any later
  failure — cp, chmod, mv, or a signal — cleans it up too.

- I3 (controller ruling): the script only ever checked `[ -w "$dir" ]`,
  so a 0444 file (or one owned by another uid) was silently replaced
  via rename, defeating the file's own write protection even though
  spec §5 step 3 reads that way literally. Added `[ -w "$target" ]`
  before the branch; a non-writable target is refused with "The file
  is read-only for the container user." on stderr and a distinct exit
  code (5, `EXIT_READ_ONLY`) that `classify_write` maps to that same
  message rather than falling into the generic clipped-stderr arm.

- M10: added six `#[cfg(unix)]` tests that run WRITE_SCRIPT for real
  via `sh -c` against a temp directory on the host (not just needle
  matches against the script text) — clean save, stale-base conflict,
  gone target, unreadable target (I1), read-only target (I3), and a
  failed stage leaving no partial file behind (I2). The unreadable/
  read-only tests self-skip with a message if permission bits turn out
  not to block root, rather than false-failing under a root test
  runner.

Verified: `cargo test --offline file_viewer` — 19/19 passing, pristine
(up from 12; 6 new host-execution tests plus 1 for the new exit-5
classify_write arm). `cargo clippy --offline` (and `--tests`) — no
warnings in file_viewer::write; the 28 warnings clippy reports are
all pre-existing, in unrelated files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EditorPane loads the resolved file, polls it every 2 s while visible,
reloads a clean buffer silently and shows the "Changed on disk" banner
for a dirty one, saves against the loaded hash, and intercepts closing
with unsaved edits. ViewerApp routes to the editor, the not-found list
or the choose list.

Preflight rulings carried: one reload helper that passes the truncated
flag and polled hash (P3/P14), a poll right after a save conflict so
Overwrite on save adopts the current hash (P4), a chunked base64
encoder (P5), StatusIndicator for the badge (P11), banner-only test
queries (P2), and a Range geometry stub for jsdom (P17). A save the
container user may not write is reported as read-only and keeps the
buffer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A second click while the first window was still being built removed its
registry entry, giving a broken window, a duplicate and a cap bypass. The
registry now records when a window is built; `reserve` dedupes, prunes only
built entries whose window is gone (any state, so a leak cannot hold a cap
slot), and enforces the cap in one critical section. Choosing a file already
open elsewhere focuses that window and closes the chooser instead of
resolving a second entry. The not-running sentence names the real action.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Keep CRLF (or CR) line endings and a UTF-8 BOM through the editor:
  textFormat.ts records the dominant separator and the BOM on load and
  restores both on save, so a save changes only the user's edits.
- A clean document whose reload failed retries on the next poll.
- A poll that overlaps a save, or was issued before one settled, is
  ignored instead of reading the pre-save hash as a change.
- A conflict whose follow-up poll has no hash shows an error with a
  Reload button rather than an Overwrite that could only conflict again.
- Match write.rs's exact read-only message; show the read-only reason as
  visible text; error banners are role="alert".
- vite/client types move to src/vite-env.d.ts.
- Tests: CRLF and BOM saves, reload retry, poll/save race, null-hash
  conflict, Save and close success and failure, and CodeEditor.setDoc
  keeping cursor and scroll.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add CLAUDE.md structure notes for the viewer/ frontend and file_viewer/
backend, plus a Key Conventions reminder that a new local window needs its
own capability file and lib.rs's on_window_event guard. Extend default.json's
threat-model census to name file-viewer.json and its allow-destroy grant.

Tighten the capability test in file_viewer/mod.rs from a substring check to
a parsed serde_json assertion of the exact windows list and permission set
for both capability files, per Task 12's controller ruling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- CLAUDE.md: document the viewer.html fallback trap (missing/broken Vite entry
  silently serves index.html into the viewer window) and point at the Rust test
  that pins it.
- Give the manual verification checklist a durable, tracked home: append it as
  a markdown task list to the terminal-file-viewer design spec, including the
  file-path hover key-hint wording check and the CRLF/BOM round-trip save check.
- default.json: state the app-command residual risk and the pending AppManifest
  lockdown directly in the capability file's own description, not only in
  CLAUDE.md, since this file is the reviewed threat model of record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- write.rs: a save's new base is sha256 of the bytes written; the script's
  post-mv hash comes back as disk_hash, and a mismatch (another writer landed
  after us) shows "Changed on disk" instead of being adopted (ledger M2).
- write.rs: conflict:/gone:/read-only strings are constants with a pure
  saved_file() mapping and tests; app/src/viewer/ipcMessages.ts is the one TS
  copy and a cargo test checks it against the Rust originals.
- write.rs: the comment now says the in-place `cat >` fallback follows a
  planted symlink, and why that is accepted (runs as claude).
- poll.rs: a file deleted between `test -f` and `sha256sum` reads as gone.
- viewerState/EditorPane: poll_failed carries its message; only the
  "Start the project before" refusal reads as Container not running, anything
  else gets its own banner and leaves Save enabled.
- EditorPane: a failed first read shows Retry and is retried by the poll.
- spec §1: refused OSC 8 targets keep the refusal card (Task 9 ruling).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Five viewer commands (viewer_poll_file was added), both existing census
tests, the wrapper-import shape of the frontend, controller rulings on
CI scope / Box::leak / webviews, and a spike (§7) that reproduced every
mechanism claim against tauri-build 2.6.0 in a throwaway worktree. One
design change from the spike: stale autogenerated permissions are pruned
selectively rather than by remove_dir_all, because tauri-build emits
rerun-if-changed=permissions and wholesale regeneration would rebuild
the crate on every cargo invocation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Six tasks: shared command_census module with unit tests; the atomic
build.rs + grants + census-test commit with negative proofs; the
embedded-manifest read-back test; the vitest import-closure check; the
threat-model and CLAUDE.md rewrite; full verification and hand-off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves the generate_handler! parser out of the lib.rs test into
src/command_census.rs and adds the capability rules (one allow-* grant per
command, in the file its name says, no deny-*) with unit tests. No
behaviour change yet: build.rs does not use it until the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 1 (Minor promoted to required fix): the parser applied
rsplit("::").next() once per line, so `a::x, b::y,` on a single line
collapsed to one item and silently dropped a::x — a denied command at
runtime with nothing flagging it. Strip `//` comments per line first (a
whole-line comment strips to nothing, a trailing one leaves the code before
it), then split the cleaned text on `,` so every grant is its own item
regardless of how many share a line.

Adds two_commands_on_one_line_are_both_kept (the regression case) and
a_fixture_shaped_like_the_real_handler_list_parses_every_command (section
comments plus 1-, 2- and 3-segment paths, mirroring lib.rs's real shape).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
build.rs now derives an AppManifest from generate_handler!, which makes
tauri 2.11 apply the ACL to app commands (it skips them entirely without
one). default.json grants the 110 main-window commands, file-viewer.json
the five viewer_* commands, and build.rs refuses to build on a missing,
misspelled, duplicated, misfiled or deny-* grant, or on a hand-written
permission file. Stale autogenerated permissions are pruned per build.

Closes the residual risk recorded by the terminal file viewer: a
compromised viewer window could invoke any app command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tauri-build loads capabilities/**/*.{json,toml,json5} plus inline
app.security.capabilities from any tauri config or TAURI_CONFIG, but the
census read only top-level capabilities/*.json, so a .toml, a
subdirectory or an inline capability could over-grant a window with a
green build. build.rs now fails on any of those, on a JSON5/TOML tauri
config it cannot read, and on a `webviews` or `remote` key in a
capability file. OS/editor junk (.DS_Store, swap files) that tauri never
loads is skipped in capabilities/ and permissions/. Each failure is
headed by the check that failed rather than always "capabilities do not
match generate_handler!".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 2 already closed the risk (build.rs declares a Tauri AppManifest and
gates every app command per window); this task brings the docs in line so
the threat model of record no longer claims app commands are ungated or
that any local window can call any app command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Task 2's fix round (008c8c0 on feat/app-manifest-lockdown) taught build.rs
to refuse any capability source command_census.rs can't see — non-top-level
capabilities/ entries, .toml/.json5, webviews/remote keys, inline
app.security.capabilities in any tauri config or TAURI_CONFIG — to name
which check failed, and to skip OS/editor junk. Documents those checks in
both capability descriptions and CLAUDE.md's Key Conventions, plus the
known gap that a new tauri.<platform>.conf.json only takes effect on a
clean or CI build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes four plan-mandated Important findings from review round 1:
namespace imports of tauri-commands.ts are resolved into the wrapper
set (not dropped), export{...}from/export*from re-exports are
followed by the closure walk and counted as wrapper usage, relative
dynamic import() is followed by the closure walk and also checked at
the @tauri-apps/api/core boundary, and an unresolvable non-relative
specifier now throws (fail-closed) instead of silently exiting the
closure. Also narrows the asset-extension filter to an allowlist of
.ts/.tsx/.js/.jsx as source, everything else treated as a non-source
asset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fix round 2: re-review found the resolve-based namespace-import fix
from round 1 unsound for non-dot-access uses. fn(X), const y = X, and
X["name"]/X[expr] all hand the whole tauri-commands.ts namespace
object somewhere the member-access regex can't follow, and none of
them failed closed. In any viewer-closure file that namespace-imports
tauri-commands, every occurrence of the alias after its import line
(comments and strings stripped, best-effort) must now be a plain
alias.identifier member access or the test throws, naming the file
and telling the author to use named imports instead. Also fails
closed on `export * as ns from ".../tauri-commands"`, which the
member-access scan can't audit either. Both checks are scoped to the
viewer side of the ACL boundary (wrapperNamesImportedBy's new strict
parameter) since that's where a missed case is a real escape; the
main-window count stays permissive as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fix round 3: re-review found stripCommentsAndStrings collapsed whole
backtick template literals, including ${...} substitutions, before the
namespace-alias occurrence scan ever saw them. A tagged template hands
each substitution's value to the tag function by reference,
unstringified, so tag`${X}` smuggled the tauri-commands.ts namespace
object past the check exactly like fn(X) does, and neither threw.

Replaced the regex-based comment/string stripper with a small
hand-rolled scanner (skipQuoted/scanSubstitution/maskTemplate) that
drops literal template text but keeps a substitution's source intact,
recursively re-stripped for its own comments/strings/nested templates,
so an alias referenced only inside ${...} stays visible to (and, when
used via member access, correctly counted by) the occurrence scan.
Unterminated strings/comments/templates/substitutions now throw
(fail-closed) rather than running off the end of the text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace the regex/hand-rolled lexer with ts.createSourceFile + AST walking:
module edges from import/export-from/literal import(); aliases, query
suffixes, computed import(), require, import.meta.glob and any non
member-access use of a tauri-commands namespace alias throw; wrapper to
command extraction is read from the AST too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Relative specifiers now follow Vite 6's tryCleanFsResolve order (exact
file, js->ts twin, .mjs/.js/.mts/.ts/.jsx/.tsx/.json, then index), so a
dotted name like ./evil.impl and a .mjs shadowing a .ts resolve to the
file Vite loads. The @tauri-apps/api(/core) boundary covers every code
file under src/, tests included; the main-window count includes
.js/.mjs/.mts/.jsx sources. tauri-commands.ts must call invoke inside a
wrapper's function body and may not load modules dynamically.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs(acl): reconcile spec prose with the shipped implementation
Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 7s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m49s
Build App (Preview) / build-linux (pull_request) Successful in 5m16s
Build App (Preview) / build-windows (pull_request) Successful in 10m4s
Build App (Preview) / prune-previews (pull_request) Successful in 9s
71ba5076db
Final-wave cleanups from the whole-branch review (final-review.md Minor
1-5): spec §4 now says selective pruning, not "deletes the directory
every build"; spec §3.3 now describes the TypeScript-AST scan
(fail-closed Vite-order resolution, namespace imports as member access
only, the every-code-file boundary check) instead of the old
regex/chunk description; the viewer spec's historical "every command
is callable from every window" line gets a dated "closed by the
AppManifest lockdown" note; the lib.rs doc comment on
the_generated_app_manifest_matches_the_handler_list no longer claims
independence from the shared parser it actually reuses; and the vitest
command-name regex now allows digits, matching Rust's [a-z0-9_]+.

Also adds a cargo test backstop
(the_tauri_config_capability_check_runs_against_the_real_tree) that
runs build.rs's tauri-config capability check against the real
app/src-tauri tree on every `cargo test`, closing the gap where a new
tauri.<platform>.conf.json on an already-built tree only gets checked
by build.rs on a clean build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jknapp merged commit 8305c96e20 into main 2026-09-23 17:05:50 +00:00
jknapp deleted branch feat/terminal-file-viewer 2026-09-23 17:05:50 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: CyberCoveLLC/Triple-C#60