use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
use warble::{ProfileComponentMount, ProfileFile};
const SUPPORTED_OVERLAY_VERSION: u32 = 1;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OverlayFile {
pub overlay: u32,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub mount: Vec<OverlayMount>,
#[serde(default)]
pub unmount: Vec<String>,
#[serde(default)]
pub bind: HashMap<String, HashMap<String, serde_yaml::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OverlayMount {
#[serde(rename = "use")]
pub use_id: String,
#[serde(default = "default_true")]
pub entrypoint: bool,
#[serde(default)]
pub bind: Option<HashMap<String, serde_yaml::Value>>,
}
fn default_true() -> bool {
true
}
pub fn read_overlay(path: &Path) -> Result<OverlayFile, String> {
let raw = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read overlay {}: {e}", path.display()))?;
let overlay: OverlayFile = serde_yaml::from_str(&raw)
.map_err(|e| format!("failed to parse overlay {}: {e}", path.display()))?;
if overlay.overlay != SUPPORTED_OVERLAY_VERSION {
return Err(format!(
"overlay {} declares format version {}, which this build does not understand \
(supported: {SUPPORTED_OVERLAY_VERSION})",
path.display(),
overlay.overlay
));
}
Ok(overlay)
}
pub fn apply_overlay(profile: &mut ProfileFile, overlay: &OverlayFile) -> Result<(), String> {
for entry in &overlay.mount {
if overlay.unmount.contains(&entry.use_id) {
return Err(format!(
"overlay both mounts and unmounts '{}' — the intent is contradictory, so it is \
refused rather than resolved in one direction",
entry.use_id
));
}
}
let mut seen: Vec<&str> = Vec::with_capacity(overlay.mount.len());
for entry in &overlay.mount {
if seen.contains(&entry.use_id.as_str()) {
return Err(format!(
"overlay mounts '{}' more than once — taking the last would silently discard the \
other entry's bind values",
entry.use_id
));
}
seen.push(&entry.use_id);
}
for id in &overlay.unmount {
if !profile.components.iter().any(|m| &m.use_id == id) {
return Err(format!(
"overlay unmounts '{id}', which the profile does not mount — a typo here would \
otherwise remove nothing while appearing to"
));
}
}
for id in overlay.bind.keys() {
if !profile.components.iter().any(|m| &m.use_id == id) {
return Err(format!(
"overlay binds values on '{id}', which the profile does not mount — to add it, \
use 'mount' with its bind values"
));
}
}
for entry in &overlay.mount {
if profile.components.iter().any(|m| m.use_id == entry.use_id) {
return Err(format!(
"overlay mounts '{}', which the profile already mounts — use 'bind' to change an \
existing mount's values",
entry.use_id
));
}
}
if let Some(system_prompt) = &overlay.system_prompt {
profile.system_prompt = Some(system_prompt.clone());
}
profile
.components
.retain(|m| !overlay.unmount.contains(&m.use_id));
for (id, values) in &overlay.bind {
let mount = profile
.components
.iter_mut()
.find(|m| &m.use_id == id)
.expect("presence was checked above");
let existing = mount.bind.get_or_insert_with(HashMap::new);
for (key, value) in values {
existing.insert(key.clone(), value.clone());
}
}
for entry in &overlay.mount {
profile.components.push(ProfileComponentMount {
use_id: entry.use_id.clone(),
entrypoint: entry.entrypoint,
config: None,
bind: entry.bind.clone(),
tier_overrides: None,
realization_kind: None,
guardrails: None,
brief: None,
});
}
if profile.components.is_empty() {
return Err(
"overlay leaves the profile with no mounted behaviors, which is not a harness that \
can be dispatched"
.to_string(),
);
}
Ok(())
}