Extract dotfile lists to shared constants and add shell history files

Richard Feldman created

Move the hardcoded dotfile lists from sandbox_macos.rs and sandbox_linux.rs
into shared constants (READ_ONLY_DOTFILES, READ_WRITE_DOTFILES) on
SandboxConfig so both platform implementations use the same canonical list
in the same order.

Add .bash_history and .zsh_history as READ_WRITE_DOTFILES so shells can
persist command history without silent failures or error messages.

Change summary

crates/sandbox/src/sandbox.rs       | 24 ++++++++++++++++++++++++
crates/sandbox/src/sandbox_linux.rs | 22 ++++++++--------------
crates/sandbox/src/sandbox_macos.rs | 28 +++++++++++++---------------
3 files changed, 45 insertions(+), 29 deletions(-)

Detailed changes

crates/sandbox/src/sandbox.rs 🔗

@@ -170,6 +170,30 @@ impl ResolvedSystemPaths {
 }
 
 impl SandboxConfig {
+    /// Shell configuration dotfiles that need read-only access.
+    /// Both macOS and Linux sandbox implementations use this list.
+    pub const READ_ONLY_DOTFILES: &[&str] = &[
+        ".bashrc",
+        ".bash_login",
+        ".bash_profile",
+        ".gitconfig",
+        ".inputrc",
+        ".profile",
+        ".terminfo",
+        ".zlogin",
+        ".zlogout",
+        ".zprofile",
+        ".zshenv",
+        ".zshrc",
+    ];
+
+    /// Shell history dotfiles that need read-write access so shells can
+    /// persist command history without silent failures.
+    pub const READ_WRITE_DOTFILES: &[&str] = &[
+        ".bash_history",
+        ".zsh_history",
+    ];
+
     /// Default environment variables to pass through to sandboxed terminals.
     pub fn default_allowed_env_vars() -> Vec<String> {
         vec![

crates/sandbox/src/sandbox_linux.rs 🔗

@@ -104,26 +104,20 @@ pub fn apply_sandbox(config: &SandboxConfig) -> Result<()> {
 
     if let Ok(home) = std::env::var("HOME") {
         let home = Path::new(&home);
-        for dotfile in &[
-            ".bashrc",
-            ".bash_profile",
-            ".bash_login",
-            ".profile",
-            ".zshrc",
-            ".zshenv",
-            ".zprofile",
-            ".zlogin",
-            ".zlogout",
-            ".inputrc",
-            ".terminfo",
-            ".gitconfig",
-        ] {
+        for dotfile in SandboxConfig::READ_ONLY_DOTFILES {
             let path = home.join(dotfile);
             if path.exists() {
                 ruleset = add_path_rule(ruleset, &path, fs_read())
                     .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
             }
         }
+        for dotfile in SandboxConfig::READ_WRITE_DOTFILES {
+            let path = home.join(dotfile);
+            if path.exists() {
+                ruleset = add_path_rule(ruleset, &path, fs_all())
+                    .map_err(|e| Error::other(format!("landlock dotfile rule: {e}")))?;
+            }
+        }
         let config_dir = home.join(".config");
         if config_dir.exists() {
             ruleset = add_path_rule(ruleset, &config_dir, fs_read())

crates/sandbox/src/sandbox_macos.rs 🔗

@@ -420,23 +420,10 @@ pub(crate) fn generate_sbpl_profile(
         write_subpath_rule(&mut p, path, "file-read* file-write*");
     }
 
-    // User shell config files: read-only access to $HOME dotfiles
+    // User shell config files
     if let Ok(home) = std::env::var("HOME") {
         let home = Path::new(&home);
-        for dotfile in &[
-            ".zshrc",
-            ".zshenv",
-            ".zprofile",
-            ".zlogin",
-            ".zlogout",
-            ".bashrc",
-            ".bash_profile",
-            ".bash_login",
-            ".profile",
-            ".inputrc",
-            ".terminfo",
-            ".gitconfig",
-        ] {
+        for dotfile in SandboxConfig::READ_ONLY_DOTFILES {
             let path = home.join(dotfile);
             if path.exists() {
                 write!(
@@ -447,6 +434,17 @@ pub(crate) fn generate_sbpl_profile(
                 .unwrap();
             }
         }
+        for dotfile in SandboxConfig::READ_WRITE_DOTFILES {
+            let path = home.join(dotfile);
+            if path.exists() {
+                write!(
+                    p,
+                    "(allow file-read* file-write* (literal \"{}\"))\n",
+                    sbpl_escape(&path)
+                )
+                .unwrap();
+            }
+        }
         // XDG config directory
         let config_dir = home.join(".config");
         if config_dir.exists() {