Skip to content

Commit 5827122

Browse files
author
forge-admin
committed
security(spawner): redact sensitive env_vars in AgentConfig Debug Refs #1719
1 parent cf83553 commit 5827122

1 file changed

Lines changed: 128 additions & 1 deletion

File tree

crates/terraphim_spawner/src/config.rs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct ResourceLimits {
2121
}
2222

2323
/// Configuration for an agent
24-
#[derive(Debug, Clone)]
24+
#[derive(Clone)]
2525
pub struct AgentConfig {
2626
/// Agent identifier
2727
pub agent_id: String,
@@ -44,6 +44,43 @@ pub struct AgentConfig {
4444
pub supports_stdin: bool,
4545
}
4646

47+
/// Returns `true` if an env-var key name indicates a sensitive credential.
48+
fn is_sensitive_key(key: &str) -> bool {
49+
let upper = key.to_uppercase();
50+
upper.contains("TOKEN")
51+
|| upper.contains("KEY")
52+
|| upper.contains("SECRET")
53+
|| upper.contains("PASSWORD")
54+
}
55+
56+
impl std::fmt::Debug for AgentConfig {
57+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58+
let redacted: std::collections::HashMap<&str, &str> = self
59+
.env_vars
60+
.iter()
61+
.map(|(k, v)| {
62+
let val: &str = if is_sensitive_key(k) {
63+
"***REDACTED***"
64+
} else {
65+
v.as_str()
66+
};
67+
(k.as_str(), val)
68+
})
69+
.collect();
70+
f.debug_struct("AgentConfig")
71+
.field("agent_id", &self.agent_id)
72+
.field("cli_command", &self.cli_command)
73+
.field("args", &self.args)
74+
.field("working_dir", &self.working_dir)
75+
.field("env_vars", &redacted)
76+
.field("required_api_keys", &self.required_api_keys)
77+
.field("resource_limits", &self.resource_limits)
78+
.field("use_stdin", &self.use_stdin)
79+
.field("supports_stdin", &self.supports_stdin)
80+
.finish()
81+
}
82+
}
83+
4784
impl AgentConfig {
4885
/// Create agent config from a provider
4986
pub fn from_provider(provider: &Provider) -> Result<Self, ValidationError> {
@@ -562,6 +599,78 @@ mod tests {
562599
);
563600
}
564601

602+
#[test]
603+
fn test_is_sensitive_key_detects_known_patterns() {
604+
assert!(is_sensitive_key("ANTHROPIC_API_KEY"));
605+
assert!(is_sensitive_key("OPENAI_API_KEY"));
606+
assert!(is_sensitive_key("GITHUB_TOKEN"));
607+
assert!(is_sensitive_key("DB_PASSWORD"));
608+
assert!(is_sensitive_key("APP_SECRET"));
609+
assert!(is_sensitive_key("api_key"));
610+
assert!(is_sensitive_key("auth_token"));
611+
}
612+
613+
#[test]
614+
fn test_is_sensitive_key_allows_safe_keys() {
615+
assert!(!is_sensitive_key("HOME"));
616+
assert!(!is_sensitive_key("PATH"));
617+
assert!(!is_sensitive_key("LOG_LEVEL"));
618+
assert!(!is_sensitive_key("RUST_LOG"));
619+
assert!(!is_sensitive_key("AGENT_ID"));
620+
}
621+
622+
#[test]
623+
fn test_debug_redacts_sensitive_env_vars() {
624+
let mut env_vars = HashMap::new();
625+
env_vars.insert(
626+
"ANTHROPIC_API_KEY".to_string(),
627+
"sk-ant-real-secret".to_string(),
628+
);
629+
env_vars.insert("GITHUB_TOKEN".to_string(), "ghp_real_token".to_string());
630+
env_vars.insert("LOG_LEVEL".to_string(), "debug".to_string());
631+
env_vars.insert("HOME".to_string(), "/home/agent".to_string());
632+
633+
let config = AgentConfig {
634+
agent_id: "test-agent".to_string(),
635+
cli_command: "claude".to_string(),
636+
args: vec![],
637+
working_dir: None,
638+
env_vars,
639+
required_api_keys: vec![],
640+
resource_limits: ResourceLimits::default(),
641+
use_stdin: false,
642+
supports_stdin: true,
643+
};
644+
645+
let debug_output = format!("{:?}", config);
646+
647+
// Sensitive values must not appear
648+
assert!(
649+
!debug_output.contains("sk-ant-real-secret"),
650+
"API key value must be redacted"
651+
);
652+
assert!(
653+
!debug_output.contains("ghp_real_token"),
654+
"Token value must be redacted"
655+
);
656+
657+
// Redaction marker must appear
658+
assert!(
659+
debug_output.contains("***REDACTED***"),
660+
"Redaction marker must appear for sensitive keys"
661+
);
662+
663+
// Non-sensitive values must be visible
664+
assert!(
665+
debug_output.contains("debug"),
666+
"Non-sensitive env var value must be visible"
667+
);
668+
assert!(
669+
debug_output.contains("/home/agent"),
670+
"Non-sensitive env var value must be visible"
671+
);
672+
}
673+
565674
#[test]
566675
fn test_model_args_pi_rust_bare() {
567676
let args = AgentConfig::model_args("pi-rust", "glm-5.1");
@@ -594,4 +703,22 @@ mod tests {
594703
let keys = AgentConfig::infer_api_keys("/home/alex/.local/bin/pi-rust");
595704
assert!(keys.is_empty());
596705
}
706+
707+
#[test]
708+
fn test_debug_empty_env_vars_shows_nothing_redacted() {
709+
let config = AgentConfig {
710+
agent_id: "test".to_string(),
711+
cli_command: "claude".to_string(),
712+
args: vec![],
713+
working_dir: None,
714+
env_vars: HashMap::new(),
715+
required_api_keys: vec![],
716+
resource_limits: ResourceLimits::default(),
717+
use_stdin: false,
718+
supports_stdin: true,
719+
};
720+
let debug_output = format!("{:?}", config);
721+
assert!(!debug_output.contains("***REDACTED***"));
722+
assert!(debug_output.contains("AgentConfig"));
723+
}
597724
}

0 commit comments

Comments
 (0)