use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::ffi::OsString;
pub const INHERITED_ENV: &[&str] = &[
"PATH",
"PATHEXT",
"SYSTEMROOT",
"WINDIR",
"ComSpec",
"TMPDIR",
"TMP",
"TEMP",
"LANG",
"LC_ALL",
"LC_CTYPE",
"HOME",
"USERPROFILE",
"USER",
"LOGNAME",
"SHELL",
"XDG_RUNTIME_DIR",
"TERM_PROGRAM",
];
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum EnvValue {
Literal(String),
Source(EnvSource),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum EnvSource {
HostEnv { host_env: String },
Secret { secret: String },
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub enum EnvPolicy {
Sanitized {
#[serde(default)]
inherit: Vec<String>,
#[serde(default)]
set: BTreeMap<String, EnvValue>,
},
InheritAll {
#[serde(default)]
set: BTreeMap<String, EnvValue>,
},
}
impl Default for EnvPolicy {
fn default() -> Self {
Self::Sanitized {
inherit: Vec::new(),
set: BTreeMap::new(),
}
}
}
#[async_trait::async_trait]
pub trait SecretResolver: Send + Sync {
async fn resolve(&self, reference: &str) -> Option<String>;
}
pub struct NoSecrets;
#[async_trait::async_trait]
impl SecretResolver for NoSecrets {
async fn resolve(&self, _reference: &str) -> Option<String> {
None
}
}
pub async fn resolve(
policy: &EnvPolicy,
extra: &BTreeMap<String, String>,
secrets: &dyn SecretResolver,
) -> crate::Result<BTreeMap<String, OsString>> {
let mut resolved: BTreeMap<String, OsString> = BTreeMap::new();
match policy {
EnvPolicy::Sanitized { inherit, set } => {
for key in INHERITED_ENV
.iter()
.copied()
.chain(inherit.iter().map(String::as_str))
{
if let Some(value) = std::env::var_os(key).filter(|v| !v.is_empty()) {
resolved.insert(key.to_owned(), value);
}
}
resolved.insert("TERM".into(), OsString::from("dumb"));
resolved.insert("NO_COLOR".into(), OsString::from("1"));
apply_set(&mut resolved, set, secrets).await?;
}
EnvPolicy::InheritAll { set } => {
for (key, value) in std::env::vars_os() {
resolved.insert(key.into_string().unwrap_or_default(), value);
}
apply_set(&mut resolved, set, secrets).await?;
}
}
for (key, value) in extra {
resolved.insert(key.clone(), OsString::from(value));
}
Ok(resolved)
}
async fn apply_set(
resolved: &mut BTreeMap<String, OsString>,
set: &BTreeMap<String, EnvValue>,
secrets: &dyn SecretResolver,
) -> crate::Result<()> {
for (name, value) in set {
let value = match value {
EnvValue::Literal(v) => Some(v.clone()),
EnvValue::Source(EnvSource::HostEnv { host_env }) => std::env::var(host_env).ok(),
EnvValue::Source(EnvSource::Secret { secret }) => secrets.resolve(secret).await,
}
.ok_or_else(|| {
crate::error::AdapterError::permanent("agent_env_source_unavailable", "env/resolve")
.with_detail(format!("environment entry `{name}` could not be resolved"))
})?;
resolved.insert(name.clone(), OsString::from(value));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct Resolver(Option<String>);
#[async_trait::async_trait]
impl SecretResolver for Resolver {
async fn resolve(&self, _reference: &str) -> Option<String> {
self.0.clone()
}
}
fn spec_env(key: &str, value: &str) {
unsafe { std::env::set_var(key, value) };
}
#[tokio::test]
async fn sanitized_never_inherits_unlisted_host_variables() {
spec_env("A4AA_TEST_SECRET_MARKER", "leak-check");
let resolved = resolve(&EnvPolicy::default(), &BTreeMap::new(), &NoSecrets)
.await
.unwrap();
assert!(!resolved.contains_key("A4AA_TEST_SECRET_MARKER"));
}
#[tokio::test]
async fn unresolved_secret_fails_closed() {
let mut set = BTreeMap::new();
set.insert(
"AGENT_TOKEN".to_owned(),
EnvValue::Source(EnvSource::Secret {
secret: "missing-ref".into(),
}),
);
let policy = EnvPolicy::Sanitized {
inherit: Vec::new(),
set,
};
let error = resolve(&policy, &BTreeMap::new(), &NoSecrets)
.await
.unwrap_err();
assert_eq!(error.code, "agent_env_source_unavailable");
}
#[tokio::test]
async fn resolved_secret_is_injected() {
let mut set = BTreeMap::new();
set.insert(
"AGENT_TOKEN".to_owned(),
EnvValue::Source(EnvSource::Secret {
secret: "token-ref".into(),
}),
);
let policy = EnvPolicy::Sanitized {
inherit: Vec::new(),
set,
};
let resolved = resolve(&policy, &BTreeMap::new(), &Resolver(Some("s3cr3t".into())))
.await
.unwrap();
assert_eq!(resolved.get("AGENT_TOKEN").unwrap(), "s3cr3t");
}
#[tokio::test]
async fn inherit_all_is_explicit_and_observable() {
spec_env("A4AA_TEST_INHERIT_MARKER", "visible");
let policy = EnvPolicy::InheritAll {
set: BTreeMap::new(),
};
let resolved = resolve(&policy, &BTreeMap::new(), &NoSecrets)
.await
.unwrap();
assert_eq!(resolved.get("A4AA_TEST_INHERIT_MARKER").unwrap(), "visible");
}
}