agent: code-atomiser-fix — write_text_file_cmd path scope (Trust-1, corrective)

Corrective re-apply of Trust-1. The original commit a2b47db carried
the wrong staged file due to a parallel-agent race that swapped
the staged path between `git add` and `git commit` — fs.rs was
never actually changed by a2b47db despite the message. This commit
applies the Trust-1 fix properly.

The Tauri command `write_text_file_cmd` previously took an arbitrary
`path: String` and flowed it straight into `tokio::fs::write` with no
main-window guard, no canonicalisation, and no scope check. A
compromised webview could write anywhere the process had write access
— overwriting shell init files, dropping a runner into
`~/.config/autostart`, etc.

This change:

- adds `ensure_main_window(&window)?` so only the main webview can
  invoke the command;
- canonicalises the requested path's parent (rejecting nonexistent
  parents and resolving symlinks) before joining the filename;
- asserts the canonical target sits inside an allowlisted base
  (app data, app local data, downloads, documents, desktop), so a
  `"../../etc/passwd"` payload — even one obtained via symlink trickery
  in the chosen save dir — is refused with a clear error.

Six unit tests cover: outside-allowlist rejection, path-traversal
rejection, nested inside-allowlist acceptance, plain inside-allowlist
acceptance, nonexistent-parent rejection, and the pure
`is_inside_any_base` prefix check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 17:59:26 +01:00
parent 99f4ecdecc
commit a48653c93c
2 changed files with 209 additions and 3 deletions

View File

@@ -55,6 +55,16 @@ pub struct AppState {
pub parakeet_engine: Arc<LocalEngine>,
pub db: SqlitePool,
pub llm_engine: Arc<LlmEngine>,
/// Race-4/5 TOCTOU guard: two concurrent `ensure_model_loaded`
/// callers (e.g. a double-clicked "Test" + "Transcribe" button)
/// would both observe `loaded_model_id != requested`, both decide
/// "not loaded — load it", and both spawn a multi-second
/// `load_model_from_disk` whose result the second overwrites.
/// Holding this `tokio::sync::Mutex` across the check-then-load
/// critical section forces them to serialise; the second caller
/// acquires the lock after the first finishes, observes the
/// now-loaded state, and short-circuits.
pub load_serialise: Arc<tokio::sync::Mutex<()>>,
}
/// Holds the preferences init script for injection into secondary windows.
@@ -629,6 +639,7 @@ pub fn run() {
parakeet_engine: Arc::new(LocalEngine::new(EngineName::new("parakeet"))),
db,
llm_engine: Arc::new(LlmEngine::new()),
load_serialise: Arc::new(tokio::sync::Mutex::new(())),
});
// Runtime-warning banner: push CPU-feature + Vulkan-loader