use std::collections::BTreeMap;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExitStatus {
pub code: Option<i32>,
pub signal: Option<i32>,
}
impl ExitStatus {
pub fn from_code(code: i32) -> Self {
Self {
code: Some(code),
signal: None,
}
}
pub fn from_signal(signal: i32) -> Self {
Self {
code: None,
signal: Some(signal),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EnvMode {
InheritClean,
Replace,
Patch,
}
const EXPLICIT_SENSITIVE_ENV_NAMES: &[&str] = &[
"GITHUB_TOKEN",
"GH_TOKEN",
"HARN_CLOUD_API_KEY",
"BURIN_ADMIN_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
];
const SENSITIVE_ENV_PREFIXES: &[&str] = &[
"ANTHROPIC_",
"OPENAI_",
"OPENROUTER_",
"FIREWORKS_",
"TOGETHER_",
"XAI_",
"GROQ_",
];
pub fn is_sensitive_env_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
if EXPLICIT_SENSITIVE_ENV_NAMES.contains(&upper.as_str()) {
return true;
}
if SENSITIVE_ENV_PREFIXES
.iter()
.any(|prefix| upper.starts_with(prefix))
{
return true;
}
upper.ends_with("_API_KEY")
|| upper.ends_with("_TOKEN")
|| upper.ends_with("_SECRET")
|| upper.ends_with("_KEY")
}
#[derive(Clone, Debug)]
pub struct SpawnSpec {
pub builtin: &'static str,
pub program: String,
pub args: Vec<String>,
pub cwd: Option<PathBuf>,
pub env: BTreeMap<String, String>,
pub env_mode: EnvMode,
pub use_stdin: bool,
pub configure_process_group: bool,
}
pub trait ProcessHandle: Send {
fn pid(&self) -> Option<u32>;
fn process_group_id(&self) -> Option<u32>;
fn killer(&self) -> Arc<dyn ProcessKiller>;
fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>>;
fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>>;
fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>>;
fn wait_with_timeout(
&mut self,
timeout: Option<Duration>,
) -> io::Result<(Option<ExitStatus>, bool)>;
fn wait(&mut self) -> io::Result<ExitStatus>;
}
pub trait ProcessKiller: Send + Sync {
fn kill(&self);
}
pub trait ProcessSpawner: Send + Sync {
fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError>;
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ProcessError {
#[error("invalid argv: {0}")]
InvalidArgv(String),
#[error("sandbox setup failed: {0}")]
SandboxSetup(String),
#[error("sandbox cwd rejected: {0}")]
SandboxCwd(String),
#[error("sandbox rejected spawn: {0}")]
SandboxSpawn(String),
#[error("spawn failed: {0}")]
Spawn(String),
}
use std::cell::RefCell;
thread_local! {
static THREAD_SPAWNER: RefCell<Option<Arc<dyn ProcessSpawner>>> = const { RefCell::new(None) };
}
pub fn install_spawner(spawner: Arc<dyn ProcessSpawner>) -> SpawnerGuard {
let prev = THREAD_SPAWNER.with(|slot| slot.replace(Some(spawner)));
SpawnerGuard { prev: Some(prev) }
}
pub struct SpawnerGuard {
#[allow(clippy::option_option)]
prev: Option<Option<Arc<dyn ProcessSpawner>>>,
}
impl Drop for SpawnerGuard {
fn drop(&mut self) {
if let Some(prev) = self.prev.take() {
THREAD_SPAWNER.with(|slot| {
*slot.borrow_mut() = prev;
});
}
}
}
pub fn current_spawner() -> Arc<dyn ProcessSpawner> {
THREAD_SPAWNER
.with(|slot| slot.borrow().clone())
.unwrap_or_else(super::real::default_spawner)
}
pub fn spawn_process(spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
current_spawner().spawn(spec)
}
#[cfg(test)]
mod tests {
use super::is_sensitive_env_name;
#[test]
fn denies_secret_bearing_names() {
assert!(is_sensitive_env_name("ANTHROPIC_API_KEY"));
assert!(is_sensitive_env_name("OPENAI_API_KEY"));
assert!(is_sensitive_env_name("SOME_VENDOR_TOKEN"));
assert!(is_sensitive_env_name("MY_CLIENT_SECRET"));
assert!(is_sensitive_env_name("RANDOM_KEY"));
assert!(is_sensitive_env_name("GITHUB_TOKEN"));
assert!(is_sensitive_env_name("GH_TOKEN"));
assert!(is_sensitive_env_name("HARN_CLOUD_API_KEY"));
assert!(is_sensitive_env_name("BURIN_ADMIN_TOKEN"));
assert!(is_sensitive_env_name("AWS_SECRET_ACCESS_KEY"));
assert!(is_sensitive_env_name("AWS_SESSION_TOKEN"));
assert!(is_sensitive_env_name("OPENROUTER_BASE_URL"));
assert!(is_sensitive_env_name("FIREWORKS_ACCOUNT"));
assert!(is_sensitive_env_name("TOGETHER_ORG"));
assert!(is_sensitive_env_name("XAI_REGION"));
assert!(is_sensitive_env_name("GROQ_PROJECT"));
}
#[test]
fn allows_benign_build_and_toolchain_names() {
assert!(!is_sensitive_env_name("PATH"));
assert!(!is_sensitive_env_name("HOME"));
assert!(!is_sensitive_env_name("CARGO_HOME"));
assert!(!is_sensitive_env_name("LANG"));
assert!(!is_sensitive_env_name("LC_ALL"));
assert!(!is_sensitive_env_name("TERM"));
assert!(!is_sensitive_env_name("USER"));
assert!(!is_sensitive_env_name("RUSTUP_HOME"));
assert!(!is_sensitive_env_name("CARGO_TARGET_DIR"));
assert!(!is_sensitive_env_name("SHELL"));
}
#[test]
fn matches_case_insensitively() {
assert!(is_sensitive_env_name("anthropic_api_key"));
assert!(is_sensitive_env_name("github_token"));
assert!(!is_sensitive_env_name("path"));
}
}