From 88b0b56a6dabab5b3eb0697699b2f410942ab2db Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 1 May 2026 02:33:50 +0000 Subject: [PATCH 1/9] fix: env_clear() to prevent credential leakage to agent subprocess Agent subprocess inherited all OAB env vars (including DISCORD_BOT_TOKEN, SLACK_BOT_TOKEN, etc.) because spawn() never called env_clear(). Now the child process starts with a clean environment and only receives: - HOME and PATH as baseline - Explicit [agent].env entries from config Fixes #669 --- src/acp/connection.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 9c556a86..129c117b 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -154,6 +154,11 @@ impl AcpConnection { { cmd.creation_flags(0x00000200); // CREATE_NEW_PROCESS_GROUP } + // Clear inherited env to prevent credential leakage (e.g. DISCORD_BOT_TOKEN). + // Only [agent].env values + essential baseline vars are passed through. + cmd.env_clear(); + cmd.env("HOME", working_dir); + cmd.env("PATH", std::env::var("PATH").unwrap_or_default()); for (k, v) in env { cmd.env(k, expand_env(v)); } From 276cecbe3c496925ff18bc012f460d89baf21feb Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 1 May 2026 02:41:59 +0000 Subject: [PATCH 2/9] =?UTF-8?q?remove=20[agent].env=20=E2=80=94=20OAB=20on?= =?UTF-8?q?ly=20passes=20HOME=20and=20PATH=20to=20coding=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All supported backends use OAuth/file-based auth and do not need env var injection. Removing [agent].env eliminates the risk of accidentally passing sensitive credentials to the agent subprocess. --- Cargo.lock | 19 +------------------ config.toml.example | 8 -------- src/acp/connection.rs | 14 +------------- src/acp/pool.rs | 1 - src/config.rs | 3 --- 5 files changed, 2 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 876430a9..de9b066a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,15 +507,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1055,7 +1046,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openab" -version = "0.8.1" +version = "0.8.2" dependencies = [ "anyhow", "async-trait", @@ -1243,18 +1234,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ "bitflags", - "getopts", "memchr", - "pulldown-cmark-escape", "unicase", ] -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "pxfm" version = "0.1.28" diff --git a/config.toml.example b/config.toml.example index 4c4a37fc..daa7b9c1 100644 --- a/config.toml.example +++ b/config.toml.example @@ -54,34 +54,26 @@ working_dir = "/home/agent" # command = "claude" # args = ["--acp"] # working_dir = "/home/agent" -# env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } # [agent] # command = "codex" # args = ["--acp"] # working_dir = "/home/agent" -# env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } # [agent] # command = "gemini" # args = ["--acp"] # working_dir = "/home/agent" -# env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } # [agent] # command = "copilot" # args = ["--acp", "--stdio"] # working_dir = "/home/agent" -# env = {} # Auth via: kubectl exec -it -- gh auth login -p https -w # [agent] # command = "opencode" # args = ["acp"] # working_dir = "/home/node" -# # Note: opencode handles tool authorization internally and never emits -# # session/request_permission — all tools run without user confirmation, -# # equivalent to --trust-all-tools on other backends. -# # Run `opencode auth login` once before starting openab. # [agent] # command = "cursor-agent" diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 129c117b..e4e2231a 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -73,14 +73,6 @@ fn build_permission_response(params: Option<&Value>) -> Value { } } -fn expand_env(val: &str) -> String { - if val.starts_with("${") && val.ends_with('}') { - let key = &val[2..val.len() - 1]; - std::env::var(key).unwrap_or_default() - } else { - val.to_string() - } -} use tokio::time::Instant; /// A content block for the ACP prompt — either text or image. @@ -127,7 +119,6 @@ impl AcpConnection { command: &str, args: &[String], working_dir: &str, - env: &std::collections::HashMap, ) -> Result { info!(cmd = command, ?args, cwd = working_dir, "spawning agent"); @@ -155,13 +146,10 @@ impl AcpConnection { cmd.creation_flags(0x00000200); // CREATE_NEW_PROCESS_GROUP } // Clear inherited env to prevent credential leakage (e.g. DISCORD_BOT_TOKEN). - // Only [agent].env values + essential baseline vars are passed through. + // Only HOME and PATH are passed through. All backends use OAuth/file-based auth. cmd.env_clear(); cmd.env("HOME", working_dir); cmd.env("PATH", std::env::var("PATH").unwrap_or_default()); - for (k, v) in env { - cmd.env(k, expand_env(v)); - } let mut proc = cmd .spawn() .map_err(|e| anyhow!("failed to spawn {command}: {e}"))?; diff --git a/src/acp/pool.rs b/src/acp/pool.rs index c64bd88b..e77b5a89 100644 --- a/src/acp/pool.rs +++ b/src/acp/pool.rs @@ -136,7 +136,6 @@ impl SessionPool { &self.config.command, &self.config.args, &self.config.working_dir, - &self.config.env, ) .await?; diff --git a/src/config.rs b/src/config.rs index 79011110..5f0c930f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,6 @@ use crate::markdown::TableMode; use regex::Regex; use serde::Deserialize; -use std::collections::HashMap; use std::path::Path; /// Controls whether the bot processes messages from other Discord bots. @@ -215,8 +214,6 @@ pub struct AgentConfig { pub args: Vec, #[serde(default = "default_working_dir")] pub working_dir: String, - #[serde(default)] - pub env: HashMap, } #[derive(Debug, Deserialize)] From adac377190ade1e2c28611dffaf311466862c046 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 1 May 2026 02:45:31 +0000 Subject: [PATCH 3/9] =?UTF-8?q?Revert=20"remove=20[agent].env=20=E2=80=94?= =?UTF-8?q?=20OAB=20only=20passes=20HOME=20and=20PATH=20to=20coding=20CLI"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 276cecbe3c496925ff18bc012f460d89baf21feb. --- Cargo.lock | 19 ++++++++++++++++++- config.toml.example | 8 ++++++++ src/acp/connection.rs | 14 +++++++++++++- src/acp/pool.rs | 1 + src/config.rs | 3 +++ 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de9b066a..876430a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1046,7 +1055,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openab" -version = "0.8.2" +version = "0.8.1" dependencies = [ "anyhow", "async-trait", @@ -1234,10 +1243,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ "bitflags", + "getopts", "memchr", + "pulldown-cmark-escape", "unicase", ] +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "pxfm" version = "0.1.28" diff --git a/config.toml.example b/config.toml.example index daa7b9c1..4c4a37fc 100644 --- a/config.toml.example +++ b/config.toml.example @@ -54,26 +54,34 @@ working_dir = "/home/agent" # command = "claude" # args = ["--acp"] # working_dir = "/home/agent" +# env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } # [agent] # command = "codex" # args = ["--acp"] # working_dir = "/home/agent" +# env = { OPENAI_API_KEY = "${OPENAI_API_KEY}" } # [agent] # command = "gemini" # args = ["--acp"] # working_dir = "/home/agent" +# env = { GEMINI_API_KEY = "${GEMINI_API_KEY}" } # [agent] # command = "copilot" # args = ["--acp", "--stdio"] # working_dir = "/home/agent" +# env = {} # Auth via: kubectl exec -it -- gh auth login -p https -w # [agent] # command = "opencode" # args = ["acp"] # working_dir = "/home/node" +# # Note: opencode handles tool authorization internally and never emits +# # session/request_permission — all tools run without user confirmation, +# # equivalent to --trust-all-tools on other backends. +# # Run `opencode auth login` once before starting openab. # [agent] # command = "cursor-agent" diff --git a/src/acp/connection.rs b/src/acp/connection.rs index e4e2231a..129c117b 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -73,6 +73,14 @@ fn build_permission_response(params: Option<&Value>) -> Value { } } +fn expand_env(val: &str) -> String { + if val.starts_with("${") && val.ends_with('}') { + let key = &val[2..val.len() - 1]; + std::env::var(key).unwrap_or_default() + } else { + val.to_string() + } +} use tokio::time::Instant; /// A content block for the ACP prompt — either text or image. @@ -119,6 +127,7 @@ impl AcpConnection { command: &str, args: &[String], working_dir: &str, + env: &std::collections::HashMap, ) -> Result { info!(cmd = command, ?args, cwd = working_dir, "spawning agent"); @@ -146,10 +155,13 @@ impl AcpConnection { cmd.creation_flags(0x00000200); // CREATE_NEW_PROCESS_GROUP } // Clear inherited env to prevent credential leakage (e.g. DISCORD_BOT_TOKEN). - // Only HOME and PATH are passed through. All backends use OAuth/file-based auth. + // Only [agent].env values + essential baseline vars are passed through. cmd.env_clear(); cmd.env("HOME", working_dir); cmd.env("PATH", std::env::var("PATH").unwrap_or_default()); + for (k, v) in env { + cmd.env(k, expand_env(v)); + } let mut proc = cmd .spawn() .map_err(|e| anyhow!("failed to spawn {command}: {e}"))?; diff --git a/src/acp/pool.rs b/src/acp/pool.rs index e77b5a89..c64bd88b 100644 --- a/src/acp/pool.rs +++ b/src/acp/pool.rs @@ -136,6 +136,7 @@ impl SessionPool { &self.config.command, &self.config.args, &self.config.working_dir, + &self.config.env, ) .await?; diff --git a/src/config.rs b/src/config.rs index 5f0c930f..79011110 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::markdown::TableMode; use regex::Regex; use serde::Deserialize; +use std::collections::HashMap; use std::path::Path; /// Controls whether the bot processes messages from other Discord bots. @@ -214,6 +215,8 @@ pub struct AgentConfig { pub args: Vec, #[serde(default = "default_working_dir")] pub working_dir: String, + #[serde(default)] + pub env: HashMap, } #[derive(Debug, Deserialize)] From 7913f79171677704f71ff3157a691e3f79a11f6c Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 1 May 2026 02:47:40 +0000 Subject: [PATCH 4/9] =?UTF-8?q?warn=20when=20[agent].env=20is=20configured?= =?UTF-8?q?=20=E2=80=94=20values=20are=20exfiltration=20risk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log a warning at startup listing the env var keys being passed to the agent. Also add a security warning in config.toml.example advising users to prefer OAuth login over env var API keys. --- config.toml.example | 3 +++ src/acp/connection.rs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/config.toml.example b/config.toml.example index 4c4a37fc..7b7a6776 100644 --- a/config.toml.example +++ b/config.toml.example @@ -54,6 +54,9 @@ working_dir = "/home/agent" # command = "claude" # args = ["--acp"] # working_dir = "/home/agent" +# ⚠️ SECURITY WARNING: Any env var listed here is accessible to the agent. +# A user could trick the agent into leaking these values via prompt injection. +# All supported backends support OAuth login — prefer that over env var API keys. # env = { ANTHROPIC_API_KEY = "${ANTHROPIC_API_KEY}" } # [agent] diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 129c117b..ce1d3e48 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -162,6 +162,13 @@ impl AcpConnection { for (k, v) in env { cmd.env(k, expand_env(v)); } + if !env.is_empty() { + let 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" + ); + } let mut proc = cmd .spawn() .map_err(|e| anyhow!("failed to spawn {command}: {e}"))?; From 71d9ab8a2094edaf6969725881f672d346756984 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 1 May 2026 02:55:07 +0000 Subject: [PATCH 5/9] add security warning to helm NOTES.txt about [agent].env risk --- charts/openab/templates/NOTES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index b18488d7..82213b5c 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -48,3 +48,7 @@ Agents deployed: kubectl rollout restart deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} {{- end }} {{- end }} + +⚠️ SECURITY: The agent subprocess only receives HOME, PATH, and explicitly configured [agent].env vars. + Any env var passed via [agent].env is accessible to the agent and could be exfiltrated via prompt injection. + All supported backends use OAuth/file-based auth — avoid passing API keys via env vars when possible. From 9234064136d6cf8752cd3974702493a3fcce0342 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Fri, 1 May 2026 03:03:03 +0000 Subject: [PATCH 6/9] fix: add USER env var, PATH fallback, ASCII-only log message - Add USER baseline env var (many Unix tools and git need it) - PATH falls back to /usr/local/bin:/usr/bin:/bin instead of empty string - Remove emoji from tracing::warn! for log aggregation compatibility --- src/acp/connection.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/acp/connection.rs b/src/acp/connection.rs index ce1d3e48..1deb3eeb 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -158,7 +158,8 @@ impl AcpConnection { // Only [agent].env values + essential baseline vars are passed through. cmd.env_clear(); cmd.env("HOME", working_dir); - cmd.env("PATH", std::env::var("PATH").unwrap_or_default()); + cmd.env("USER", std::env::var("USER").unwrap_or_else(|_| "agent".into())); + cmd.env("PATH", std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".into())); for (k, v) in env { cmd.env(k, expand_env(v)); } @@ -166,7 +167,7 @@ impl AcpConnection { let 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" + "[agent].env is set -- these values are accessible to the agent and could be exfiltrated via prompt injection" ); } let mut proc = cmd From a6ceeee30ca3e81dcc5693868bacb801a02223c8 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Fri, 1 May 2026 03:04:42 +0000 Subject: [PATCH 7/9] docs: clarify HOME=working_dir intent, note env override behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 普渡法師 --- config.toml.example | 1 + src/acp/connection.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/config.toml.example b/config.toml.example index 7b7a6776..a9ad9271 100644 --- a/config.toml.example +++ b/config.toml.example @@ -57,6 +57,7 @@ working_dir = "/home/agent" # ⚠️ SECURITY WARNING: Any env var listed here is accessible to the agent. # A user could trick the agent into leaking these values via prompt injection. # 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}" } # [agent] diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 1deb3eeb..e8be10e3 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -157,6 +157,8 @@ impl AcpConnection { // Clear inherited env to prevent credential leakage (e.g. DISCORD_BOT_TOKEN). // Only [agent].env values + essential baseline vars are passed through. cmd.env_clear(); + // HOME is intentionally set to working_dir (not the host user's home) to + // isolate the agent's filesystem scope. [agent].env can override if needed. cmd.env("HOME", working_dir); cmd.env("USER", std::env::var("USER").unwrap_or_else(|_| "agent".into())); cmd.env("PATH", std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".into())); From de41c50bb238cd9b7cac95167c74546f2a343e55 Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Fri, 1 May 2026 03:06:02 +0000 Subject: [PATCH 8/9] fix: add Windows baseline env vars (SystemRoot, USERPROFILE, etc.) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit env_clear() on Windows removes SystemRoot which breaks DLL loading. Add platform-specific baseline vars: - Unix: USER - Windows: USERPROFILE, USERNAME, SystemRoot, SystemDrive Co-authored-by: 覺渡法師 --- src/acp/connection.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/acp/connection.rs b/src/acp/connection.rs index e8be10e3..5d1ed5c4 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -160,8 +160,20 @@ impl AcpConnection { // HOME is intentionally set to working_dir (not the host user's home) to // isolate the agent's filesystem scope. [agent].env can override if needed. cmd.env("HOME", working_dir); - cmd.env("USER", std::env::var("USER").unwrap_or_else(|_| "agent".into())); cmd.env("PATH", std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".into())); + #[cfg(unix)] + { + cmd.env("USER", std::env::var("USER").unwrap_or_else(|_| "agent".into())); + } + #[cfg(windows)] + { + // Windows requires SystemRoot for DLL loading and basic OS functionality. + // USERPROFILE is the Windows equivalent of HOME. + cmd.env("USERPROFILE", working_dir); + cmd.env("USERNAME", std::env::var("USERNAME").unwrap_or_else(|_| "agent".into())); + if let Ok(v) = std::env::var("SystemRoot") { cmd.env("SystemRoot", v); } + if let Ok(v) = std::env::var("SystemDrive") { cmd.env("SystemDrive", v); } + } for (k, v) in env { cmd.env(k, expand_env(v)); } From 51bc2560b3de532509cdabc701ebef75a7329d5b Mon Sep 17 00:00:00 2001 From: chaodufashi Date: Fri, 1 May 2026 03:07:30 +0000 Subject: [PATCH 9/9] fix: preserve real HOME/USERPROFILE instead of working_dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit working_dir defaults to /tmp and is the agent's cwd, not its home. Setting HOME=working_dir breaks OAuth auth file lookup (~/.codex, ~/.claude, ~/.config/gh). Preserve the real HOME from the parent process, falling back to working_dir only if HOME is unset. Same fix for USERPROFILE on Windows. Co-authored-by: 擺渡法師 --- src/acp/connection.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/acp/connection.rs b/src/acp/connection.rs index 5d1ed5c4..aebe8525 100644 --- a/src/acp/connection.rs +++ b/src/acp/connection.rs @@ -157,9 +157,10 @@ impl AcpConnection { // Clear inherited env to prevent credential leakage (e.g. DISCORD_BOT_TOKEN). // Only [agent].env values + essential baseline vars are passed through. cmd.env_clear(); - // HOME is intentionally set to working_dir (not the host user's home) to - // isolate the agent's filesystem scope. [agent].env can override if needed. - cmd.env("HOME", working_dir); + // Preserve the real HOME so agents can find OAuth/auth files (~/.codex, + // ~/.claude, ~/.config/gh, etc.). working_dir is already set via + // current_dir() above and is not necessarily the user's home directory. + cmd.env("HOME", std::env::var("HOME").unwrap_or_else(|_| working_dir.into())); cmd.env("PATH", std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".into())); #[cfg(unix)] { @@ -169,7 +170,7 @@ impl AcpConnection { { // Windows requires SystemRoot for DLL loading and basic OS functionality. // USERPROFILE is the Windows equivalent of HOME. - cmd.env("USERPROFILE", working_dir); + cmd.env("USERPROFILE", std::env::var("USERPROFILE").unwrap_or_else(|_| working_dir.into())); cmd.env("USERNAME", std::env::var("USERNAME").unwrap_or_else(|_| "agent".into())); if let Ok(v) = std::env::var("SystemRoot") { cmd.env("SystemRoot", v); } if let Ok(v) = std::env::var("SystemDrive") { cmd.env("SystemDrive", v); }