use std::collections::BTreeMap;
use std::fmt;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub const DEFAULT_STATE_DIR: &str = ".cargoless";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Default,
TfToml,
Env,
Cli,
}
impl Source {
pub fn describe(self) -> &'static str {
match self {
Source::Default => "default (v0-compatible)",
Source::TfToml => "tf.toml",
Source::Env => "environment",
Source::Cli => "CLI flag",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Provenance {
pub cas_dir: Source,
pub state_dir: Source,
pub repo: Source,
pub bind: Source,
pub corun: Source,
pub auth_token: Source,
}
impl Default for Provenance {
fn default() -> Self {
Self {
cas_dir: Source::Default,
state_dir: Source::Default,
repo: Source::Default,
bind: Source::Default,
corun: Source::Default,
auth_token: Source::Default,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FleetOverrides {
pub cas_dir: Option<PathBuf>,
pub state_dir: Option<PathBuf>,
pub repo: Option<PathBuf>,
pub bind: Option<String>,
pub corun: Option<bool>,
pub auth_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FleetConfig {
pub cas_dir: Option<PathBuf>,
pub state_dir: PathBuf,
pub repo: Option<PathBuf>,
pub bind: Option<SocketAddr>,
pub corun: bool,
pub auth_token: Option<String>,
pub provenance: Provenance,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FleetConfigError {
BadTfToml {
line_no: usize,
line: String,
why: String,
},
BadBind {
value: String,
why: String,
},
BadBool {
origin: &'static str,
key: String,
value: String,
},
}
impl fmt::Display for FleetConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FleetConfigError::BadTfToml { line_no, line, why } => write!(
f,
"tf.toml: {why} (line {line_no}: `{line}`).\n \
[fleet] keys: repo, bind, corun, auth_token. \
[project] state_dir. [cache] cas_dir."
),
FleetConfigError::BadBind { value, why } => write!(
f,
"invalid bind address `{value}`: {why}.\n \
expected `HOST:PORT`, e.g. `127.0.0.1:8080` (loopback, \
safe) or `0.0.0.0:8080` (network — requires --auth-token)."
),
FleetConfigError::BadBool { origin, key, value } => write!(
f,
"{origin}: `{key}` expects a boolean (`true`/`false`), \
got `{value}`."
),
}
}
}
impl std::error::Error for FleetConfigError {}
impl FleetConfig {
pub fn defaults() -> Self {
Self {
cas_dir: None,
state_dir: PathBuf::from(DEFAULT_STATE_DIR),
repo: None,
bind: None,
corun: true,
auth_token: None,
provenance: Provenance::default(),
}
}
pub fn resolve(
repo_root: impl AsRef<Path>,
overrides: FleetOverrides,
) -> Result<Self, FleetConfigError> {
let env = |k: &str| std::env::var(k).ok();
Self::resolve_layered(repo_root.as_ref(), overrides, &env)
}
pub fn resolve_layered(
repo_root: &Path,
ov: FleetOverrides,
env: &dyn Fn(&str) -> Option<String>,
) -> Result<Self, FleetConfigError> {
let mut cfg = Self::defaults();
if let Ok(text) = std::fs::read_to_string(repo_root.join("tf.toml")) {
apply_tf_toml_overlay(&mut cfg, &text)?;
}
if let Some(v) = env("TF_CAS_DIR").filter(|s| !s.is_empty()) {
cfg.cas_dir = Some(PathBuf::from(v));
cfg.provenance.cas_dir = Source::Env;
}
if let Some(v) = env("TF_STATE_DIR").filter(|s| !s.is_empty()) {
cfg.state_dir = PathBuf::from(v);
cfg.provenance.state_dir = Source::Env;
}
if let Some(v) = env("TF_REPO").filter(|s| !s.is_empty()) {
cfg.repo = Some(PathBuf::from(v));
cfg.provenance.repo = Source::Env;
}
if let Some(v) = env("TF_BIND").filter(|s| !s.is_empty()) {
cfg.bind = Some(parse_bind(&v)?);
cfg.provenance.bind = Source::Env;
}
if let Some(v) = env("TF_NO_CORUN").filter(|s| !s.is_empty()) {
if parse_bool("env TF_NO_CORUN", "TF_NO_CORUN", &v)? {
cfg.corun = false;
cfg.provenance.corun = Source::Env;
}
}
if let Some(v) = env("CARGOLESS_AUTH_TOKEN").filter(|s| !s.trim().is_empty()) {
cfg.auth_token = Some(v);
cfg.provenance.auth_token = Source::Env;
}
if let Some(v) = ov.cas_dir {
cfg.cas_dir = Some(v);
cfg.provenance.cas_dir = Source::Cli;
}
if let Some(v) = ov.state_dir {
cfg.state_dir = v;
cfg.provenance.state_dir = Source::Cli;
}
if let Some(v) = ov.repo {
cfg.repo = Some(v);
cfg.provenance.repo = Source::Cli;
}
if let Some(v) = ov.bind {
cfg.bind = Some(parse_bind(&v)?);
cfg.provenance.bind = Source::Cli;
}
if let Some(b) = ov.corun {
cfg.corun = b;
cfg.provenance.corun = Source::Cli;
}
if let Some(v) = ov.auth_token.filter(|s| !s.trim().is_empty()) {
cfg.auth_token = Some(v);
cfg.provenance.auth_token = Source::Cli;
}
Ok(cfg)
}
pub fn daemon_mode(&self) -> bool {
self.repo.is_some()
}
pub fn requires_auth(&self) -> bool {
match self.bind {
Some(addr) => !addr.ip().is_loopback(),
None => false,
}
}
pub fn effective_auth_token(&self) -> Option<&str> {
self.auth_token.as_deref().filter(|t| !t.trim().is_empty())
}
pub fn security_check(&self) -> Result<(), FleetConfigError> {
if self.requires_auth() && self.effective_auth_token().is_none() {
let value = self.bind.map(|a| a.to_string()).unwrap_or_default();
return Err(FleetConfigError::BadBind {
value,
why: "non-loopback bind requires --auth-token / \
CARGOLESS_AUTH_TOKEN (refusing unauthenticated \
network exposure)"
.to_string(),
});
}
Ok(())
}
pub fn state_dir_abs(&self, repo_root: &Path) -> PathBuf {
if self.state_dir.is_absolute() {
self.state_dir.clone()
} else {
repo_root.join(&self.state_dir)
}
}
}
fn parse_bind(s: &str) -> Result<SocketAddr, FleetConfigError> {
SocketAddr::from_str(s.trim()).map_err(|e| FleetConfigError::BadBind {
value: s.to_string(),
why: e.to_string(),
})
}
fn parse_bool(origin: &'static str, key: &str, v: &str) -> Result<bool, FleetConfigError> {
match v.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Ok(true),
"false" | "0" | "no" | "off" => Ok(false),
_ => Err(FleetConfigError::BadBool {
origin,
key: key.to_string(),
value: v.to_string(),
}),
}
}
fn apply_tf_toml_overlay(cfg: &mut FleetConfig, text: &str) -> Result<(), FleetConfigError> {
let mut section = String::new();
for (idx, raw) in text.lines().enumerate() {
let line_no = idx + 1;
let line = strip_comment(raw).trim();
if line.is_empty() {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
section = name.trim().to_string();
continue; }
let Some((key, val)) = line.split_once('=') else {
continue; };
let key = key.trim();
let val = unquote(val.trim());
match (section.as_str(), key) {
("project", "state_dir") => {
cfg.state_dir = PathBuf::from(&val);
cfg.provenance.state_dir = Source::TfToml;
}
("cache", "cas_dir") => {
cfg.cas_dir = Some(PathBuf::from(&val));
cfg.provenance.cas_dir = Source::TfToml;
}
("fleet", "repo") => {
cfg.repo = Some(PathBuf::from(&val));
cfg.provenance.repo = Source::TfToml;
}
("fleet", "bind") => {
cfg.bind = Some(parse_bind(&val).map_err(|_| FleetConfigError::BadTfToml {
line_no,
line: raw.trim().to_string(),
why: format!("invalid bind address `{val}`"),
})?);
cfg.provenance.bind = Source::TfToml;
}
("fleet", "corun") => {
let b = parse_bool("tf.toml", "corun", &val).map_err(|_| {
FleetConfigError::BadTfToml {
line_no,
line: raw.trim().to_string(),
why: format!("`corun` expects true/false, got `{val}`"),
}
})?;
cfg.corun = b;
cfg.provenance.corun = Source::TfToml;
}
("fleet", "auth_token") if !val.trim().is_empty() => {
cfg.auth_token = Some(val);
cfg.provenance.auth_token = Source::TfToml;
}
_ => {}
}
}
Ok(())
}
fn strip_comment(line: &str) -> &str {
let mut in_str = false;
for (i, c) in line.char_indices() {
match c {
'"' => in_str = !in_str,
'#' if !in_str => return &line[..i],
_ => {}
}
}
line
}
fn unquote(s: &str) -> String {
s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(s)
.to_string()
}
pub fn overrides_from_map(m: &BTreeMap<String, String>) -> FleetOverrides {
FleetOverrides {
cas_dir: m.get("cas-dir").map(PathBuf::from),
state_dir: m.get("state-dir").map(PathBuf::from),
repo: m.get("repo").map(PathBuf::from),
bind: m.get("bind").cloned(),
corun: if m.contains_key("no-corun") {
Some(false)
} else {
None
},
auth_token: m.get("auth-token").cloned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_env(_: &str) -> Option<String> {
None
}
#[test]
fn defaults_are_v0() {
let c = FleetConfig::defaults();
assert_eq!(c.cas_dir, None, "v0: per-process PID-scoped CAS");
assert_eq!(c.state_dir, PathBuf::from(".cargoless"));
assert_eq!(c.repo, None, "v0: no daemon mode");
assert_eq!(c.bind, None, "v0: no network transport");
assert!(c.corun, "corun default-on (inert until repo set)");
assert_eq!(c.auth_token, None);
assert!(!c.daemon_mode());
assert!(!c.requires_auth());
assert!(c.security_check().is_ok());
}
#[test]
fn resolve_no_inputs_equals_defaults() {
let tmp = std::env::temp_dir().join("cl-cfg-empty-xyz");
let _ = std::fs::create_dir_all(&tmp);
let c = FleetConfig::resolve_layered(&tmp, FleetOverrides::default(), &no_env).unwrap();
assert_eq!(c, FleetConfig::defaults());
}
#[test]
fn precedence_cli_beats_env_beats_toml_beats_default() {
let dir = std::env::temp_dir().join(format!("cl-cfg-prec-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("tf.toml"),
"[project]\nstate_dir = \".triform/cargoless\"\n\
[fleet]\nrepo = \"/from/toml\"\ncorun = false\n",
)
.unwrap();
let c = FleetConfig::resolve_layered(&dir, FleetOverrides::default(), &no_env).unwrap();
assert_eq!(c.state_dir, PathBuf::from(".triform/cargoless"));
assert_eq!(c.provenance.state_dir, Source::TfToml);
assert_eq!(c.repo, Some(PathBuf::from("/from/toml")));
assert!(!c.corun);
assert_eq!(c.provenance.corun, Source::TfToml);
let env = |k: &str| match k {
"TF_REPO" => Some("/from/env".to_string()),
_ => None,
};
let c = FleetConfig::resolve_layered(&dir, FleetOverrides::default(), &env).unwrap();
assert_eq!(c.repo, Some(PathBuf::from("/from/env")));
assert_eq!(c.provenance.repo, Source::Env);
assert_eq!(c.state_dir, PathBuf::from(".triform/cargoless"));
let ov = FleetOverrides {
repo: Some(PathBuf::from("/from/cli")),
..Default::default()
};
let c = FleetConfig::resolve_layered(&dir, ov, &env).unwrap();
assert_eq!(c.repo, Some(PathBuf::from("/from/cli")));
assert_eq!(c.provenance.repo, Source::Cli);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn tf_toml_overlay_is_tolerant_of_cli_owned_keys() {
let dir = std::env::temp_dir().join(format!("cl-cfg-tol-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("tf.toml"),
"[project]\nroot = \"/proj\"\ntarget = \"wasm32-unknown-unknown\"\n\
state_dir = \".triform/cargoless\"\n\
[cache]\ndir = \"/tmp/cache\"\ncas_dir = \"/shared/cas\"\n\
[serve]\nport = 8080\n",
)
.unwrap();
let c = FleetConfig::resolve_layered(&dir, FleetOverrides::default(), &no_env).unwrap();
assert_eq!(c.state_dir, PathBuf::from(".triform/cargoless"));
assert_eq!(c.cas_dir, Some(PathBuf::from("/shared/cas")));
assert_eq!(c.repo, None);
assert_eq!(c.bind, None);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn bind_parsing_and_auth_predicate() {
let ov = FleetOverrides {
bind: Some("127.0.0.1:8080".to_string()),
..Default::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert_eq!(c.bind.unwrap().to_string(), "127.0.0.1:8080");
assert!(!c.requires_auth());
assert!(c.security_check().is_ok());
let ov = FleetOverrides {
bind: Some("0.0.0.0:8080".to_string()),
..Default::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert!(c.requires_auth());
assert!(c.security_check().is_err());
let ov = FleetOverrides {
bind: Some("0.0.0.0:8080".to_string()),
auth_token: Some("s3cr3t".to_string()),
..Default::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert!(c.security_check().is_ok());
}
#[test]
fn bad_bind_is_actionable() {
let ov = FleetOverrides {
bind: Some("not-an-addr".to_string()),
..Default::default()
};
let e = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap_err();
assert!(matches!(e, FleetConfigError::BadBind { .. }));
assert!(e.to_string().contains("--auth-token"));
}
#[test]
fn no_corun_via_env_and_cli() {
let env = |k: &str| (k == "TF_NO_CORUN").then(|| "1".to_string());
let c = FleetConfig::resolve_layered(
std::path::Path::new("/nonexistent"),
FleetOverrides::default(),
&env,
)
.unwrap();
assert!(!c.corun);
assert_eq!(c.provenance.corun, Source::Env);
let ov = FleetOverrides {
corun: Some(false),
..Default::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert!(!c.corun);
assert_eq!(c.provenance.corun, Source::Cli);
}
#[test]
fn auth_token_prefers_env_over_toml() {
let dir = std::env::temp_dir().join(format!("cl-cfg-tok-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("tf.toml"), "[fleet]\nauth_token = \"from-toml\"\n").unwrap();
let env = |k: &str| (k == "CARGOLESS_AUTH_TOKEN").then(|| "from-env".to_string());
let c = FleetConfig::resolve_layered(&dir, FleetOverrides::default(), &env).unwrap();
assert_eq!(c.auth_token.as_deref(), Some("from-env"));
assert_eq!(c.provenance.auth_token, Source::Env);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn blank_auth_token_is_no_token_via_cli_env_toml() {
for blank in ["", " ", "\t", " \t "] {
let ov = FleetOverrides {
auth_token: Some(blank.to_string()),
..FleetOverrides::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert_eq!(c.auth_token, None, "CLI blank {blank:?} ⇒ no token");
assert_eq!(c.effective_auth_token(), None);
let env = |k: &str| (k == "CARGOLESS_AUTH_TOKEN").then(|| blank.to_string());
let c = FleetConfig::resolve_layered(
std::path::Path::new("/nonexistent"),
FleetOverrides::default(),
&env,
)
.unwrap();
assert_eq!(c.auth_token, None, "env blank {blank:?} ⇒ no token");
assert_eq!(c.effective_auth_token(), None);
let dir = std::env::temp_dir().join(format!(
"cl-cfg-blank-{}-{}",
std::process::id(),
blank.len()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("tf.toml"),
format!("[fleet]\nauth_token = \"{blank}\"\n"),
)
.unwrap();
let c = FleetConfig::resolve_layered(&dir, FleetOverrides::default(), &no_env).unwrap();
assert_eq!(c.auth_token, None, "toml blank {blank:?} ⇒ no token");
assert_eq!(c.effective_auth_token(), None);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn blank_auth_token_nonloopback_refuses_security_check() {
let ov = FleetOverrides {
bind: Some("0.0.0.0:8080".to_string()),
auth_token: Some(" ".to_string()),
..FleetOverrides::default()
};
let c = FleetConfig::resolve_layered(std::path::Path::new("/nonexistent"), ov, &no_env)
.unwrap();
assert!(
matches!(c.security_check(), Err(FleetConfigError::BadBind { .. })),
"non-loopback + blank CLI token MUST refuse (no unauth socket)"
);
let mut c2 = FleetConfig::defaults();
c2.bind = Some("0.0.0.0:9090".parse().unwrap());
c2.auth_token = Some(" \t ".to_string());
assert_eq!(c2.effective_auth_token(), None);
assert!(
matches!(c2.security_check(), Err(FleetConfigError::BadBind { .. })),
"blank token in FleetConfig + non-loopback MUST still refuse"
);
let mut ok = FleetConfig::defaults();
ok.bind = Some("0.0.0.0:9090".parse().unwrap());
ok.auth_token = Some("s3cr3t".to_string());
assert!(ok.security_check().is_ok());
assert_eq!(ok.effective_auth_token(), Some("s3cr3t"));
}
#[test]
fn state_dir_abs_resolution() {
let c = FleetConfig::defaults();
assert_eq!(
c.state_dir_abs(std::path::Path::new("/repo")),
PathBuf::from("/repo/.cargoless")
);
let mut c = FleetConfig::defaults();
c.state_dir = PathBuf::from("/abs/state");
assert_eq!(
c.state_dir_abs(std::path::Path::new("/repo")),
PathBuf::from("/abs/state")
);
}
#[test]
fn overrides_from_map_helper() {
let mut m = BTreeMap::new();
m.insert("repo".to_string(), "/r".to_string());
m.insert("no-corun".to_string(), String::new());
let ov = overrides_from_map(&m);
assert_eq!(ov.repo, Some(PathBuf::from("/r")));
assert_eq!(ov.corun, Some(false));
assert_eq!(ov.cas_dir, None);
}
}