Compare commits

..
Author SHA1 Message Date
shadowdaoandClaude Opus 5.5 71ba5076db 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
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>
2026-09-22 23:25:50 -07:00
shadowdaoandClaude Opus 5.5 a479bce639 test(acl): resolve like Vite and cover every code file in capabilities.test.ts
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>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 f407d23bc0 test(acl): scan capabilities.test.ts imports with the TypeScript AST, fail-closed
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>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 ccff01a13a fix(acl): keep template-substitution source visible to the namespace-alias scan
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>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 6d4f32e81c fix(acl): fail closed on namespace-alias value-smuggling and export-* re-export
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>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 5688534a4a fix(acl): close the closure-walk blind spots capabilities.test.ts missed
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>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 8bac2a9533 test(acl): each window's code imports only the wrappers it is granted
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 23:12:19 -07:00
shadowdaoandClaude Opus 5.5 e10b16a081 docs(acl): describe the 008c8c0 layout checks and the platform-conf known limit
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>
2026-09-22 22:38:32 -07:00
shadowdaoandClaude Opus 5.5 bc4980e014 docs(acl): threat model and conventions say app commands are ACL-gated
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>
2026-09-22 22:38:32 -07:00
shadowdaoandClaude Opus 5.5 1401369ffa test(acl): the embedded app manifest equals generate_handler!
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:36:11 -07:00
shadowdaoandClaude Opus 5.5 008c8c0328 fix(acl): refuse capabilities the census cannot see, and name the failed check
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>
2026-09-22 22:32:46 -07:00
shadowdaoandClaude Opus 5.5 05d991181d feat(acl): gate every app command per window via a Tauri AppManifest
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>
2026-09-22 22:26:18 -07:00
shadowdaoandClaude Opus 5.5 92a2d9de4a fix(acl): comma-split the handler parser so two commands on one line both survive
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>
2026-09-22 22:20:08 -07:00
shadowdaoandClaude Opus 5.5 ed6596436e refactor(acl): shared command census module for build.rs and tests
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>
2026-09-22 22:15:25 -07:00
shadowdaoandClaude Opus 5.5 7c8ad62da4 docs(acl): implementation plan for the AppManifest lockdown
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>
2026-09-22 22:12:25 -07:00
shadowdaoandClaude Opus 5.5 32acdf39fe docs(acl): revise lockdown spec for the implemented viewer and review rulings
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>
2026-09-22 22:12:25 -07:00
shadowdaoandClaude Opus 5.5 bf2291089a fix(viewer): final-review fixes — save base from written bytes, honest poll errors, retryable first read
- 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>
2026-09-22 21:52:43 -07:00
shadowdaoandClaude Opus 5.5 398281c8b5 docs(viewer): fix round 1 — fallback trap, tracked checklist, residual-risk framing
- 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>
2026-09-22 21:36:15 -07:00
shadowdaoandClaude Opus 5.5 90991fee32 docs: terminal file viewer structure and capability census
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>
2026-09-22 21:30:22 -07:00
shadowdaoandClaude Opus 5.5 16bfb3984c fix(viewer): byte-faithful saves, poll/save races, clearer errors
- 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>
2026-09-22 21:29:44 -07:00
shadowdaoandClaude Opus 5.5 804213a517 fix(viewer): atomic find-or-reserve so an in-flight window is never read as stale
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>
2026-09-22 21:23:25 -07:00
shadowdaoandClaude Opus 5.5 20e60b78a1 feat(viewer): viewer window UI with live reload, save and close guard
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>
2026-09-22 21:22:52 -07:00
shadowdaoandClaude Opus 5.5 caaf70a66c fix(viewer): real errors, no leftover temp files, refuse read-only saves
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>
2026-09-22 21:20:11 -07:00
shadowdaoandClaude Opus 5.5 d4783b3306 feat(viewer): label-gated viewer commands and window creation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 21:17:51 -07:00
shadowdaoandClaude Opus 5.5 a25d9c2be3 fix(viewer): hold file-path clicks to the card's modifier promise
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>
2026-09-22 21:17:34 -07:00
shadowdaoandClaude Opus 5.5 f970c4e895 feat(viewer): clickable file paths and file: hyperlinks in the terminal
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>
2026-09-22 21:13:27 -07:00
shadowdaoandClaude Opus 5.5 5b66e861df feat(viewer): CodeMirror editor, theme and target-line highlight
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>
2026-09-22 21:12:05 -07:00
shadowdaoandClaude Opus 5.5 bbcaee7797 feat(viewer): poll and save scripts run as the container user
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>
2026-09-22 21:11:19 -07:00
shadowdaoandClaude Opus 5.5 a80c146b8a feat(viewer): second Vite entry, viewer capability, CodeMirror deps
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>
2026-09-22 21:08:01 -07:00
shadowdaoandClaude Opus 5.5 247ec764ea feat(viewer): reload/conflict reducer and editability rules
- 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>
2026-09-22 21:07:28 -07:00
shadowdaoandClaude Opus 5.5 3177af202d feat(viewer): pure file-path matcher for terminal text
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>
2026-09-22 21:06:52 -07:00
shadowdaoandClaude Opus 5.5 554870a50f feat(viewer): candidate paths and container probe for the file viewer
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>
2026-09-22 21:06:30 -07:00
shadowdaoandClaude Opus 5.5 d32dc9b446 feat(viewer): window registry with cap, dedupe and sequential labels
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>
2026-09-22 21:05:33 -07:00
shadowdaoandClaude Opus 5.5 4ada54ffc4 feat(viewer): join wrapped xterm rows for link matching
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>
2026-09-22 21:05:06 -07:00
shadowdaoandClaude Opus 5.5 3c1d120305 feat(viewer): IPC types, wrappers and Rust module skeleton for the file viewer
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>
2026-09-22 20:56:33 -07:00
shadowdaoandClaude Fable 5.1 20befa1320 docs: terminal file viewer spec review and implementation plan
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>
2026-09-22 20:53:49 -07:00
shadowdaoandClaude Opus 5.5 508ff6d037 docs: design spec for per-window app-command lockdown via AppManifest
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>
2026-09-22 20:39:38 -07:00
3 changed files with 3 additions and 112 deletions
+3 -101
View File
@@ -5,11 +5,9 @@ name: Build App (Preview)
# sync. # sync.
# #
# This is also the **PR build check**: it compiles Linux, macOS and Windows, so # This is also the **PR build check**: it compiles Linux, macOS and Windows, so
# a push that breaks any of them fails here. Its `test` job runs vitest and # a push that breaks any of them fails here. build-app.yml used to do that job
# `cargo test` too, so a push that breaks either suite fails here as well. # in parallel and publish nothing, which meant six OS builds per push and one
# build-app.yml used to do the build-check job in parallel and publish nothing, # unreachable set of bundles; it is now releases-only.
# which meant six OS builds per push and one unreachable set of bundles; it is
# now releases-only.
# #
# The cost of the swap, stated plainly: one prerelease per PR commit that # The cost of the swap, stated plainly: one prerelease per PR commit that
# touches `app/**` — so the workflow prunes its own, keeping the newest # touches `app/**` — so the workflow prunes its own, keeping the newest
@@ -223,102 +221,6 @@ jobs:
echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_OUTPUT echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_OUTPUT
echo "Release ${TAG} is id ${RELEASE_ID}" echo "Release ${TAG} is id ${RELEASE_ID}"
# The test suites. Before this job CI ran neither: every check below lived on
# a developer's machine. The one that matters most is the app-command ACL
# census — `cargo test` is what re-checks the committed capability files and
# `gen/schemas/acl-manifests.json` against `generate_handler!`, and vitest's
# `capabilities.test.ts` is what keeps each window's code to the wrappers its
# capability grants. A command left ungranted builds fine and only fails at
# runtime ("not allowed by ACL"), so these tests are the merge-time guard.
#
# Independent of the release: no `needs`, so it runs alongside the three
# platform builds rather than in front of them, and a red test fails the PR
# check without holding up a preview someone may want to try anyway.
#
# Setup mirrors build-linux on purpose — the same Node, the same apt set
# (`cargo test` compiles the whole Tauri crate, so it needs WebKitGTK like
# a real build) and `npm ci` from the lockfile for the reasons given there.
test:
runs-on: ubuntu-latest
steps:
- name: Install Node.js 22
run: |
NEED_INSTALL=false
if command -v node >/dev/null 2>&1; then
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
OLD_NODE_DIR=$(dirname "$(which node)")
echo "Found Node.js $(node --version) at $(which node) (major: ${NODE_MAJOR})"
if [ "$NODE_MAJOR" -lt 22 ]; then
echo "Node.js ${NODE_MAJOR} is too old, removing before installing 22..."
sudo rm -f "${OLD_NODE_DIR}/node" "${OLD_NODE_DIR}/npm" "${OLD_NODE_DIR}/npx" "${OLD_NODE_DIR}/corepack"
hash -r
NEED_INSTALL=true
fi
else
echo "Node.js not found, installing 22..."
NEED_INSTALL=true
fi
if [ "$NEED_INSTALL" = true ]; then
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
hash -r
fi
node --version
npm --version
- name: Checkout
uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
libssl-dev \
libxdo-dev \
pkg-config \
build-essential \
curl
- name: Install Rust stable
run: |
if command -v rustup >/dev/null 2>&1; then
rustup update stable
rustup default stable
else
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
fi
export PATH="$HOME/.cargo/bin:$PATH"
rustc --version
cargo --version
- name: Install frontend dependencies
working-directory: ./app
run: npm ci
# `npm run build` is `tsc && vite build`: the type check, and the
# `dist/` that `tauri::generate_context!` needs to exist before the Rust
# crate — and so `cargo test` — will compile at all.
- name: Type-check and build the frontend
working-directory: ./app
run: npm run build
- name: Frontend tests (vitest)
working-directory: ./app
run: npx vitest run
# `--locked`: test against the committed Cargo.lock, never a re-resolved
# one, for the same reason the frontend uses `npm ci`.
- name: Backend tests (cargo test)
working-directory: ./app/src-tauri
run: |
export PATH="$HOME/.cargo/bin:$PATH"
cargo test --locked
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [compute-version, create-release] needs: [compute-version, create-release]
-7
View File
@@ -843,10 +843,3 @@ Frontend tests use Vitest with jsdom environment and React Testing Library. Setu
cd app cd app
npx vitest run src/path/to/test.test.ts npx vitest run src/path/to/test.test.ts
``` ```
CI runs both suites on every PR: the `test` job in `.gitea/workflows/build-app-preview.yml` does
`npm run build`, `npx vitest run` and `cargo test --locked`, in parallel with the platform builds.
It is the only place `cargo test` runs on merge, which matters most for the app-command ACL
census — an ungranted command compiles and only fails at runtime. `build-app.yml` (releases
from `main`) deliberately does not repeat it. The runner is root, so the few Rust tests that
exercise file permissions skip themselves there.
@@ -55,9 +55,6 @@ validation stays in Rust as today), changing any plugin grant, `removeUnusedComm
- CI (`.gitea/workflows/build-app-preview.yml`, `build-app.yml`) runs `npm run build` - CI (`.gitea/workflows/build-app-preview.yml`, `build-app.yml`) runs `npm run build`
(`tsc && vite build`) and `tauri build`. It runs **neither `cargo test` nor `vitest`**. Any (`tsc && vite build`) and `tauri build`. It runs **neither `cargo test` nor `vitest`**. Any
check that must hold on every merge therefore has to fail the *build*, not a test. check that must hold on every merge therefore has to fail the *build*, not a test.
*(2026-09-23: no longer true — `build-app-preview.yml` gained a `test` job that runs
`vitest` and `cargo test --locked` on every PR. The build-time check stays the backstop that
runs inside every `tauri build`, release builds included.)*
## 2. Mechanism (tauri-build 2.6.0 / tauri-utils 2.9.0 / tauri 2.11.0) ## 2. Mechanism (tauri-build 2.6.0 / tauri-utils 2.9.0 / tauri 2.11.0)
@@ -470,7 +467,6 @@ Manual (`npm run tauri dev`, then a release `tauri build` on Linux for the AppIm
developer's machine. Recommendation to the user, not part of this change: add developer's machine. Recommendation to the user, not part of this change: add
`cd app && npm run test` and `cd app/src-tauri && cargo test` steps to `cd app && npm run test` and `cd app/src-tauri && cargo test` steps to
`build-app-preview.yml` after the dependency install. `build-app-preview.yml` after the dependency install.
*(Done 2026-09-23: the `test` job in `build-app-preview.yml`.)*
2. **`Box::leak` in `build.rs`: acceptable** (controller ruling; either was allowed). Chosen 2. **`Box::leak` in `build.rs`: acceptable** (controller ruling; either was allowed). Chosen
over a generated include file because it is two lines, needs no `OUT_DIR` plumbing, and the over a generated include file because it is two lines, needs no `OUT_DIR` plumbing, and the
build script exits immediately afterwards. build script exits immediately afterwards.