use std::path::{Component, Path, PathBuf};
use crate::{containment::StateStoreReport, errors::RuntimeError};
pub const STATE_PREOPEN_NAME: &str = "state";
const STATE_ROOT_DIR: &str = "state";
const STATE_DIR_MODE: u32 = 0o700;
pub fn validate_store_name(store: &str) -> Result<(), RuntimeError> {
let refuse = |message: &str| {
Err(RuntimeError::InvalidStateStore {
store: store.to_string(),
message: message.to_string(),
})
};
if store.is_empty() {
return refuse("a store name must not be empty");
}
if store.contains('/') {
return refuse("a store name is a single path segment and must not contain '/'");
}
if store.starts_with('.') {
return refuse("a store name must not begin with '.'");
}
let path = Path::new(store);
if path.is_absolute() {
return refuse("a store name is a single path segment and must not be absolute");
}
let mut segments = path.components();
match (segments.next(), segments.next()) {
(Some(Component::Normal(_)), None) => Ok(()),
_ => refuse("a store name is a single path segment and must not contain '.' or '..'"),
}
}
pub fn ensure_state_store(store: &str) -> Result<PathBuf, RuntimeError> {
validate_store_name(store)?;
let home = crate::murmur_home::ensure_murmur_home().map_err(|message| {
RuntimeError::StateStoreUnavailable {
store: store.to_string(),
path: "~/.murmur".to_string(),
message,
}
})?;
ensure_state_store_in(&home, store)
}
pub fn state_store_reports<'a, I>(
artifacts: I,
capsule_name: &str,
) -> Result<Vec<StateStoreReport>, RuntimeError>
where
I: IntoIterator<Item = (&'a str, Option<&'a murmur_artifact::Capabilities>)>,
{
let mut reports = Vec::new();
let mut home: Option<PathBuf> = None;
for (name, capabilities) in artifacts {
let Some(state) = capabilities.and_then(|capabilities| capabilities.state.as_ref()) else {
continue;
};
let store = resolve_store_name(state, capsule_name);
validate_store_name(&store)?;
let home = match home.as_deref() {
Some(home) => home.to_path_buf(),
None => home.insert(murmur_home(&store)?).clone(),
};
reports.push(StateStoreReport {
artifact: name.to_string(),
host_path: state_store_path_in(&home, &store)?.display().to_string(),
store,
});
}
Ok(reports)
}
pub(crate) fn resolve_store_name(
state: &murmur_artifact::StateCapabilities,
capsule_name: &str,
) -> String {
state
.store
.clone()
.unwrap_or_else(|| capsule_name.to_string())
}
fn state_store_path_in(murmur_home: &Path, store: &str) -> Result<PathBuf, RuntimeError> {
validate_store_name(store)?;
Ok(murmur_home.join(STATE_ROOT_DIR).join(store))
}
fn ensure_state_store_in(murmur_home: &Path, store: &str) -> Result<PathBuf, RuntimeError> {
let store_path = state_store_path_in(murmur_home, store)?;
let root = store_path
.parent()
.expect("a store path always has the state root as its parent");
create_private_dir(store, root)?;
create_private_dir(store, &store_path)?;
Ok(store_path)
}
fn murmur_home(store: &str) -> Result<PathBuf, RuntimeError> {
murmur_home_dir().map_err(|message| RuntimeError::StateStoreUnavailable {
store: store.to_string(),
path: "~/.murmur".to_string(),
message,
})
}
pub(crate) fn murmur_home_dir() -> Result<PathBuf, String> {
let home = std::env::var_os("HOME")
.filter(|home| !home.is_empty())
.map(PathBuf::from)
.ok_or_else(|| {
"the home directory could not be resolved: HOME is not set in the environment"
.to_string()
})?;
if !home.is_absolute() {
return Err(
"the home directory could not be resolved: HOME is not an absolute path".to_string(),
);
}
Ok(home.join(".murmur"))
}
fn create_private_dir(store: &str, path: &Path) -> Result<(), RuntimeError> {
ensure_private_dir(path, STATE_DIR_MODE).map_err(|message| {
RuntimeError::StateStoreUnavailable {
store: store.to_string(),
path: path.display().to_string(),
message,
}
})
}
pub(crate) fn ensure_private_dir(path: &Path, mode: u32) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
std::fs::create_dir_all(path)
.map_err(|err| format!("failed to create the directory: {err}"))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|err| format!("failed to set mode {mode:04o}: {err}"))
}
#[cfg(test)]
mod tests {
use super::*;
use murmur_artifact::{Capabilities, StateCapabilities};
fn state_capabilities(store: Option<&str>) -> Capabilities {
Capabilities {
network: None,
peer_fetch: None,
filesystem: None,
shell: None,
spawn: None,
env: None,
limits: None,
resources: None,
state: Some(StateCapabilities {
store: store.map(str::to_string),
}),
task_io: None,
conversation: None,
plan: None,
containment: None,
}
}
#[test]
fn a_single_segment_is_the_only_accepted_store_name() {
for store in ["shey", "murmur-tool-corpus", "notes_2026", "a"] {
validate_store_name(store).unwrap_or_else(|err| panic!("{store} must be valid: {err}"));
}
}
#[test]
fn a_store_name_that_is_not_a_single_segment_is_refused_by_name() {
for store in [
"",
"../escape",
"/abs/path",
"a/b",
".",
"..",
".hidden",
"a/",
] {
let err = match validate_store_name(store) {
Ok(()) => panic!("'{store}' must be refused"),
Err(err) => err,
};
assert!(
matches!(err, RuntimeError::InvalidStateStore { .. }),
"'{store}' must refuse as InvalidStateStore, got: {err}"
);
assert!(
err.to_string().contains(&format!("'{store}'")),
"the refusal must quote the offending value: {err}"
);
}
}
#[test]
fn resolving_a_path_creates_nothing() {
let home = tempfile::tempdir().unwrap();
let murmur_home = home.path().join(".murmur");
let path = state_store_path_in(&murmur_home, "shey").unwrap();
assert_eq!(path, murmur_home.join("state/shey"));
assert!(!murmur_home.exists());
}
#[test]
fn ensuring_a_store_creates_the_root_and_the_store_at_0700() {
use std::os::unix::fs::PermissionsExt;
let home = tempfile::tempdir().unwrap();
let murmur_home = home.path().join(".murmur");
let path = ensure_state_store_in(&murmur_home, "shey").unwrap();
let root = murmur_home.join("state");
assert_eq!(path, root.join("shey"));
for dir in [&root, &path] {
assert!(dir.is_dir(), "{} must exist", dir.display());
let mode = std::fs::metadata(dir).unwrap().permissions().mode() & 0o777;
assert_eq!(
mode,
0o700,
"{} must be 0700, got {mode:04o}",
dir.display()
);
}
std::fs::write(path.join("notes.jsonl"), b"one\n").unwrap();
assert_eq!(ensure_state_store_in(&murmur_home, "shey").unwrap(), path);
assert_eq!(
std::fs::read_to_string(path.join("notes.jsonl")).unwrap(),
"one\n"
);
}
#[test]
fn ensuring_a_store_reasserts_the_mode_on_an_existing_directory() {
use std::os::unix::fs::PermissionsExt;
let home = tempfile::tempdir().unwrap();
let murmur_home = home.path().join(".murmur");
let path = murmur_home.join("state/shey");
std::fs::create_dir_all(&path).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
ensure_state_store_in(&murmur_home, "shey").unwrap();
let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
}
#[test]
fn ensuring_a_store_holds_a_wide_murmur_home_owner_only() {
let home = tempfile::tempdir().unwrap();
crate::murmur_home::wide_dir(&home.path().join(".murmur"), 0o755);
crate::murmur_home::run_with_home(
"state_store::tests::inner_ensure_state_store_under_scratch_home",
home.path(),
);
assert_eq!(
crate::murmur_home::mode_of(&home.path().join(".murmur")),
0o700
);
assert_eq!(
crate::murmur_home::mode_of(&home.path().join(".murmur/state/shey")),
0o700
);
}
#[test]
#[ignore = "run by ensuring_a_store_holds_a_wide_murmur_home_owner_only"]
fn inner_ensure_state_store_under_scratch_home() {
if !crate::murmur_home::in_scratch_home() {
return;
}
ensure_state_store("shey").unwrap();
}
#[test]
fn two_store_names_never_resolve_into_one_directory() {
let home = tempfile::tempdir().unwrap();
let murmur_home = home.path().join(".murmur");
let first = ensure_state_store_in(&murmur_home, "capsule-one").unwrap();
let second = ensure_state_store_in(&murmur_home, "capsule-two").unwrap();
assert_ne!(first, second);
std::fs::write(first.join("notes.jsonl"), b"one\n").unwrap();
assert!(!second.join("notes.jsonl").exists());
}
#[test]
fn an_unresolvable_home_names_the_store_that_needed_it() {
let err = RuntimeError::StateStoreUnavailable {
store: "shey".to_string(),
path: "~/.murmur".to_string(),
message: "the home directory could not be resolved: HOME is not set in the environment"
.to_string(),
};
let message = err.to_string();
assert!(message.contains("shey"), "must name the store: {message}");
assert!(
message.contains("home directory could not be resolved"),
"must say what could not be resolved: {message}"
);
}
#[test]
fn a_malformed_name_is_refused_before_the_home_lookup() {
let declared = state_capabilities(Some("a/b"));
assert!(matches!(
state_store_reports([("notes-tool", Some(&declared))], "state-capsule"),
Err(RuntimeError::InvalidStateStore { .. })
));
}
#[test]
fn reports_default_the_store_name_to_the_capsule_name() {
let declared = state_capabilities(None);
let named = state_capabilities(Some("shey"));
let reports = state_store_reports(
[
("notes-tool", Some(&declared)),
("corpus-tool", Some(&named)),
("plain-tool", None),
],
"state-capsule",
)
.unwrap();
assert_eq!(reports.len(), 2, "an undeclared artifact reports nothing");
assert_eq!(reports[0].artifact, "notes-tool");
assert_eq!(reports[0].store, "state-capsule");
assert!(reports[0]
.host_path
.ends_with(".murmur/state/state-capsule"));
assert_eq!(reports[1].artifact, "corpus-tool");
assert_eq!(reports[1].store, "shey");
assert!(reports[1].host_path.ends_with(".murmur/state/shey"));
}
#[test]
fn an_undeclared_artifact_set_reports_nothing_and_resolves_no_home() {
let plain = Capabilities {
state: None,
..state_capabilities(None)
};
assert!(state_store_reports(
[("plain-tool", Some(&plain)), ("bare-tool", None)],
"capsule"
)
.unwrap()
.is_empty());
}
}