Fold builtin mounts into scoped config
Remove the separate builtin_mounts field from Ramekin. Builtin mounts
(workspace, pi data, agent dir, sessions) are now a Builtin scope in
the layered config system — the highest precedence layer, so they
can never be overridden by user or project configs.

Config::load() takes the builtin mounts as a parameter and appends
them as the final layer. The config command shows a single unified
'Volume mounts' list with each entry tagged by its scope.
change wtxyqtxyqyvyxzvvmzpmssltqynqusko
commit 4e334f8cc48fdab9e2a9eafd0c5eeec67de4f3cd
author Alpha Chen <alpha@kejadlen.dev>
date
parent pymyluzy
diff --git a/src/config.rs b/src/config.rs
index 738f7ce..6b94537 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -29,6 +29,8 @@ pub enum Scope {
     User,
     /// Project-level `<workspace>/.ramekin/config.kdl`.
     Project,
+    /// Internal mounts managed by ramekin (highest precedence).
+    Builtin,
 }
 
 impl fmt::Display for Scope {
@@ -37,6 +39,7 @@ impl fmt::Display for Scope {
             Self::Default => write!(f, "default"),
             Self::User => write!(f, "user"),
             Self::Project => write!(f, "project"),
+            Self::Builtin => write!(f, "builtin"),
         }
     }
 }
@@ -103,9 +106,10 @@ impl Config {
     /// 1. Default (hardcoded)
     /// 2. User (`~/.config/ramekin/config.kdl`) — only if the file exists
     /// 3. Project (`<workspace>/.ramekin/config.kdl`) — only if the file exists
+    /// 4. Builtin (internal mounts passed by the caller)
     ///
     /// Returns an error if a config file exists but can't be parsed.
-    pub fn load(workspace: &Path) -> Result<ScopedConfig> {
+    pub fn load(workspace: &Path, builtin_mounts: Vec<ResolvedMount>) -> Result<ScopedConfig> {
         let mut layers = Vec::new();
 
         // Default layer (always present)
@@ -145,6 +149,13 @@ impl Config {
             });
         }
 
+        // Builtin layer (always present, highest precedence)
+        layers.push(ConfigLayer {
+            scope: Scope::Builtin,
+            path: None,
+            mounts: builtin_mounts,
+        });
+
         Ok(ScopedConfig { layers })
     }
 
@@ -422,6 +433,7 @@ mod tests {
         assert_eq!(Scope::Default.to_string(), "default");
         assert_eq!(Scope::User.to_string(), "user");
         assert_eq!(Scope::Project.to_string(), "project");
+        assert_eq!(Scope::Builtin.to_string(), "builtin");
     }
 
     #[test]
@@ -560,7 +572,7 @@ mod tests {
     #[test]
     fn load_with_no_config_files() {
         // Use a workspace with no .ramekin/config.kdl
-        let config = Config::load(Path::new("/tmp")).unwrap();
+        let config = Config::load(Path::new("/tmp"), vec![]).unwrap();
         // Should have at least the default layer
         assert!(!config.layers.is_empty());
         assert_eq!(config.layers[0].scope, Scope::Default);
@@ -579,16 +591,21 @@ mod tests {
         )
         .unwrap();
 
-        let config = Config::load(&dir).unwrap();
+        let config = Config::load(&dir, vec![]).unwrap();
 
         // Clean up
         let _ = fs_err::remove_dir_all(&dir);
 
-        // Should have default + project layers
-        assert!(config.layers.len() >= 2);
-        let project_layer = config.layers.last().unwrap();
-        assert_eq!(project_layer.scope, Scope::Project);
+        // Should have default + project + builtin layers
+        assert!(config.layers.len() >= 3);
+        let project_layer = config
+            .layers
+            .iter()
+            .find(|l| l.scope == Scope::Project)
+            .expect("project layer missing");
         assert_eq!(project_layer.mounts.len(), 2);
         assert_eq!(project_layer.mounts[0].target, "/container/tmp");
+        // Builtin is always last (highest precedence)
+        assert_eq!(config.layers.last().unwrap().scope, Scope::Builtin);
     }
 }
diff --git a/src/main.rs b/src/main.rs
index 7888726..5d400e4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -67,7 +67,6 @@ struct Ramekin {
     repo_sessions_dir: PathBuf,
     cache_dir: PathBuf,
     custom_dockerfile: Option<PathBuf>,
-    builtin_mounts: Vec<config::ResolvedMount>,
     config: config::ScopedConfig,
 }
 
@@ -123,7 +122,7 @@ impl Ramekin {
             .is_file()
             .then_some(custom_dockerfile_path);
 
-        // Builtin mounts (always present)
+        // Builtin mounts (always present, not overridable)
         let builtin_entries = [
             (&pi_data_dir, "/root/.pi"),
             (&agent_dir, "/root/.pi/agent"),
@@ -139,9 +138,8 @@ impl Ramekin {
             })
             .collect();
 
-        // Config mounts (user-configurable, skipped when source doesn't exist)
-        let config =
-            config::Config::load(&workspace).wrap_err("failed to load ramekin configuration")?;
+        let config = config::Config::load(&workspace, builtin_mounts)
+            .wrap_err("failed to load ramekin configuration")?;
 
         Ok(Self {
             workspace,
@@ -150,7 +148,6 @@ impl Ramekin {
             repo_sessions_dir,
             cache_dir,
             custom_dockerfile,
-            builtin_mounts,
             config,
         })
     }
@@ -185,18 +182,7 @@ impl Ramekin {
         );
 
         println!();
-        println!("Built-in volume mounts");
-        for m in &self.builtin_mounts {
-            println!(
-                "  {} {} → {}",
-                check(&m.source),
-                m.source.display(),
-                m.display_target()
-            );
-        }
-
-        println!();
-        println!("Config volume mounts");
+        println!("Volume mounts");
         let merged = self.config.merged_mounts();
         if merged.is_empty() {
             println!("  (none)");
@@ -280,14 +266,12 @@ impl Ramekin {
             .create_cache_directory(format!("sessions/{session_id}"))
             .wrap_err("failed to create session directory")?;
 
-        let config_mounts: Vec<_> = self
+        let all_mounts: Vec<_> = self
             .config
             .merged_mounts()
             .into_iter()
             .map(|(_, m)| m)
             .collect();
-        let all_mounts: Vec<&config::ResolvedMount> =
-            self.builtin_mounts.iter().chain(config_mounts).collect();
         let compose = generate_compose(&dockerfile, &build_context, &all_mounts);
         let compose_file = session_dir.join("compose.yml");
         fs_err::write(&compose_file, &compose)?;