Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions charts/openab/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
18 changes: 17 additions & 1 deletion charts/openab/tests/configmap_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,20 @@ tests:
asserts:
- matchRegex:
path: data["config.toml"]
pattern: 'max_bot_turns = 30'
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'
8 changes: 8 additions & 0 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
85 changes: 80 additions & 5 deletions src/acp/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String>,
inherit_keys: &[String],
) -> (std::collections::HashMap<String, String>, Vec<String>) {
let mut result: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut inherited: Vec<String> = 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<String, String>,
inherit_env: &[String],
) -> Result<Self> {
info!(cmd = command, ?args, cwd = working_dir, "spawning agent");

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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());
}
}
1 change: 1 addition & 0 deletions src/acp/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ impl SessionPool {
&self.config.args,
&self.config.working_dir,
&self.config.env,
&self.config.inherit_env,
)
.await?;

Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ pub struct AgentConfig {
pub working_dir: String,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub inherit_env: Vec<String>,
}

#[derive(Debug, Deserialize)]
Expand Down
Loading