diff --git a/Cargo.lock b/Cargo.lock index aac61b40..e8eaa89a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "c0mpute-api" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "c0mpute-proto", @@ -501,7 +501,7 @@ dependencies = [ [[package]] name = "c0mpute-bench" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "blake3", @@ -512,7 +512,7 @@ dependencies = [ [[package]] name = "c0mpute-cli" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "async-trait", @@ -549,7 +549,7 @@ dependencies = [ [[package]] name = "c0mpute-core" -version = "0.3.57" +version = "0.3.58" dependencies = [ "aes-gcm", "anyhow", @@ -603,7 +603,7 @@ dependencies = [ [[package]] name = "c0mpute-doctor" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "serde", @@ -615,7 +615,7 @@ dependencies = [ [[package]] name = "c0mpute-envelope" -version = "0.3.57" +version = "0.3.58" dependencies = [ "blake3", "bs58", @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "c0mpute-gateway" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "axum", @@ -650,7 +650,7 @@ dependencies = [ [[package]] name = "c0mpute-market" -version = "0.3.57" +version = "0.3.58" dependencies = [ "c0mpute-envelope", "serde", @@ -660,7 +660,7 @@ dependencies = [ [[package]] name = "c0mpute-net" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "async-trait", @@ -681,7 +681,7 @@ dependencies = [ [[package]] name = "c0mpute-placement" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "async-trait", @@ -702,7 +702,7 @@ dependencies = [ [[package]] name = "c0mpute-proto" -version = "0.3.57" +version = "0.3.58" dependencies = [ "blake3", "hex", @@ -714,7 +714,7 @@ dependencies = [ [[package]] name = "c0mpute-sealed" -version = "0.3.57" +version = "0.3.58" dependencies = [ "aes", "aes-gcm", @@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "c0mpute-secure-chat" -version = "0.3.57" +version = "0.3.58" dependencies = [ "aes-gcm", "anyhow", @@ -759,7 +759,7 @@ dependencies = [ [[package]] name = "c0mpute-store" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "blake3", @@ -778,7 +778,7 @@ dependencies = [ [[package]] name = "c0mpute-transcode" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "c0mpute-proto", @@ -791,7 +791,7 @@ dependencies = [ [[package]] name = "c0mpute-update" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "hex", @@ -806,7 +806,7 @@ dependencies = [ [[package]] name = "c0mpute-verify" -version = "0.3.57" +version = "0.3.58" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 71fa53d5..dc22616d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.3.57" +version = "0.3.58" edition = "2024" license = "MIT" repository = "https://github.com/profullstack/c0mpute" diff --git a/node/crates/c0mpute-cli/src/swarm_cli.rs b/node/crates/c0mpute-cli/src/swarm_cli.rs index 843c330b..73464e57 100644 --- a/node/crates/c0mpute-cli/src/swarm_cli.rs +++ b/node/crates/c0mpute-cli/src/swarm_cli.rs @@ -138,7 +138,25 @@ async fn boot(config_path: &std::path::Path, quiet: bool) -> Result { let cfg = Config::load_or_default(config_path)?; let id_dir = agent_swarm::cli_identity_dir().context("no data directory")?; let net = c0mpute_core::dm::boot_client(&id_dir).await?; - let svc = SwarmService::start(net.clone(), &agent_swarm::dir_under(&cfg.storage.root), cfg.swarm.lobby).await?; + let shared = agent_swarm::dir_under(&cfg.storage.root); + let dir = if writable(&shared) { + shared + } else { + // The node runs as another user (a root service beside a user + // CLI): work from our own directory. This CLI still syncs and + // gossips through its own node; that worker just won't see swarms + // joined here until joined there too. + let own = c0mpute_core::config::data_dir().context("no data directory")?.join("swarm"); + if !quiet { + eprintln!( + "note: {} is not writable by this user; using {} (the local worker will not see swarms joined here)", + shared.display(), + own.display() + ); + } + own + }; + let svc = SwarmService::start(net.clone(), &dir, cfg.swarm.lobby).await?; // Reach some peers, let gossipsub learn their subscriptions, catch up. let deadline = tokio::time::Instant::now() + Duration::from_secs(15); while net.connected_peers().await.is_empty() && tokio::time::Instant::now() < deadline { @@ -157,6 +175,45 @@ async fn boot(config_path: &std::path::Path, quiet: bool) -> Result { Ok(Ctx { svc, cfg }) } +/// Whether this user can create files in `dir` (creating it if missing). +fn writable(dir: &std::path::Path) -> bool { + if std::fs::create_dir_all(dir).is_err() { + return false; + } + // The directory, every topic directory in it, and every file we append + // to or rewrite (a root-owned file inside a writable directory fails + // the same way). + fn dir_ok(d: &std::path::Path) -> bool { + let probe = d.join(format!(".write-probe-{}", std::process::id())); + let ok = std::fs::write(&probe, b"").is_ok(); + let _ = std::fs::remove_file(&probe); + ok + } + fn file_ok(f: &std::path::Path) -> bool { + std::fs::OpenOptions::new().append(true).open(f).is_ok() + } + if !dir_ok(dir) { + return false; + } + let Ok(entries) = std::fs::read_dir(dir) else { return false }; + for e in entries.flatten() { + let p = e.path(); + if p.is_dir() { + if !dir_ok(&p) { + return false; + } + if let Ok(inner) = std::fs::read_dir(&p) + && !inner.flatten().map(|f| f.path()).filter(|f| f.is_file()).all(|f| file_ok(&f)) + { + return false; + } + } else if p.is_file() && !file_ok(&p) { + return false; + } + } + true +} + /// Give gossip a moment to leave before the process exits. async fn linger() { tokio::time::sleep(Duration::from_secs(3)).await; @@ -633,4 +690,24 @@ mod tests { assert!(names.contains(&n.to_string()), "{n}"); } } + + #[test] + fn writable_dirs() { + let d = tempfile::tempdir().unwrap(); + let sw = d.path().join("swarm"); + assert!(writable(&sw), "created and writable"); + std::fs::create_dir_all(sw.join("abc")).unwrap(); + std::fs::write(sw.join("abc/events.jsonl"), b"").unwrap(); + assert!(writable(&sw)); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let f = sw.join("abc/events.jsonl"); + std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o444)).unwrap(); + // root writes anyway; assert only when this user really cannot. + if std::fs::OpenOptions::new().append(true).open(&f).is_err() { + assert!(!writable(&sw), "a read-only log means: use our own dir"); + } + } + } }