feat(#29): per-workspace trust list with /trust slash command

Adds a persistent allowlist of external paths the agent may read/write
from outside the current workspace, scoped to the workspace it was
granted in. The list lives in ~/.deepseek/workspace-trust.json with
schema {"workspaces": {"<ws>": ["<trusted>", ...]}}; canonical paths on
both sides keep symlink-aliased macOS tempdirs sane.

Surface area:
 * crates/tui/src/workspace_trust.rs — new module: load_for / add /
   remove plus *_at variants for tests that need an explicit file path
   rather than HOME mutation.
 * tools/spec.rs — ToolContext gains trusted_external_paths and
   resolve_path consults it before returning PathEscape, both for the
   existing-path branch and the to-be-created (parent-canonical) branch.
 * core/engine.rs — build_tool_context loads the trust snapshot on every
   tool dispatch so /trust mutations apply on the next call.
 * commands/config.rs — /trust now takes subcommands (add, remove,
   list, on, off, status) instead of being a single all-or-nothing
   toggle. Tilde expansion handled in-line.
 * commands/mod.rs — registry entry updated with the new usage string
   and a dispatcher that forwards args.
 * tools/diagnostics.rs — adds trusted_external_paths to the JSON
   output so the agent and the user can see the list at a glance.

The interactive "Allow once / Always allow / Deny" prompt that the
issue describes is deferred — for v0.5.1 the workflow is "grant
ahead with /trust add". A future change will add a hook in
ToolContext::resolve_path that surfaces an ApprovalRequest when an
escape path is hit, so the slash-command remains the durable
mechanism while the prompt becomes the discovery one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-04-25 14:39:37 -05:00
parent 7a85f182e2
commit bcf6ba9a8e
7 changed files with 508 additions and 16 deletions
+133 -9
View File
@@ -1,5 +1,7 @@
//! Config commands: config, settings, mode switches, trust, logout
use std::path::{Path, PathBuf};
use super::CommandResult;
use crate::config::{COMMON_DEEPSEEK_MODELS, clear_api_key, normalize_model_name};
use crate::palette;
@@ -222,10 +224,117 @@ pub fn plan_mode(app: &mut App) -> CommandResult {
)
}
/// Enable trust mode (file access outside workspace)
pub fn trust(app: &mut App) -> CommandResult {
app.trust_mode = true;
CommandResult::message("Trust mode enabled - can access files outside workspace")
/// Manage workspace-level trust and the per-path allowlist.
///
/// Subcommands:
/// - `/trust` show current state and trusted external paths
/// - `/trust on` legacy: trust the entire workspace (turn off all path checks)
/// - `/trust off` disable workspace-level trust mode
/// - `/trust add <path>` add a directory to the allowlist (#29)
/// - `/trust remove <path>` (alias `rm`) remove a path from the allowlist
/// - `/trust list` list trusted external paths for this workspace
pub fn trust(app: &mut App, arg: Option<&str>) -> CommandResult {
let raw = arg.map(str::trim).unwrap_or("");
let mut parts = raw.splitn(2, char::is_whitespace);
let sub = parts.next().unwrap_or("").to_lowercase();
let rest = parts.next().map(str::trim).unwrap_or("");
let workspace = app.workspace.clone();
match sub.as_str() {
"" | "status" | "list" => trust_status(&workspace, app, sub == "list"),
"on" | "enable" | "yes" | "y" => {
app.trust_mode = true;
CommandResult::message(
"Workspace trust mode enabled — agent file tools can now read/write any path. \
Use `/trust off` to revert; prefer `/trust add <path>` for a narrower opt-in.",
)
}
"off" | "disable" | "no" | "n" => {
app.trust_mode = false;
CommandResult::message("Workspace trust mode disabled.")
}
"add" => trust_add(&workspace, rest),
"remove" | "rm" | "del" | "delete" => trust_remove(&workspace, rest),
other => CommandResult::error(format!(
"Unknown /trust action `{other}`. Use `/trust`, `/trust on|off`, `/trust add <path>`, or `/trust remove <path>`."
)),
}
}
fn trust_status(workspace: &Path, app: &App, force_paths: bool) -> CommandResult {
let trust = crate::workspace_trust::WorkspaceTrust::load_for(workspace);
let mut lines = Vec::new();
lines.push(format!(
"Workspace trust mode: {}",
if app.trust_mode {
"enabled"
} else {
"disabled"
}
));
if trust.paths().is_empty() {
if force_paths {
lines.push("No external paths trusted from this workspace.".to_string());
} else {
lines.push(
"No external paths trusted yet. Use `/trust add <path>` to allow a directory."
.to_string(),
);
}
} else {
lines.push(format!("Trusted external paths ({}):", trust.paths().len()));
for path in trust.paths() {
lines.push(format!("{}", path.display()));
}
}
CommandResult::message(lines.join("\n"))
}
fn trust_add(workspace: &Path, raw: &str) -> CommandResult {
if raw.is_empty() {
return CommandResult::error(
"Usage: /trust add <path>. Supply an absolute path or a path relative to the workspace.",
);
}
let path = PathBuf::from(expand_tilde(raw));
if !path.exists() {
return CommandResult::error(format!(
"Path not found: {} — supply an existing directory or file.",
path.display()
));
}
match crate::workspace_trust::add(workspace, &path) {
Ok(stored) => CommandResult::message(format!(
"Added to trust list for this workspace: {}",
stored.display()
)),
Err(err) => CommandResult::error(format!("Failed to update trust list: {err}")),
}
}
fn trust_remove(workspace: &Path, raw: &str) -> CommandResult {
if raw.is_empty() {
return CommandResult::error("Usage: /trust remove <path>");
}
let path = PathBuf::from(expand_tilde(raw));
match crate::workspace_trust::remove(workspace, &path) {
Ok(true) => CommandResult::message(format!("Removed from trust list: {}", path.display())),
Ok(false) => CommandResult::message(format!("Not in trust list: {}", path.display())),
Err(err) => CommandResult::error(format!("Failed to update trust list: {err}")),
}
}
fn expand_tilde(raw: &str) -> String {
if let Some(rest) = raw.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
{
return home.join(rest).to_string_lossy().into_owned();
} else if raw == "~"
&& let Some(home) = dirs::home_dir()
{
return home.to_string_lossy().into_owned();
}
raw.to_string()
}
/// Logout - clear API key and return to onboarding
@@ -509,16 +618,31 @@ mod tests {
}
#[test]
fn test_trust_enables_flag() {
fn test_trust_on_enables_flag() {
let mut app = create_test_app();
assert!(!app.trust_mode);
let result = trust(&mut app);
assert!(result.message.is_some());
let msg = result.message.unwrap();
assert!(msg.contains("Trust mode enabled"));
let result = trust(&mut app, Some("on"));
let msg = result.message.expect("message");
assert!(msg.contains("Workspace trust mode enabled"));
assert!(app.trust_mode);
}
#[test]
fn test_trust_status_default_lists_state() {
let mut app = create_test_app();
let result = trust(&mut app, None);
let msg = result.message.expect("status message");
assert!(msg.contains("Workspace trust mode"));
}
#[test]
fn test_trust_add_requires_path() {
let mut app = create_test_app();
let result = trust(&mut app, Some("add"));
let msg = result.message.expect("error message");
assert!(msg.starts_with("Error:"), "got {msg:?}");
}
#[test]
fn test_logout_clears_api_key_state() {
let _lock = lock_test_env();
+3 -3
View File
@@ -241,8 +241,8 @@ pub const COMMANDS: &[CommandInfo] = &[
CommandInfo {
name: "trust",
aliases: &[],
description: "Enable trust mode (access files outside workspace)",
usage: "/trust",
description: "Manage workspace trust and per-path allowlist (`/trust add <path>`, `/trust list`, `/trust on|off`)",
usage: "/trust [on|off|add <path>|remove <path>|list]",
},
CommandInfo {
name: "logout",
@@ -358,7 +358,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
"yolo" => config::yolo(app),
"agent" => config::agent_mode(app),
"plan" => config::plan_mode(app),
"trust" => config::trust(app),
"trust" => config::trust(app, arg),
"logout" => config::logout(app),
// Debug commands
+7 -1
View File
@@ -2020,6 +2020,11 @@ impl Engine {
}
fn build_tool_context(&self, mode: AppMode, auto_approve: bool) -> ToolContext {
// Load the per-workspace trusted-paths list (#29) on every tool-context
// build. Cheap (a small JSON file) and always reflects the latest
// `/trust add` / `/trust remove` mutations without an explicit cache
// refresh hook.
let trusted = crate::workspace_trust::WorkspaceTrust::load_for(&self.session.workspace);
let ctx = ToolContext::with_auto_approve(
self.session.workspace.clone(),
self.session.trust_mode,
@@ -2029,7 +2034,8 @@ impl Engine {
)
.with_state_namespace(self.session.id.clone())
.with_features(self.config.features.clone())
.with_shell_manager(self.shell_manager.clone());
.with_shell_manager(self.shell_manager.clone())
.with_trusted_external_paths(trusted.paths().to_vec());
if mode == AppMode::Yolo {
ctx.with_elevated_sandbox_policy(crate::sandbox::SandboxPolicy::WorkspaceWrite {
+1
View File
@@ -51,6 +51,7 @@ mod tui;
mod ui;
mod utils;
mod working_set;
mod workspace_trust;
use crate::config::{Config, DEFAULT_TEXT_MODEL, MAX_SUBAGENTS};
use crate::eval::{EvalHarness, EvalHarnessConfig, ScenarioStepKind};
+11
View File
@@ -30,6 +30,11 @@ struct DiagnosticsOutput {
sandbox_type: Option<String>,
rustc_version: Option<String>,
cargo_version: Option<String>,
/// User-trusted external paths the agent may access from this workspace
/// (`/trust add <path>` from the slash command, persisted in
/// `~/.deepseek/workspace-trust.json`). See issue #29.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
trusted_external_paths: Vec<String>,
}
#[derive(Debug, Clone, Default)]
@@ -81,6 +86,11 @@ impl ToolSpec for DiagnosticsTool {
let sandbox_type = crate::sandbox::get_platform_sandbox().map(|s| s.to_string());
let sandbox_available = sandbox_type.is_some();
let trusted_external_paths = context
.trusted_external_paths
.iter()
.map(|p| p.display().to_string())
.collect();
let diagnostics = DiagnosticsOutput {
workspace_root,
current_dir,
@@ -92,6 +102,7 @@ impl ToolSpec for DiagnosticsTool {
sandbox_type,
rustc_version: probe_version("rustc", &["--version"], &context.workspace),
cargo_version: probe_version("cargo", &["--version"], &context.workspace),
trusted_external_paths,
};
ToolResult::json(&diagnostics).map_err(|e| ToolError::execution_failed(e.to_string()))
+69 -3
View File
@@ -198,6 +198,11 @@ pub struct ToolContext {
pub features: Features,
/// Namespace for tool state that should be scoped to the current session/thread.
pub state_namespace: String,
/// User-trusted external paths the agent may read/write even when they
/// fall outside `workspace`. Loaded from `~/.deepseek/workspace-trust.json`
/// and refreshed when the user runs `/trust add <path>`. Distinct from
/// `trust_mode`, which is the all-or-nothing legacy switch (#29).
pub trusted_external_paths: Vec<PathBuf>,
}
impl ToolContext {
@@ -219,6 +224,7 @@ impl ToolContext {
auto_approve: false,
features: Features::with_defaults(),
state_namespace: "workspace".to_string(),
trusted_external_paths: Vec::new(),
}
}
@@ -243,6 +249,7 @@ impl ToolContext {
auto_approve: false,
features: Features::with_defaults(),
state_namespace: "workspace".to_string(),
trusted_external_paths: Vec::new(),
}
}
@@ -267,9 +274,19 @@ impl ToolContext {
auto_approve,
features: Features::with_defaults(),
state_namespace: "workspace".to_string(),
trusted_external_paths: Vec::new(),
}
}
/// Set the user's trusted external paths (loaded from
/// `~/.deepseek/workspace-trust.json`). See [`Self::resolve_path`] for
/// how the list is consulted.
#[must_use]
pub fn with_trusted_external_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.trusted_external_paths = paths;
self
}
/// Resolve a path relative to workspace, validating it doesn't escape.
///
/// This handles both existing files (using canonicalize) and non-existent files
@@ -317,7 +334,10 @@ impl ToolContext {
// hasn't been canonicalized yet
let workspace_plain = normalize_path(&self.workspace);
let candidate_normalized = normalize_path(&candidate);
if !candidate_normalized.starts_with(&workspace_plain) {
if !candidate_normalized.starts_with(&workspace_plain)
&& !self.is_trusted_external_path(&candidate_canonical)
&& !self.is_trusted_external_path(&candidate_normalized)
{
return Err(ToolError::PathEscape {
path: candidate_canonical,
});
@@ -334,7 +354,9 @@ impl ToolContext {
))
})?;
if !canonical.starts_with(&workspace_canonical) {
if !canonical.starts_with(&workspace_canonical)
&& !self.is_trusted_external_path(&canonical)
{
return Err(ToolError::PathEscape { path: canonical });
}
@@ -376,9 +398,12 @@ impl ToolContext {
}
let canonical = normalize_path(&canonical);
// Validate it's under workspace
// Validate it's under workspace, OR is under a user-trusted external
// path (`/trust add <path>` from the slash command, persisted in
// `~/.deepseek/workspace-trust.json`).
if !canonical.starts_with(&workspace_canonical)
&& !canonical.starts_with(&workspace_normalized)
&& !self.is_trusted_external_path(&canonical)
{
return Err(ToolError::PathEscape { path: canonical });
}
@@ -386,6 +411,14 @@ impl ToolContext {
Ok(canonical)
}
/// Whether `path` is under any of the user-trusted external roots. The
/// caller should pass an already-canonicalized (or normalized) path.
fn is_trusted_external_path(&self, path: &Path) -> bool {
self.trusted_external_paths
.iter()
.any(|trusted| path.starts_with(trusted))
}
/// Set the trust mode.
#[allow(dead_code)]
pub fn with_trust_mode(mut self, trust: bool) -> Self {
@@ -650,6 +683,39 @@ mod tests {
assert!(result.is_ok());
}
/// Issue #29: paths under a user-trusted external directory resolve
/// successfully even though they fall outside the workspace, while
/// untrusted external paths still error with `PathEscape`.
#[test]
fn test_tool_context_trusted_external_path_allows_escape() {
let workspace = tempdir().expect("workspace tempdir");
let trusted_root = tempdir().expect("trusted tempdir");
let trusted_file = trusted_root.path().join("notes.md");
std::fs::write(&trusted_file, "shared notes").unwrap();
let ctx =
ToolContext::new(workspace.path().to_path_buf()).with_trusted_external_paths(vec![
trusted_root
.path()
.canonicalize()
.unwrap_or_else(|_| trusted_root.path().to_path_buf()),
]);
let resolved = ctx
.resolve_path(trusted_file.to_str().unwrap())
.expect("trusted path should resolve");
assert!(resolved.ends_with("notes.md"));
// Path outside workspace AND outside the trust list should still fail.
let other = tempdir().expect("untrusted tempdir");
let other_file = other.path().join("secret.md");
std::fs::write(&other_file, "x").unwrap();
let err = ctx
.resolve_path(other_file.to_str().unwrap())
.expect_err("untrusted path must error");
assert!(matches!(err, ToolError::PathEscape { .. }));
}
#[test]
fn test_required_str() {
let input = json!({"name": "test", "count": 42});
+284
View File
@@ -0,0 +1,284 @@
//! Per-workspace trust list of external paths the agent may read/write
//! without triggering a `PathEscape` error (#29).
//!
//! Storage: `~/.deepseek/workspace-trust.json`. The file is a JSON object
//! mapping each workspace's canonical path to a sorted list of canonical
//! paths the user has explicitly trusted from that workspace. Trust granted
//! in workspace A does not apply when running from workspace B.
//!
//! Threat model: this is a deliberate user opt-in to a path the workspace
//! sandbox would otherwise refuse. The only access the trust list grants is
//! through DeepSeek-TUI's own file tools (`read_file`, `write_file`, etc.) —
//! it does not loosen the OS sandbox profile (Seatbelt/Landlock) used for
//! shell commands. Sandbox-profile expansion is tracked separately so a
//! shell tool can opt into the same paths in a future release.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
const TRUST_FILE_NAME: &str = "workspace-trust.json";
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct TrustFile {
/// Map workspace canonical path → sorted unique trusted paths.
#[serde(default)]
workspaces: BTreeMap<String, Vec<String>>,
}
/// In-memory trust list for a single workspace, snapshotted at load time.
/// Tools consult this snapshot to decide whether an out-of-workspace path
/// is permitted; the engine refreshes it after `/trust` mutations.
#[derive(Debug, Default, Clone)]
pub struct WorkspaceTrust {
paths: Vec<PathBuf>,
}
impl WorkspaceTrust {
#[must_use]
#[allow(dead_code)]
pub fn empty() -> Self {
Self { paths: Vec::new() }
}
/// Load the trusted-paths snapshot for `workspace` from disk. Missing or
/// malformed files yield an empty list rather than an error so a corrupt
/// trust file never wedges the TUI; the next mutation rewrites it.
#[must_use]
pub fn load_for(workspace: &Path) -> Self {
match trust_file_path() {
Some(path) => Self::load_from_file(workspace, &path),
None => Self::empty(),
}
}
fn load_from_file(workspace: &Path, file_path: &Path) -> Self {
let key = workspace_key(workspace);
let file = read_trust_file_at(file_path).unwrap_or_default();
let paths = file
.workspaces
.get(&key)
.cloned()
.unwrap_or_default()
.into_iter()
.map(PathBuf::from)
.collect();
Self { paths }
}
/// Return the trusted paths in canonical form.
#[must_use]
pub fn paths(&self) -> &[PathBuf] {
&self.paths
}
/// Whether the candidate is trusted: the candidate (after canonical
/// normalization) starts with one of the trusted prefixes. Directory
/// trust grants access to anything under the directory.
#[must_use]
#[allow(dead_code)]
pub fn permits(&self, candidate: &Path) -> bool {
let canonical = candidate
.canonicalize()
.unwrap_or_else(|_| candidate.to_path_buf());
self.paths
.iter()
.any(|trusted| canonical.starts_with(trusted))
}
}
/// Add `path` to `workspace`'s trust list and persist. Returns the canonical
/// trusted path that was actually stored, so callers can echo it back to the
/// user.
pub fn add(workspace: &Path, path: &Path) -> Result<PathBuf> {
let trust_path = trust_file_path()
.context("home directory not available; cannot persist workspace trust list")?;
add_at(workspace, path, &trust_path)
}
fn add_at(workspace: &Path, path: &Path, trust_path: &Path) -> Result<PathBuf> {
let canonical = canonicalize_or_keep(path);
let key = workspace_key(workspace);
let mut file = read_trust_file_at(trust_path).unwrap_or_default();
let entry = file.workspaces.entry(key).or_default();
let stored = canonical.to_string_lossy().to_string();
if !entry.iter().any(|p| p == &stored) {
entry.push(stored.clone());
entry.sort();
entry.dedup();
}
write_trust_file_at(&file, trust_path)?;
Ok(canonical)
}
/// Remove `path` from `workspace`'s trust list. Returns true when an entry
/// was actually removed.
pub fn remove(workspace: &Path, path: &Path) -> Result<bool> {
let Some(trust_path) = trust_file_path() else {
return Ok(false);
};
remove_at(workspace, path, &trust_path)
}
fn remove_at(workspace: &Path, path: &Path, trust_path: &Path) -> Result<bool> {
let canonical = canonicalize_or_keep(path);
let key = workspace_key(workspace);
let mut file = read_trust_file_at(trust_path).unwrap_or_default();
let stored = canonical.to_string_lossy().to_string();
let removed = match file.workspaces.get_mut(&key) {
Some(entry) => {
let len_before = entry.len();
entry.retain(|p| p != &stored);
let changed = entry.len() != len_before;
if entry.is_empty() {
file.workspaces.remove(&key);
}
changed
}
None => false,
};
if removed {
write_trust_file_at(&file, trust_path)?;
}
Ok(removed)
}
fn workspace_key(workspace: &Path) -> String {
canonicalize_or_keep(workspace)
.to_string_lossy()
.into_owned()
}
fn canonicalize_or_keep(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn trust_file_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".deepseek").join(TRUST_FILE_NAME))
}
fn read_trust_file_at(path: &Path) -> Result<TrustFile> {
if !path.exists() {
return Ok(TrustFile::default());
}
let raw = std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
serde_json::from_str(&raw).with_context(|| format!("parse {}", path.display()))
}
fn write_trust_file_at(file: &TrustFile, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create dir {}", parent.display()))?;
}
let json = serde_json::to_string_pretty(file).context("serialize trust file")?;
std::fs::write(path, json).with_context(|| format!("write {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
/// Set up an isolated fake `~/.deepseek/workspace-trust.json` location.
/// Returns the tmpdir (kept alive for the test) plus the explicit trust
/// file path passed to the `*_at` helpers — avoids touching `$HOME` so
/// tests run safely in parallel.
fn isolated_trust_path() -> (TempDir, PathBuf) {
let tmp = TempDir::new().expect("tempdir");
let trust_path = tmp.path().join(".deepseek").join("workspace-trust.json");
(tmp, trust_path)
}
#[test]
fn empty_trust_for_unknown_workspace() {
let (tmp, trust_path) = isolated_trust_path();
let workspace = tmp.path().join("ws");
std::fs::create_dir_all(&workspace).unwrap();
let trust = WorkspaceTrust::load_from_file(&workspace, &trust_path);
assert!(trust.paths().is_empty());
assert!(!trust.permits(Path::new("/anywhere")));
}
#[test]
fn add_persists_and_load_returns_path() {
let (tmp, trust_path) = isolated_trust_path();
let workspace = tmp.path().join("ws");
let other = tmp.path().join("data/notes");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(&other).unwrap();
let stored = add_at(&workspace, &other, &trust_path).expect("add");
// On macOS, /var/folders is a symlink to /private/var/folders so the
// canonical form may live under that prefix. Compare using
// canonicalize on both ends.
let canonical_other = other.canonicalize().unwrap_or(other.clone());
assert_eq!(stored, canonical_other);
let trust = WorkspaceTrust::load_from_file(&workspace, &trust_path);
assert_eq!(trust.paths().len(), 1);
// Create the file so canonicalize resolves through any symlinks; the
// stored trust path uses the canonical form.
let inner = other.join("file.md");
std::fs::write(&inner, "x").unwrap();
assert!(trust.permits(&inner));
assert!(!trust.permits(Path::new("/etc/passwd")));
}
#[test]
fn add_is_idempotent() {
let (tmp, trust_path) = isolated_trust_path();
let workspace = tmp.path().join("ws");
let other = tmp.path().join("data/notes");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(&other).unwrap();
let _ = add_at(&workspace, &other, &trust_path).unwrap();
let _ = add_at(&workspace, &other, &trust_path).unwrap();
let trust = WorkspaceTrust::load_from_file(&workspace, &trust_path);
assert_eq!(trust.paths().len(), 1);
}
#[test]
fn trust_is_workspace_scoped() {
let (tmp, trust_path) = isolated_trust_path();
let ws_a = tmp.path().join("ws-a");
let ws_b = tmp.path().join("ws-b");
let other = tmp.path().join("data/notes");
std::fs::create_dir_all(&ws_a).unwrap();
std::fs::create_dir_all(&ws_b).unwrap();
std::fs::create_dir_all(&other).unwrap();
add_at(&ws_a, &other, &trust_path).unwrap();
assert_eq!(
WorkspaceTrust::load_from_file(&ws_a, &trust_path)
.paths()
.len(),
1
);
assert_eq!(
WorkspaceTrust::load_from_file(&ws_b, &trust_path)
.paths()
.len(),
0
);
}
#[test]
fn remove_deletes_path() {
let (tmp, trust_path) = isolated_trust_path();
let workspace = tmp.path().join("ws");
let other = tmp.path().join("data/notes");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(&other).unwrap();
add_at(&workspace, &other, &trust_path).unwrap();
let removed = remove_at(&workspace, &other, &trust_path).unwrap();
assert!(removed);
let trust = WorkspaceTrust::load_from_file(&workspace, &trust_path);
assert!(trust.paths().is_empty());
}
}