systemprompt_config/
state_dir.rs1use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use crate::paths::WritableRoot;
19
20const PROBE: &str = ".systemprompt-write-probe";
21
22static PROBE_SEQ: AtomicU64 = AtomicU64::new(0);
23
24#[derive(Debug, thiserror::Error)]
25#[error("{}{}: {source}{ownership}", label(*.name), dir.display())]
26pub struct StateDirError {
27 pub name: Option<&'static str>,
28 pub dir: PathBuf,
29 pub ownership: String,
30 #[source]
31 pub source: std::io::Error,
32}
33
34#[derive(Debug, thiserror::Error)]
35#[error(
36 "Runtime-state directories are not writable by this process:\n{}\nMount each as a \
37 persistent volume writable by the server's uid (in the published image: uid 1000; on \
38 Kubernetes set securityContext.fsGroup: 1000).",
39 failures.iter().map(|e| format!(" - {e}")).collect::<Vec<_>>().join("\n")
40)]
41pub struct StateDirsError {
42 pub failures: Vec<StateDirError>,
43}
44
45fn label(name: Option<&'static str>) -> String {
46 name.map(|n| format!("{n} ")).unwrap_or_default()
47}
48
49pub fn create_state_dir(dir: &Path) -> Result<(), StateDirError> {
50 probe_dir(None, dir)
51}
52
53fn probe_dir(name: Option<&'static str>, dir: &Path) -> Result<(), StateDirError> {
54 let probe = dir.join(format!(
55 "{PROBE}-{}-{}",
56 std::process::id(),
57 PROBE_SEQ.fetch_add(1, Ordering::Relaxed)
58 ));
59 let outcome = std::fs::create_dir_all(dir)
60 .and_then(|()| std::fs::write(&probe, b""))
61 .and_then(|()| std::fs::remove_file(&probe));
62 outcome.map_err(|source| StateDirError {
63 name,
64 dir: dir.to_path_buf(),
65 ownership: ownership(dir),
66 source,
67 })
68}
69
70pub fn ensure_state_dirs_writable(roots: &[WritableRoot]) -> Result<(), StateDirsError> {
71 let failures: Vec<StateDirError> = roots
72 .iter()
73 .filter_map(|root| probe_dir(Some(root.name), &root.path).err())
74 .collect();
75 if failures.is_empty() {
76 return Ok(());
77 }
78 Err(StateDirsError { failures })
79}
80
81#[cfg(unix)]
82fn ownership(dir: &Path) -> String {
83 use std::os::unix::fs::MetadataExt;
84 let uid = nix::unistd::geteuid();
85 let owner = dir
88 .ancestors()
89 .find_map(|p| std::fs::metadata(p).ok().map(|m| (p.to_path_buf(), m)))
90 .map(|(p, m)| {
91 format!(
92 "; {} is owned by uid {} gid {} mode {:o}",
93 p.display(),
94 m.uid(),
95 m.gid(),
96 m.mode() & 0o7777
97 )
98 })
99 .unwrap_or_default();
100 format!(" (running as uid {uid}{owner}; the mount must be writable by uid {uid})")
101}
102
103#[cfg(not(unix))]
104fn ownership(_dir: &Path) -> String {
105 String::new()
106}