Watch
0
0
Fork
You've already forked hyperhive
0

swarm-review-classifier: claude's environment is an allowlist, not the daemon's

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.
This commit is contained in:
atlas 2026-10-12 04:15:50 +02:00 • committed by mara
commit af1495731e
4 changed files with 176 additions and 21 deletions

View file

@ -4,7 +4,8 @@
//! Each classification is one `claude --print` with no session, no tools and
//! no MCP server, in a directory made for it and removed after it, so nothing
//! carries from one run to the next. The API key is read from the store for
//! every run and reaches only the child's environment.
//! every run and reaches only the child's environment, which keeps nothing of
//! the daemon's beyond [`crate::child_env`]'s allowlist.
use std::future::Future;
use std::path::{Path, PathBuf};
@ -113,9 +114,13 @@ impl KeySource for Store {
}
}
/// The `claude` binary the classifications run on.
/// The `claude` binary the classifications run on, started through the
/// trampoline that clears its environment.
pub struct Claude {
pub program: PathBuf,
/// This binary, which execs [`Self::program`] when started as the
/// trampoline.
pub trampoline: PathBuf,
}
impl Model for Claude {
@ -142,23 +147,7 @@ impl Claude {
.await
.and(tokio::fs::write(&system, prompt::SYSTEM).await)
.map_err(|e| format!("preparing the run's directory: {e}"))?;
let config = Config {
model: Some(MODEL.to_owned()),
cwd: Some(dir.to_owned()),
system_prompt_file: Some(system),
strict_mcp_config: true,
// `--tools ""`: no built-in tool at all.
tools: Some(String::new()),
program: Some(self.program.clone()),
idle_timeout: Some(IDLE_TIMEOUT),
claude_config_dir: Some(dir.join("config")),
env: vec![
("ANTHROPIC_API_KEY".to_owned(), api_key.to_owned()),
("HOME".to_owned(), dir.display().to_string()),
("OTEL_RESOURCE_ATTRIBUTES".to_owned(), attributes.to_owned()),
],
..Config::default()
};
let config = self.config(dir, system, api_key, attributes);
let running =
hive_claude::Claude::spawn(&config, &Attach::OneOff).map_err(|e| e.to_string())?;
let cancel = running.cancel_handle();
@ -178,6 +167,32 @@ impl Claude {
}
}
}
/// One classification's invocation: the trampoline as the program, with
/// the real `claude` and the run's own variables in its environment.
fn config(&self, dir: &Path, system: PathBuf, api_key: &str, attributes: &str) -> Config {
Config {
model: Some(MODEL.to_owned()),
cwd: Some(dir.to_owned()),
system_prompt_file: Some(system),
strict_mcp_config: true,
// `--tools ""`: no built-in tool at all.
tools: Some(String::new()),
program: Some(self.trampoline.clone()),
idle_timeout: Some(IDLE_TIMEOUT),
claude_config_dir: Some(dir.join("config")),
env: vec![
(
crate::child_env::EXEC_CLAUDE.to_owned(),
self.program.display().to_string(),
),
("ANTHROPIC_API_KEY".to_owned(), api_key.to_owned()),
("HOME".to_owned(), dir.display().to_string()),
("OTEL_RESOURCE_ATTRIBUTES".to_owned(), attributes.to_owned()),
],
..Config::default()
}
}
}
/// A directory of its own for one classification.
@ -329,6 +344,27 @@ mod tests {
);
}
#[test]
fn claude_is_started_through_the_trampoline() {
let claude = Claude {
program: PathBuf::from("/nix/store/x-claude-code/bin/claude"),
trampoline: PathBuf::from(
"/nix/store/y-swarm-review-classifier/bin/swarm-review-classifier",
),
};
let config = claude.config(
Path::new("/tmp/run"),
PathBuf::from("/tmp/run/system.md"),
"k",
"a=b",
);
assert_eq!(config.program, Some(claude.trampoline.clone()));
assert!(config.env.contains(&(
crate::child_env::EXEC_CLAUDE.to_owned(),
"/nix/store/x-claude-code/bin/claude".to_owned()
)));
}
#[test]
fn the_repo_and_pr_extend_the_containers_own_attributes() {
let r = run(&[]);