//! The `claude` child's environment: an allowlist, not the daemon's. //! //! `hive-claude` sets its variables on top of the parent's environment, and //! the daemon's holds the store identity paths and the queue coordinates. So //! `claude` is spawned as this same binary with [`EXEC_CLAUDE`] naming the //! real one: [`exec_if_requested`] runs first in `main`, clears the //! environment down to [`ALLOWED`] and execs `claude` with the same //! arguments, in the same process. use std::ffi::OsString; use std::os::unix::process::CommandExt as _; use std::process::Command; /// Set on the child only: the `claude` binary to exec. pub const EXEC_CLAUDE: &str = "SWARM_REVIEW_CLASSIFIER_EXEC_CLAUDE"; /// Every variable `claude` keeps: the key, what it needs to run, and its /// telemetry settings. const ALLOWED: &[&str] = &[ "ANTHROPIC_API_KEY", "PATH", "HOME", "CLAUDE_CONFIG_DIR", "CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_METRICS_EXPORTER", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE", "OTEL_RESOURCE_ATTRIBUTES", ]; /// The variables of `vars` on [`ALLOWED`]. pub fn allowed(vars: impl IntoIterator) -> Vec<(OsString, OsString)> { vars.into_iter() .filter(|(name, _)| ALLOWED.iter().any(|a| name == a)) .collect() } /// When this process is the trampoline, exec `claude` and never return. pub fn exec_if_requested() { let Some(claude) = std::env::var_os(EXEC_CLAUDE) else { return; }; let error = Command::new(&claude) .args(std::env::args_os().skip(1)) .env_clear() .envs(allowed(std::env::vars_os())) .exec(); eprintln!("exec {}: {error}", claude.to_string_lossy()); std::process::exit(127); } #[cfg(test)] mod tests { use super::*; #[test] fn only_the_allowlist_reaches_claude() { let daemon = [ ("ANTHROPIC_API_KEY", "k"), ("HOME", "/tmp/run"), ("OTEL_RESOURCE_ATTRIBUTES", "repo=o/r,pr=7"), ( "BAO_CLIENT_KEY", "/run/credentials/x/review-classifier-bao-key", ), ("BAO_ADDR", "https://bao.example:8200"), ("CREDENTIALS_DIRECTORY", "/run/credentials/x"), ( "SWARM_REVIEW_CLASSIFIER_NATS_URL", "tls://nats.example:4222", ), (EXEC_CLAUDE, "/bin/claude"), ] .map(|(n, v)| (OsString::from(n), OsString::from(v))); let kept: Vec = allowed(daemon) .into_iter() .map(|(n, _)| n.to_string_lossy().into_owned()) .collect(); assert_eq!( kept, ["ANTHROPIC_API_KEY", "HOME", "OTEL_RESOURCE_ATTRIBUTES"] ); } }