diff --git a/charts/openab/templates/configmap.yaml b/charts/openab/templates/configmap.yaml index 43e895e3..78316b59 100644 --- a/charts/openab/templates/configmap.yaml +++ b/charts/openab/templates/configmap.yaml @@ -106,6 +106,9 @@ data: {{- if $cfg.env }} env = { {{ $first := true }}{{ range $k, $v := $cfg.env }}{{ if not $first }}, {{ end }}{{ $k }} = {{ $v | toJson }}{{ $first = false }}{{ end }} } {{- end }} + {{- if $cfg.inheritEnv }} + inherit_env = {{ $cfg.inheritEnv | toJson }} + {{- end }} [pool] max_sessions = {{ ($cfg.pool).maxSessions | default 10 }} diff --git a/charts/openab/tests/configmap_test.yaml b/charts/openab/tests/configmap_test.yaml index 223ea0b1..31002809 100644 --- a/charts/openab/tests/configmap_test.yaml +++ b/charts/openab/tests/configmap_test.yaml @@ -141,4 +141,20 @@ tests: asserts: - matchRegex: path: data["config.toml"] - pattern: 'max_bot_turns = 30' \ No newline at end of file + pattern: 'max_bot_turns = 30' + + - it: renders inherit_env as TOML array + set: + agents.kiro.inheritEnv: + - "API_BASE_URL" + - "MODEL_NAME" + asserts: + - matchRegex: + path: data["config.toml"] + pattern: 'inherit_env = \["API_BASE_URL","MODEL_NAME"\]' + + - it: does not render inherit_env when unset + asserts: + - notMatchRegex: + path: data["config.toml"] + pattern: 'inherit_env' diff --git a/config.toml.example b/config.toml.example index 277bdcf1..8ba46af2 100644 --- a/config.toml.example +++ b/config.toml.example @@ -59,6 +59,14 @@ working_dir = "/home/agent" # All supported backends support OAuth login — prefer that over env var API keys. # Note: env vars here can override baseline vars (HOME, PATH, USER) if needed. # env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } +# +# By default, the agent subprocess only inherits these baseline vars: +# Linux/macOS: HOME, PATH, USER +# Windows: USERPROFILE, USERNAME, PATH, SystemRoot, SystemDrive +# +# To pass additional env vars from the OAB process (e.g. vars injected via K8s envFrom), +# list them in inherit_env. Keys in [agent].env take precedence over inherited ones. +# inherit_env = ["API_BASE_URL", "MODEL_NAME"] # [agent] # command = "codex" diff --git a/docs/config-reference.md b/docs/config-reference.md index 96e90e48..2dd5bd1f 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -90,6 +90,9 @@ The AI agent subprocess that OpenAB spawns to handle messages via ACP. | `args` | string[] | `[]` | CLI arguments passed to the agent. | | `working_dir` | string | `"/tmp"` | Working directory for the agent process. | | `env` | map | `{}` | Extra environment variables (e.g. `{ ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" }`). | +| `inherit_env` | string[] | `[]` | Env var names to inherit from the OAB process (e.g. vars injected via K8s `envFrom`). Keys in `env` take precedence. | + +> **Default inherited vars:** After `env_clear()`, the agent always receives `HOME`, `PATH`, and `USER` (on Windows: `USERPROFILE`, `USERNAME`, `PATH`, `SystemRoot`, `SystemDrive`). Use `inherit_env` to pass additional vars beyond this baseline. ### Agent examples diff --git a/src/acp/connection.rs b/src/acp/connection.rs index aebe8525..f49c0f50 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -122,12 +122,39 @@ pub struct AcpConnection { _reader_handle: JoinHandle<()>, } +/// Build the final set of env vars for the agent subprocess. +/// `explicit` ([agent].env) takes precedence over `inherit` ([agent].inherit_env). +/// Returns (merged env map, list of keys that were inherited from the process). +fn build_agent_env( + explicit: &std::collections::HashMap, + inherit_keys: &[String], +) -> (std::collections::HashMap, Vec) { + let mut result: std::collections::HashMap = std::collections::HashMap::new(); + let mut inherited: Vec = Vec::new(); + + for (k, v) in explicit { + result.insert(k.clone(), expand_env(v)); + } + + for key in inherit_keys { + if !result.contains_key(key) { + if let Ok(v) = std::env::var(key) { + result.insert(key.clone(), v); + inherited.push(key.clone()); + } + } + } + + (result, inherited) +} + impl AcpConnection { pub async fn spawn( command: &str, args: &[String], working_dir: &str, env: &std::collections::HashMap, + inherit_env: &[String], ) -> Result { info!(cmd = command, ?args, cwd = working_dir, "spawning agent"); @@ -178,11 +205,19 @@ impl AcpConnection { for (k, v) in env { cmd.env(k, expand_env(v)); } - if !env.is_empty() { - let keys: Vec<&String> = env.keys().collect(); + // Inherit selected env vars from the OAB process (e.g. vars injected + // via Kubernetes envFrom). Keys already in [agent].env are skipped — + // explicit values take precedence. + let (agent_env, inherited_keys) = build_agent_env(env, inherit_env); + for (k, v) in &agent_env { + cmd.env(k, v); + } + if !agent_env.is_empty() { + let explicit_keys: Vec<&String> = env.keys().collect(); tracing::warn!( - ?keys, - "[agent].env is set -- these values are accessible to the agent and could be exfiltrated via prompt injection" + ?explicit_keys, + ?inherited_keys, + "[agent].env/inherit_env is set -- these values are accessible to the agent and could be exfiltrated via prompt injection" ); } let mut proc = cmd @@ -559,7 +594,7 @@ impl Drop for AcpConnection { #[cfg(test)] mod tests { - use super::{build_permission_response, pick_best_option}; + use super::{build_agent_env, build_permission_response, pick_best_option}; use serde_json::json; #[test] @@ -652,4 +687,44 @@ mod tests { json!({"outcome": {"outcome": "selected", "optionId": "allow_always"}}) ); } + + #[test] + fn explicit_env_takes_precedence_over_inherit_env() { + let key = "OAB_TEST_PRECEDENCE"; + std::env::set_var(key, "from_process"); + let mut explicit = std::collections::HashMap::new(); + explicit.insert(key.to_string(), "from_config".to_string()); + let inherit = vec![key.to_string()]; + + let (result, inherited) = build_agent_env(&explicit, &inherit); + + assert_eq!(result.get(key).unwrap(), "from_config"); + assert!(!inherited.contains(&key.to_string())); + std::env::remove_var(key); + } + + #[test] + fn inherit_env_copies_from_process() { + let key = "OAB_TEST_INHERIT"; + std::env::set_var(key, "process_value"); + let explicit = std::collections::HashMap::new(); + let inherit = vec![key.to_string()]; + + let (result, inherited) = build_agent_env(&explicit, &inherit); + + assert_eq!(result.get(key).unwrap(), "process_value"); + assert!(inherited.contains(&key.to_string())); + std::env::remove_var(key); + } + + #[test] + fn inherit_env_skips_missing_vars() { + let explicit = std::collections::HashMap::new(); + let inherit = vec!["OAB_TEST_NONEXISTENT_VAR_12345".to_string()]; + + let (result, inherited) = build_agent_env(&explicit, &inherit); + + assert!(!result.contains_key("OAB_TEST_NONEXISTENT_VAR_12345")); + assert!(inherited.is_empty()); + } } diff --git a/src/acp/pool.rs b/src/acp/pool.rs index bafa186a..a146abb0 100644 --- a/src/acp/pool.rs +++ b/src/acp/pool.rs @@ -171,6 +171,7 @@ impl SessionPool { &self.config.args, &self.config.working_dir, &self.config.env, + &self.config.inherit_env, ) .await?; diff --git a/src/config.rs b/src/config.rs index 79011110..50f37804 100644 --- a/src/config.rs +++ b/src/config.rs @@ -217,6 +217,8 @@ pub struct AgentConfig { pub working_dir: String, #[serde(default)] pub env: HashMap, + #[serde(default)] + pub inherit_env: Vec, } #[derive(Debug, Deserialize)]