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
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>
This commit is contained in:
@@ -291,19 +291,44 @@ a command that genuinely needs both is a design change worth a visible edit to
|
||||
that is wrong for the tree as built, because the viewer does not call `invoke` itself — it
|
||||
imports wrappers from the shared `lib/tauri-commands.ts` (`viewerGetState`, `viewerChooseFile`
|
||||
in `ViewerApp.tsx`; `viewerReadFile`, `viewerPollFile`, `viewerWriteFile` in `EditorPane.tsx`).
|
||||
So the test follows imports instead:
|
||||
So the test follows imports instead, and does so by parsing every file with the TypeScript
|
||||
compiler (`ts.createSourceFile`), not a regex scan, so comments, strings, template substitutions
|
||||
and regex literals are the parser's problem rather than ours:
|
||||
|
||||
- **Only `lib/tauri-commands.ts` may import `@tauri-apps/api/core`** (true today for every
|
||||
non-test file). That is what makes the rest of the test complete: no other file can invoke.
|
||||
- Build the wrapper map from `tauri-commands.ts`: each `export const NAME = …` chunk contains
|
||||
exactly one `invoke<…>("<literal>"` (115 wrappers, 115 commands today). A chunk with zero or
|
||||
more than one literal, or a non-literal first argument, fails the test.
|
||||
- The **viewer closure** is the transitive set of relative imports from `src/viewer/main.tsx`
|
||||
(`.ts`/`.tsx`, tests excluded, `.css` ignored). Today it reaches `lib/tauri-commands`,
|
||||
`lib/types`, `components/ui/{Button,StatusIndicator,unavailable}` and
|
||||
- **Only `lib/tauri-commands.ts` may import `@tauri-apps/api/core`**, checked against *every*
|
||||
code file under `src/` — tests included, not just the files the two closures below cover,
|
||||
because the viewer's closure can reach anything a relative import can. A specifier this scan
|
||||
cannot read as a literal (a computed `import()`/`require()` argument) is treated as an offender
|
||||
too, so an un-auditable dynamic import fails the same way a literal one to
|
||||
`@tauri-apps/api/core` would. That is what makes the rest of the test complete: no other file
|
||||
can invoke.
|
||||
- The wrapper map is read from `tauri-commands.ts`'s AST: each exported `const NAME = …` must
|
||||
call `invoke` exactly once, inside its own function body (not at module load, not inside an
|
||||
IIFE, not as a default argument), with a string-literal command name matching Rust's
|
||||
`[a-z0-9_]+` (115 wrappers, 115 commands today). Zero or more than one call, a non-literal
|
||||
argument, `invoke` referenced other than as a direct call, or any dynamic `import()`/`require()`
|
||||
in the file at all, fails the test.
|
||||
- Every other module edge is resolved the way Vite 6 actually resolves it, in Vite's own order:
|
||||
the exact relative path if it names a file; else, for a `.js`/`.mjs`/`.cjs`/`.jsx` path, its
|
||||
TypeScript twin (`.js`→`.ts`, then `.tsx`); else `path + ext` over `resolve.extensions`
|
||||
(`.mjs .js .mts .ts .jsx .tsx .json`) in that order; else, for a directory, `index + ext` in the
|
||||
same order. A path alias, a query/fragment suffix (`?worker`, `?raw`), a directory import that
|
||||
would switch Vite to package-entry resolution, or a relative import that resolves outside
|
||||
`src/`, all fail the test rather than being silently skipped or misresolved — resolution is
|
||||
fail-closed, not best-effort.
|
||||
- A wrapper counts as used when a file names it directly (a named import, a named re-export by
|
||||
name) or reaches it through a namespace import (`import * as X from ".../tauri-commands"`)
|
||||
**used only as member access**, `X.name` (including `typeof X.name` in a type position). Any
|
||||
other use of that namespace binding — passed to a function, spread, indexed, aliased again —
|
||||
fails closed rather than being read as "no wrapper used"; `export *`/`export * as` of the
|
||||
wrappers file is refused outright, since it cannot be resolved to specific names.
|
||||
- The **viewer closure** is the transitive set of module targets reachable from
|
||||
`src/viewer/main.tsx` under that same fail-closed resolution (static imports, `export … from`,
|
||||
literal `import()`; tests excluded, non-code assets ignored). Today it reaches
|
||||
`lib/tauri-commands`, `lib/types`, `components/ui/{Button,StatusIndicator,unavailable}` and
|
||||
`components/projects/home/filePreview`, none of which import a wrapper except the two viewer
|
||||
files. `V` = commands of the wrappers those files import; `M` = commands of the wrappers
|
||||
imported by every other non-test file under `src/`.
|
||||
files. `V` = commands of the wrappers those files reach; `M` = commands of the wrappers reached
|
||||
by every other non-test file under `src/`.
|
||||
- Assert `M ∩ V = ∅`; `allow-<slug>(V)` **equals** the bare set of `file-viewer.json` (no
|
||||
over-grant to the untrusted window); `allow-<slug>(M)` is a **subset** of the bare set of
|
||||
`default.json`; and every wrapper's command is in exactly one of the two files (a cheap mirror
|
||||
@@ -389,7 +414,7 @@ code that runs in each window only reaches wrappers that window is granted.
|
||||
| A registered command is left out of `default.json` | Runtime: that feature's invoke rejected with `not allowed. Permissions associated with this command: allow-…` (release: `Command … not allowed by ACL`) | `build.rs` §3.2 fails the build before any binary exists; the vitest §3.3 fails locally |
|
||||
| Typo in an `allow-*` string | Build | tauri's `validate_capabilities` and the §3.2 check, both at build time |
|
||||
| Parser regression yields an empty command list | Would silently restore the unguarded state | §3.2 step 2 panics on empty; §3.4 asserts `acl-manifests.json` has the full set |
|
||||
| Stale `permissions/autogenerated/<cmd>.toml` after a command is removed | A capability could still reference a dead permission and validate | §3.2 step 3 deletes the directory every build; the directory is gitignored |
|
||||
| Stale `permissions/autogenerated/<cmd>.toml` after a command is removed | A capability could still reference a dead permission and validate | §3.2 step 3 selectively prunes only the files whose `<name>` is no longer a registered command, every build (never `remove_dir_all` — Decision 9); live files are left alone so the build settles; the directory is gitignored |
|
||||
| A `deny-*` added "for the viewer only" | Denies the command for main too (global) | §3.2 rejects any bare `deny-*` |
|
||||
| `file-viewer.json` grants a main-only command | The viewer window gains reach | §3.2 `expected_windows` assertion and §3.3 equality check |
|
||||
| Main-window code imports a `viewer*` wrapper (or the viewer imports a main one) | Runtime denial in that window | §3.3 `M ∩ V = ∅` and the per-side set checks |
|
||||
|
||||
@@ -56,7 +56,9 @@ AppManifest spec, `2026-09-22-app-manifest-lockdown-design.md`).
|
||||
`close()`. `lib.rs`'s `on_window_event` returns early for any label but `main`.
|
||||
- `build.rs` is a bare `tauri_build::build()`: **every app command is callable from every local
|
||||
window**. `capabilities/default.json` gates plugin commands only and lists `windows: ["main"]`.
|
||||
App commands need no capability entry (CLAUDE.md, "Key Conventions").
|
||||
App commands need no capability entry (CLAUDE.md, "Key Conventions"). *(Historical snapshot at
|
||||
`3537b23`. Closed 2026-09-22 by the AppManifest lockdown
|
||||
(`2026-09-22-app-manifest-lockdown-design.md`) — see §6 below.)*
|
||||
- The frontend does not know a terminal's cwd. Terminal execs start in `/workspace`; each project
|
||||
path is bind-mounted at `/workspace/<mount_name>` (`ProjectPath { host_path, mount_name }`,
|
||||
rows with an empty `mount_name` are skipped at container creation). `/workspace` itself is not a
|
||||
|
||||
Reference in New Issue
Block a user