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.
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)?;