From 2899860531a7ac66e1d207065bf55b1de2bd3a41 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 2 May 2026 08:55:12 +0000 Subject: [PATCH] feat: add [agent].inherit_env to selectively inherit env vars Adds an optional inherit_env config that allows specific env vars from the OAB process to be passed through to the agent subprocess. This supports Kubernetes envFrom workflows where env vars are injected into the pod but need to reach the agent. - env_clear() security default unchanged - [agent].env explicit values take precedence over inherited ones - Inherited keys logged in the existing security warning - Helm chart template renders inherit_env as TOML array - config.toml.example updated with documentation Closes #699 --- charts/openab/templates/configmap.yaml | 3 + charts/openab/tests/configmap_test.yaml | 18 +++++- config.toml.example | 8 +++ docs/config-reference.md | 3 + src/acp/connection.rs | 85 +++++++++++++++++++++++-- src/acp/pool.rs | 1 + src/config.rs | 2 + 7 files changed, 114 insertions(+), 6 deletions(-) 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)]