use std::collections::BTreeSet;
use serde_json::Value;
use crate::installation::generation::{GENERATION_FILE_ENV, GENERATION_TOKEN_ENV};
const BASE_MCP_ENV_VARS: &[&str] = &[
"ALL_PROXY",
"ANTHROPIC_API_KEY",
"APPDATA",
"AWS_ACCESS_KEY_ID",
"AWS_ALLOW_HTTP",
"AWS_CA_BUNDLE",
"AWS_CONFIG_FILE",
"AWS_CONTAINER_AUTHORIZATION_TOKEN",
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_DEFAULT_REGION",
"AWS_EC2_METADATA_DISABLED",
"AWS_ENDPOINT_URL",
"AWS_PROFILE",
"AWS_REGION",
"AWS_ROLE_ARN",
"AWS_ROLE_SESSION_NAME",
"AWS_SDK_LOAD_CONFIG",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_SHARED_CREDENTIALS_FILE",
"AWS_STS_REGIONAL_ENDPOINTS",
"AWS_WEB_IDENTITY_TOKEN_FILE",
"HOME",
"HTTPS_PROXY",
"HTTP_PROXY",
"LOCALAPPDATA",
"NEMO_RELAY_ANTHROPIC_AUTH_HEADER",
"NEMO_RELAY_ANTHROPIC_BASE_URL",
"NEMO_RELAY_GATEWAY_URL",
"NEMO_RELAY_MAX_HOOK_PAYLOAD_BYTES",
"NEMO_RELAY_MAX_PASSTHROUGH_BODY_BYTES",
"NEMO_RELAY_OPENAI_AUTH_HEADER",
"NEMO_RELAY_OPENAI_BASE_URL",
"NEMO_RELAY_PLUGIN_IDLE_TIMEOUT_SECS",
"NEMO_RELAY_PYTHON",
"NEMO_RELAY_TRANSPARENT_RUN",
"NO_PROXY",
"OPENAI_API_KEY",
"OTEL_EXPORTER_OTLP_COMPRESSION",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_TIMEOUT",
"OTEL_EXPORTER_OTLP_TRACES_COMPRESSION",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
"OTEL_EXPORTER_OTLP_TRACES_TIMEOUT",
"OTEL_RESOURCE_ATTRIBUTES",
"OTEL_SDK_DISABLED",
"OTEL_SERVICE_NAME",
"SSL_CERT_DIR",
"SSL_CERT_FILE",
"TEMP",
"TMPDIR",
"USERPROFILE",
"XDG_CONFIG_HOME",
"XDG_RUNTIME_DIR",
"all_proxy",
"http_proxy",
"https_proxy",
"no_proxy",
];
const BLOCKED_MCP_ENV_VARS: &[&str] = &[
"NEMO_RELAY_BINDING_KIND",
"NEMO_RELAY_BOOTSTRAP_AGENT",
"NEMO_RELAY_BOOTSTRAP_FINGERPRINT",
"NEMO_RELAY_BOOTSTRAP_STATE_DIR",
"NEMO_RELAY_BOOTSTRAP_SHUTDOWN_TOKEN",
"NEMO_RELAY_CONFIG_SCOPE",
"NEMO_RELAY_FAIL_CLOSED",
"NEMO_RELAY_GATEWAY_BIND",
"NEMO_RELAY_HOST_SOCKET",
"NEMO_RELAY_MCP_GENERATION",
"NEMO_RELAY_MCP_GENERATION_FILE",
"NEMO_RELAY_NATIVE_ABI_VERSION",
"NEMO_RELAY_PLUGIN_BINARY",
"NEMO_RELAY_PLUGIN_BIND",
"NEMO_RELAY_SIDECAR_JOB_NAME",
"NEMO_RELAY_PLUGIN_CONFIG_PATH",
"NEMO_RELAY_PLUGIN_GATEWAY_URL",
"NEMO_RELAY_PLUGIN_ID",
"NEMO_RELAY_RUNTIME_OWNER",
"NEMO_RELAY_WORKER_ENDPOINT_FILE",
"NEMO_RELAY_WORKER_ID",
"NEMO_RELAY_WORKER_SOCKET",
"NEMO_RELAY_WORKER_TOKEN",
];
pub(crate) fn forwarded_names(
environment: impl IntoIterator<Item = String>,
config: Option<&Value>,
) -> Vec<String> {
forwarded_names_for_platform(environment, config, cfg!(windows))
}
pub(crate) fn forwarded_names_for_platform(
environment: impl IntoIterator<Item = String>,
config: Option<&Value>,
windows: bool,
) -> Vec<String> {
let mut names = BTreeSet::new();
for name in BASE_MCP_ENV_VARS {
insert_name(&mut names, (*name).to_string(), windows);
}
for name in environment {
if prefix_allowed(&name, windows) && !blocked(&name) {
insert_name(&mut names, name, windows);
}
}
if let Some(config) = config {
collect_config_names(config, &mut names, windows);
}
names.into_iter().collect()
}
pub(crate) fn remove_unresolved_mcp_placeholders() {
if std::env::var_os(GENERATION_FILE_ENV).is_none()
|| std::env::var_os(GENERATION_TOKEN_ENV).is_none()
{
return;
}
let unresolved = std::env::vars_os()
.filter_map(|(name, value)| {
let name_text = name.to_str()?;
let value = value.to_str()?;
(!blocked(name_text)
&& unresolved_self_placeholder_for_platform(name_text, value, cfg!(windows)))
.then_some(name)
})
.collect::<Vec<_>>();
for name in unresolved {
unsafe { std::env::remove_var(name) };
}
}
pub(crate) fn forwarded_names_match_for_platform(left: &str, right: &str, windows: bool) -> bool {
if windows {
left.eq_ignore_ascii_case(right)
} else {
left == right
}
}
pub(crate) fn previously_forwardable_name_for_platform(name: &str, windows: bool) -> bool {
!blocked(name)
&& (BASE_MCP_ENV_VARS
.iter()
.any(|base| forwarded_names_match_for_platform(name, base, windows))
|| prefix_allowed(name, windows))
}
pub(crate) fn unresolved_self_placeholder_for_platform(
name: &str,
value: &str,
windows: bool,
) -> bool {
value
.strip_prefix("${")
.and_then(|value| value.strip_suffix('}'))
.is_some_and(|placeholder| forwarded_names_match_for_platform(name, placeholder, windows))
}
fn prefix_allowed(name: &str, windows: bool) -> bool {
["NEMO_RELAY_", "OTEL_", "AWS_"].iter().any(|prefix| {
if windows {
starts_with_ignore_ascii_case(name, prefix)
} else {
name.starts_with(prefix)
}
})
}
fn blocked(name: &str) -> bool {
BLOCKED_MCP_ENV_VARS
.iter()
.any(|blocked| name.eq_ignore_ascii_case(blocked))
|| starts_with_ignore_ascii_case(name, "NEMO_RELAY_TEST_")
}
fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
}
fn insert_name(names: &mut BTreeSet<String>, name: String, windows: bool) {
if !windows
|| !names
.iter()
.any(|existing| existing.eq_ignore_ascii_case(&name))
{
names.insert(name);
}
}
fn collect_config_names(value: &Value, names: &mut BTreeSet<String>, windows: bool) {
match value {
Value::Object(object) => {
for (key, value) in object {
collect_config_field(key, value, names, windows);
}
}
Value::Array(values) => {
for value in values {
collect_config_names(value, names, windows);
}
}
_ => {}
}
}
fn collect_config_field(key: &str, value: &Value, names: &mut BTreeSet<String>, windows: bool) {
match key {
"header_env" => collect_header_env_names(value, names, windows),
"secret_access_key_var" | "session_token_var" => {
if let Some(name) = value.as_str() {
collect_config_name(name, names, windows);
}
}
_ => collect_config_names(value, names, windows),
}
}
fn collect_header_env_names(value: &Value, names: &mut BTreeSet<String>, windows: bool) {
if let Some(headers) = value.as_object() {
for name in headers.values().filter_map(Value::as_str) {
collect_config_name(name, names, windows);
}
}
}
fn collect_config_name(name: &str, names: &mut BTreeSet<String>, windows: bool) {
if !name.is_empty() && !blocked(name) {
insert_name(names, name.to_owned(), windows);
}
}