hive-claude adds its variables on top of the parent's environment, so claude inherited the daemon's store identity paths, CREDENTIALS_DIRECTORY and queue coordinates. claude now starts as this same binary with SWARM_REVIEW_CLASSIFIER_EXEC_CLAUDE naming the real one; main execs it before the runtime starts, with the environment cleared down to the API key, PATH, HOME, CLAUDE_CONFIG_DIR and claude's telemetry settings. The exec keeps the pid, so hive-claude's cancel and idle timeout still reach it. An integration test runs the built binary as the trampoline and checks that a BAO_* variable does not reach the program it execs.
85 lines
2.8 KiB
Rust
85 lines
2.8 KiB
Rust
//! 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<Item = (OsString, OsString)>) -> 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<String> = allowed(daemon)
|
|
.into_iter()
|
|
.map(|(n, _)| n.to_string_lossy().into_owned())
|
|
.collect();
|
|
assert_eq!(
|
|
kept,
|
|
["ANTHROPIC_API_KEY", "HOME", "OTEL_RESOURCE_ATTRIBUTES"]
|
|
);
|
|
}
|
|
}
|