refactor(static): centralize env var names

Add fabro-static::EnvVars as the shared registry for fixed environment variable names and migrate env reads, clap env bindings, and subprocess/test allowlists to use it.

Add clippy bans for raw std::env lookup APIs so future dynamic env facades must be documented explicitly.
This commit is contained in:
Bryan Helmkamp 2026-04-24 12:29:51 -04:00
parent e4e5eb08d7
commit 80de5ca616
85 changed files with 999 additions and 289 deletions

24
Cargo.lock generated
View file

@ -1520,6 +1520,7 @@ dependencies = [
"fabro-mcp",
"fabro-model",
"fabro-sandbox",
"fabro-static",
"fabro-test",
"fabro-types",
"fabro-util",
@ -1572,6 +1573,7 @@ dependencies = [
"fabro-http",
"fabro-model",
"fabro-oauth",
"fabro-static",
"fabro-util",
"fabro-vault",
"httpmock",
@ -1641,6 +1643,7 @@ dependencies = [
"fabro-retro",
"fabro-sandbox",
"fabro-server",
"fabro-static",
"fabro-store",
"fabro-telemetry",
"fabro-test",
@ -1699,6 +1702,7 @@ dependencies = [
"fabro-api",
"fabro-http",
"fabro-model",
"fabro-static",
"fabro-types",
"fabro-util",
"fs2",
@ -1727,6 +1731,7 @@ dependencies = [
"dirs",
"fabro-macros",
"fabro-proc",
"fabro-static",
"fabro-types",
"fabro-util",
"ipnet",
@ -1761,6 +1766,7 @@ name = "fabro-devcontainer"
version = "0.213.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-static",
"fabro-util",
"insta",
"serde",
@ -1780,6 +1786,7 @@ dependencies = [
"chrono",
"fabro-http",
"fabro-macros",
"fabro-static",
"fabro-test",
"fabro-types",
"jsonwebtoken",
@ -1832,6 +1839,7 @@ dependencies = [
name = "fabro-http"
version = "0.213.0-nightly.0"
dependencies = [
"fabro-static",
"http",
"reqwest 0.13.2",
"thiserror 2.0.18",
@ -1844,6 +1852,7 @@ dependencies = [
"anyhow",
"base64",
"fabro-config",
"fabro-static",
"fabro-types",
"fabro-vault",
"ring",
@ -1877,6 +1886,7 @@ dependencies = [
"fabro-http",
"fabro-macros",
"fabro-model",
"fabro-static",
"fabro-test",
"fabro-util",
"futures",
@ -1924,6 +1934,7 @@ dependencies = [
name = "fabro-model"
version = "0.213.0-nightly.0"
dependencies = [
"fabro-static",
"insta",
"serde",
"serde_json",
@ -1937,6 +1948,7 @@ dependencies = [
"axum",
"base64",
"fabro-http",
"fabro-static",
"fabro-test",
"fabro-util",
"hex",
@ -1990,6 +2002,7 @@ dependencies = [
"fabro-config",
"fabro-github",
"fabro-proc",
"fabro-static",
"fabro-types",
"futures",
"git2",
@ -2039,6 +2052,7 @@ dependencies = [
"fabro-sandbox",
"fabro-slack",
"fabro-spa",
"fabro-static",
"fabro-store",
"fabro-test",
"fabro-types",
@ -2092,6 +2106,7 @@ version = "0.213.0-nightly.0"
dependencies = [
"fabro-http",
"fabro-interview",
"fabro-static",
"fabro-workflow",
"futures-util",
"rustls",
@ -2112,6 +2127,10 @@ dependencies = [
"rust-embed",
]
[[package]]
name = "fabro-static"
version = "0.213.0-nightly.0"
[[package]]
name = "fabro-store"
version = "0.213.0-nightly.0"
@ -2148,6 +2167,7 @@ dependencies = [
"dirs",
"exec",
"fabro-http",
"fabro-static",
"fabro-util",
"fork",
"git2",
@ -2184,6 +2204,7 @@ dependencies = [
"fabro-config",
"fabro-http",
"fabro-proc",
"fabro-static",
"fabro-types",
"fabro-util",
"insta",
@ -2238,6 +2259,7 @@ dependencies = [
"anyhow",
"console 0.15.11",
"dirs",
"fabro-static",
"insta",
"open",
"rand 0.9.4",
@ -2301,6 +2323,7 @@ dependencies = [
"fabro-model",
"fabro-retro",
"fabro-sandbox",
"fabro-static",
"fabro-store",
"fabro-template",
"fabro-test",
@ -6946,6 +6969,7 @@ dependencies = [
"async-stream",
"axum",
"fabro-http",
"fabro-static",
"futures-util",
"http",
"serde",

View file

@ -79,6 +79,7 @@ rust-embed = "8"
percent-encoding = "2"
minijinja = "2"
fabro-http = { path = "lib/crates/fabro-http" }
fabro-static = { path = "lib/crates/fabro-static" }
graphviz-sys = { git = "https://github.com/fabro-sh/graphviz-sys" }
strum = { version = "0.28", features = ["derive"] }
zeroize = "1"

View file

@ -22,6 +22,10 @@ disallowed-methods = [
{ path = "std::fs::OpenOptions::open", reason = "Blocking open; prefer tokio::fs::OpenOptions::open on Tokio paths. OS file-lock semantics may require spawn_blocking instead. Document intentional sync I/O with #[expect(clippy::disallowed_methods, reason = \"...\")]" },
{ path = "std::env::set_var", reason = "Server/process env must be injected at construction or child-process spawn time, not mutated globally. See docs-internal/server-secrets-strategy.md" },
{ path = "std::env::remove_var", reason = "Server/process env must be injected at construction or child-process spawn time, not mutated globally. See docs-internal/server-secrets-strategy.md" },
{ path = "std::env::var", reason = "Use fabro_static::EnvVars for fixed environment variable names; document intentional process-env lookup facades with #[expect(clippy::disallowed_methods, reason = \"...\")]", allow-invalid = true },
{ path = "std::env::var_os", reason = "Use fabro_static::EnvVars for fixed environment variable names; document intentional process-env lookup facades with #[expect(clippy::disallowed_methods, reason = \"...\")]", allow-invalid = true },
{ path = "std::env::vars", reason = "Snapshotting the ambient process env must be limited to documented subprocess/test/bootstrap facades.", allow-invalid = true },
{ path = "std::env::vars_os", reason = "Snapshotting the ambient process env must be limited to documented subprocess/test/bootstrap facades.", allow-invalid = true },
{ path = "reqwest::Client::new", reason = "Use fabro_http::http_client() or fabro_http::test_http_client()", allow-invalid = true },
{ path = "reqwest::Client::builder", reason = "Use fabro_http::HttpClientBuilder::new()", allow-invalid = true },
{ path = "reqwest::blocking::Client::new", reason = "Use fabro_http::blocking_http_client() or fabro_http::blocking_test_http_client()", allow-invalid = true },

View file

@ -256,7 +256,7 @@ Solid arrows are real dependencies. Dashed lines show phases that are independen
### Phase 1 — fabro-static env var registry
- [ ] **Unit 1.1: Create `fabro-static` crate with `EnvVars` struct**
- [x] **Unit 1.1: Create `fabro-static` crate with `EnvVars` struct**
**Goal:** Ship a new leaf crate exposing all fabro + upstream env var names as `pub const` fields.
@ -292,7 +292,7 @@ Solid arrows are real dependencies. Dashed lines show phases that are independen
---
- [ ] **Unit 1.2: Migrate fixed env var names and classify dynamic reads**
- [x] **Unit 1.2: Migrate fixed env var names and classify dynamic reads**
**Goal:** Replace every fixed string-literal env var name in production code with a reference to `EnvVars`, and explicitly document the remaining dynamic env lookup facades that cannot name a single constant.

View file

@ -31,6 +31,7 @@ fabro-llm = { path = "../fabro-llm" }
fabro-model = { path = "../fabro-model" }
fabro-mcp = { path = "../fabro-mcp" }
fabro-sandbox = { path = "../fabro-sandbox" }
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
fabro-vault = { path = "../fabro-vault" }
fabro-http.workspace = true

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use fabro_llm::client::Client;
use fabro_llm::types::{Message, Request, ToolDefinition};
use fabro_model::ModelHandle;
use fabro_static::EnvVars;
use crate::config::SessionOptions;
use crate::sandbox::GrepOptions;
@ -467,8 +468,12 @@ fn format_brave_results(body: &serde_json::Value) -> String {
}
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "Web search tool setup reads the documented Brave API key override from process env."
)]
pub(crate) fn make_web_search_tool() -> RegisteredTool {
make_web_search_tool_with_api_key(std::env::var("BRAVE_SEARCH_API_KEY").ok())
make_web_search_tool_with_api_key(std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).ok())
}
fn make_web_search_tool_with_api_key(api_key: Option<String>) -> RegisteredTool {
@ -492,7 +497,10 @@ fn make_web_search_tool_with_api_key(api_key: Option<String>) -> RegisteredTool
let api_key = api_key.clone();
Box::pin(async move {
let api_key = api_key.ok_or_else(|| {
"BRAVE_SEARCH_API_KEY environment variable is not set".to_string()
format!(
"{} environment variable is not set",
EnvVars::BRAVE_SEARCH_API_KEY
)
})?;
let query = required_str(&args, "query")?;
@ -1407,8 +1415,12 @@ mod tests {
}
#[fabro_macros::e2e_test(live("BRAVE_SEARCH_API_KEY"))]
#[expect(
clippy::disallowed_methods,
reason = "Live web-search integration test reads its required API key from process env."
)]
async fn web_search_returns_results() {
let api_key = std::env::var("BRAVE_SEARCH_API_KEY")
let api_key = std::env::var(EnvVars::BRAVE_SEARCH_API_KEY)
.expect("BRAVE_SEARCH_API_KEY must be set to run this test");
let tool = make_web_search_tool_with_api_key(Some(api_key));
let env: Arc<dyn Sandbox> = Arc::new(MockSandbox::default());

View file

@ -17,6 +17,7 @@ chrono = { workspace = true, features = ["serde"] }
fabro-http.workspace = true
fabro-model = { path = "../fabro-model" }
fabro-oauth = { path = "../fabro-oauth" }
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
fabro-vault = { path = "../fabro-vault" }
serde.workspace = true

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use fabro_model::Provider;
use fabro_static::EnvVars;
use crate::credential_source::{CredentialSource, ResolvedCredentials};
use crate::{ApiCredential, EnvLookup};
@ -13,6 +14,10 @@ pub struct EnvCredentialSource {
impl EnvCredentialSource {
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "EnvCredentialSource is the provider API-key process-env facade."
)]
pub fn new() -> Self {
Self::with_env_lookup(Arc::new(|name| std::env::var(name).ok()))
}
@ -35,13 +40,13 @@ impl EnvCredentialSource {
let mut cred = ApiCredential::from_api_key(provider, key);
match provider {
Provider::Anthropic => {
cred.base_url = self.lookup("ANTHROPIC_BASE_URL");
cred.base_url = self.lookup(EnvVars::ANTHROPIC_BASE_URL);
}
Provider::OpenAi => {
cred.base_url = self.lookup("OPENAI_BASE_URL");
cred.org_id = self.lookup("OPENAI_ORG_ID");
cred.project_id = self.lookup("OPENAI_PROJECT_ID");
if let Some(account_id) = self.lookup("CHATGPT_ACCOUNT_ID") {
cred.base_url = self.lookup(EnvVars::OPENAI_BASE_URL);
cred.org_id = self.lookup(EnvVars::OPENAI_ORG_ID);
cred.project_id = self.lookup(EnvVars::OPENAI_PROJECT_ID);
if let Some(account_id) = self.lookup(EnvVars::CHATGPT_ACCOUNT_ID) {
cred.base_url = Some("https://chatgpt.com/backend-api/codex".to_string());
cred.codex_mode = true;
cred.extra_headers
@ -51,7 +56,7 @@ impl EnvCredentialSource {
}
}
Provider::Gemini => {
cred.base_url = self.lookup("GEMINI_BASE_URL");
cred.base_url = self.lookup(EnvVars::GEMINI_BASE_URL);
}
Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => {}
// OpenAiCompatible has no api_key_env_vars, so find_map returned None above.

View file

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use fabro_model::Provider;
use fabro_static::EnvVars;
use fabro_vault::Vault;
use shlex::try_quote;
use tokio::sync::RwLock as AsyncRwLock;
@ -115,6 +116,10 @@ pub struct CredentialResolver {
impl CredentialResolver {
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "CredentialResolver owns the process-env fallback used after vault lookup."
)]
pub fn new(vault: Arc<AsyncRwLock<Vault>>) -> Self {
Self::with_env_lookup(vault, Arc::new(|name| std::env::var(name).ok()))
}
@ -229,12 +234,12 @@ impl CredentialResolver {
fn to_api_credential(&self, vault: &Vault, credential: &AuthCredential) -> ApiCredential {
let base_url = match credential.provider {
Provider::Anthropic => self.lookup_env_or_vault(vault, "ANTHROPIC_BASE_URL"),
Provider::OpenAi => self.lookup_env_or_vault(vault, "OPENAI_BASE_URL"),
Provider::Gemini => self.lookup_env_or_vault(vault, "GEMINI_BASE_URL"),
Provider::Anthropic => self.lookup_env_or_vault(vault, EnvVars::ANTHROPIC_BASE_URL),
Provider::OpenAi => self.lookup_env_or_vault(vault, EnvVars::OPENAI_BASE_URL),
Provider::Gemini => self.lookup_env_or_vault(vault, EnvVars::GEMINI_BASE_URL),
Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => None,
Provider::OpenAiCompatible => {
self.lookup_env_or_vault(vault, "OPENAI_COMPATIBLE_BASE_URL")
self.lookup_env_or_vault(vault, EnvVars::OPENAI_COMPATIBLE_BASE_URL)
}
};
match &credential.details {
@ -242,8 +247,8 @@ impl CredentialResolver {
let mut cred = ApiCredential::from_api_key(credential.provider, key.clone());
cred.base_url = base_url;
if credential.provider == Provider::OpenAi {
cred.org_id = self.lookup_env_or_vault(vault, "OPENAI_ORG_ID");
cred.project_id = self.lookup_env_or_vault(vault, "OPENAI_PROJECT_ID");
cred.org_id = self.lookup_env_or_vault(vault, EnvVars::OPENAI_ORG_ID);
cred.project_id = self.lookup_env_or_vault(vault, EnvVars::OPENAI_PROJECT_ID);
}
cred
}
@ -261,8 +266,8 @@ impl CredentialResolver {
extra_headers,
base_url: Some("https://chatgpt.com/backend-api/codex".to_string()),
codex_mode: true,
org_id: self.lookup_env_or_vault(vault, "OPENAI_ORG_ID"),
project_id: self.lookup_env_or_vault(vault, "OPENAI_PROJECT_ID"),
org_id: self.lookup_env_or_vault(vault, EnvVars::OPENAI_ORG_ID),
project_id: self.lookup_env_or_vault(vault, EnvVars::OPENAI_PROJECT_ID),
}
}
}
@ -272,7 +277,7 @@ impl CredentialResolver {
let mut env_vars = HashMap::new();
let login_command = match (&credential.provider, &credential.details, kind) {
(Provider::OpenAi, AuthDetails::ApiKey { key }, CliAgentKind::Codex) => {
env_vars.insert("OPENAI_API_KEY".to_string(), key.clone());
env_vars.insert(EnvVars::OPENAI_API_KEY.to_string(), key.clone());
Some(codex_login_command(key))
}
(
@ -282,9 +287,12 @@ impl CredentialResolver {
},
CliAgentKind::Codex,
) => {
env_vars.insert("OPENAI_API_KEY".to_string(), tokens.access_token.clone());
env_vars.insert(
EnvVars::OPENAI_API_KEY.to_string(),
tokens.access_token.clone(),
);
if let Some(account_id) = account_id {
env_vars.insert("CHATGPT_ACCOUNT_ID".to_string(), account_id.clone());
env_vars.insert(EnvVars::CHATGPT_ACCOUNT_ID.to_string(), account_id.clone());
}
Some(codex_login_command(&tokens.access_token))
}
@ -295,7 +303,10 @@ impl CredentialResolver {
None
}
(_, AuthDetails::CodexOAuth { tokens, .. }, _) => {
env_vars.insert("OPENAI_API_KEY".to_string(), tokens.access_token.clone());
env_vars.insert(
EnvVars::OPENAI_API_KEY.to_string(),
tokens.access_token.clone(),
);
None
}
};
@ -319,16 +330,22 @@ pub async fn configured_providers_from_process_env(
None => Provider::ALL
.iter()
.copied()
.filter(|provider| {
provider
.api_key_env_vars()
.iter()
.any(|env_var| std::env::var(env_var).is_ok())
})
.filter(|provider| provider_has_process_env_api_key(*provider))
.collect(),
}
}
#[expect(
clippy::disallowed_methods,
reason = "Provider discovery intentionally checks documented API-key env names."
)]
fn provider_has_process_env_api_key(provider: Provider) -> bool {
provider
.api_key_env_vars()
.iter()
.any(|env_var| std::env::var(env_var).is_ok())
}
fn codex_login_command(api_key: &str) -> String {
let quoted =
try_quote(api_key).map_or_else(|_| api_key.to_string(), std::borrow::Cow::into_owned);

View file

@ -47,6 +47,7 @@ fabro-vault = { path = "../fabro-vault" }
fabro-types = { path = "../fabro-types", features = ["clap"] }
fabro-util = { path = "../fabro-util" }
fabro-http.workspace = true
fabro-static.workspace = true
clap.workspace = true
clap_complete.workspace = true
cli-table.workspace = true

View file

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use clap::{Args, Subcommand, ValueEnum};
use fabro_agent::cli::AgentArgs;
use fabro_config::{CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer};
use fabro_static::EnvVars;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_types::settings::run::MergeStrategy;
use fabro_util::printer::Printer;
@ -21,23 +22,23 @@ pub(crate) const LONG_VERSION: &str = concat!(
#[derive(Args)]
pub(crate) struct GlobalArgs {
/// Output as JSON
#[arg(long, global = true, env = "FABRO_JSON", value_parser = clap::builder::BoolishValueParser::new())]
#[arg(long, global = true, env = EnvVars::FABRO_JSON, value_parser = clap::builder::BoolishValueParser::new())]
pub json: bool,
/// Enable DEBUG-level logging (default is INFO)
#[arg(long, global = true, env = "FABRO_DEBUG", value_parser = clap::builder::BoolishValueParser::new())]
#[arg(long, global = true, env = EnvVars::FABRO_DEBUG, value_parser = clap::builder::BoolishValueParser::new())]
pub debug: bool,
/// Disable automatic upgrade check
#[arg(long, global = true, env = "FABRO_NO_UPGRADE_CHECK", value_parser = clap::builder::BoolishValueParser::new())]
#[arg(long, global = true, env = EnvVars::FABRO_NO_UPGRADE_CHECK, value_parser = clap::builder::BoolishValueParser::new())]
pub no_upgrade_check: bool,
/// Suppress non-essential output
#[arg(long, global = true, env = "FABRO_QUIET", value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "verbose")]
#[arg(long, global = true, env = EnvVars::FABRO_QUIET, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "verbose")]
pub quiet: bool,
/// Enable verbose output
#[arg(long, global = true, env = "FABRO_VERBOSE", value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "quiet")]
#[arg(long, global = true, env = EnvVars::FABRO_VERBOSE, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "quiet")]
pub verbose: bool,
}
@ -84,7 +85,7 @@ pub(crate) fn require_no_json_override(process_local_json: bool) -> anyhow::Resu
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct StorageDirArgs {
/// Local storage directory (default: ~/.fabro/storage)
#[arg(long, env = "FABRO_STORAGE_DIR")]
#[arg(long, env = EnvVars::FABRO_STORAGE_DIR)]
pub(crate) storage_dir: Option<PathBuf>,
}
@ -101,7 +102,7 @@ impl StorageDirArgs {
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct ServerTargetArgs {
/// Fabro server target: http(s) URL or absolute Unix socket path
#[arg(long = "server", env = "FABRO_SERVER")]
#[arg(long = "server", env = EnvVars::FABRO_SERVER)]
pub(crate) server: Option<String>,
}

View file

@ -1,6 +1,7 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use fabro_client::{AuthEntry, AuthStore};
use fabro_static::EnvVars;
use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format};
use serde::Serialize;
@ -166,8 +167,12 @@ fn human_state(state: OAuthState) -> &'static str {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Auth status reports whether the documented dev-token env source is configured."
)]
fn load_dev_token_if_available() -> bool {
let env_token = std::env::var("FABRO_DEV_TOKEN")
let env_token = std::env::var(EnvVars::FABRO_DEV_TOKEN)
.ok()
.filter(|token| validate_dev_token_format(token));
env_token.is_some()

View file

@ -9,9 +9,10 @@ use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use fabro_config::bind::{self, Bind, BindRequest};
use fabro_config::user::{FABRO_CONFIG_ENV, active_settings_path, default_storage_dir};
use fabro_config::user::{active_settings_path, default_storage_dir};
use fabro_server::install::{self, InstallAppState};
use fabro_server::serve::{self, ServeArgs};
use fabro_static::EnvVars;
use fabro_util::browser;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -167,7 +168,7 @@ fn maybe_install_bootstrap(
storage_dir: Option<&std::path::Path>,
serve_args: &ServeArgs,
) -> Result<Option<InstallBootstrap>> {
if explicit_config.is_some() || std::env::var_os(FABRO_CONFIG_ENV).is_some() {
if explicit_config.is_some() || has_config_env_override() {
return Ok(None);
}
@ -191,6 +192,14 @@ fn maybe_install_bootstrap(
}))
}
#[expect(
clippy::disallowed_methods,
reason = "Install bootstrap checks whether the documented FABRO_CONFIG override is set."
)]
fn has_config_env_override() -> bool {
std::env::var_os(EnvVars::FABRO_CONFIG).is_some()
}
async fn run_install_mode(bootstrap: InstallBootstrap, printer: Printer) -> Result<()> {
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
let token = bootstrap.token.clone();
@ -258,8 +267,12 @@ fn install_mode_next_step_message(supervised: bool) -> &'static str {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Install-mode URL hints honor Railway's documented public-domain env var."
)]
fn install_url_hint(bind: &Bind, token: &str) -> Option<String> {
if let Some(domain) = std::env::var("RAILWAY_PUBLIC_DOMAIN")
if let Some(domain) = std::env::var(EnvVars::RAILWAY_PUBLIC_DOMAIN)
.ok()
.filter(|value| !value.is_empty())
{
@ -286,10 +299,14 @@ fn default_install_bind_request() -> BindRequest {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Install-mode bind defaults inspect known container platform env markers."
)]
fn running_in_container() -> bool {
std::env::var_os("RAILWAY_PUBLIC_DOMAIN").is_some()
|| std::env::var_os("RAILWAY_ENVIRONMENT").is_some()
|| std::env::var_os("KUBERNETES_SERVICE_HOST").is_some()
std::env::var_os(EnvVars::RAILWAY_PUBLIC_DOMAIN).is_some()
|| std::env::var_os(EnvVars::RAILWAY_ENVIRONMENT).is_some()
|| std::env::var_os(EnvVars::KUBERNETES_SERVICE_HOST).is_some()
|| std::path::Path::new("/.dockerenv").exists()
|| std::path::Path::new("/run/.containerenv").exists()
}

View file

@ -10,10 +10,11 @@ use anyhow::{Context, Result, anyhow, bail};
use fabro_config::RuntimeDirectory;
use fabro_config::bind::{Bind, BindRequest};
use fabro_config::daemon::ServerDaemon;
use fabro_config::user::{FABRO_CONFIG_ENV, default_settings_path};
use fabro_config::user::default_settings_path;
use fabro_server::jwt_auth::auth_method_name;
use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs, resolve_runtime_server_settings_for_start};
use fabro_server::{process_env_snapshot, validate_startup};
use fabro_static::EnvVars;
use fabro_types::settings::ServerAuthMethod;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
@ -92,7 +93,7 @@ pub(crate) async fn ensure_server_running_for_storage(
config_path: &Path,
) -> Result<Bind> {
ensure_storage_server_autostart_allowed(
std::env::var_os(FABRO_CONFIG_ENV).as_deref(),
std::env::var_os(EnvVars::FABRO_CONFIG).as_deref(),
config_path,
&default_settings_path(),
)?;
@ -208,7 +209,7 @@ fn bind_matches_request(existing: &Bind, requested: &BindRequest) -> bool {
}
fn server_max_concurrent_runs_override() -> Option<usize> {
std::env::var("FABRO_SERVER_MAX_CONCURRENT_RUNS")
std::env::var(EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value > 0)

View file

@ -15,6 +15,7 @@ use std::time::Duration;
use anyhow::{Context, Result};
use fabro_config::Storage;
use fabro_config::daemon::ServerDaemon;
use fabro_static::EnvVars;
use fabro_util::Home;
use fabro_util::printer::Printer;
use serde::Serialize;
@ -120,13 +121,17 @@ fn dir_size(path: &Path) -> u64 {
total
}
#[expect(
clippy::disallowed_methods,
reason = "Uninstall scans shell config paths honoring the conventional ZDOTDIR env var."
)]
fn find_shell_configs_with_sentinel() -> Vec<PathBuf> {
let mut found = Vec::new();
let Some(home) = dirs::home_dir() else {
return found;
};
let zdotdir = std::env::var("ZDOTDIR")
let zdotdir = std::env::var(EnvVars::ZDOTDIR)
.ok()
.map_or_else(|| home.clone(), PathBuf::from);

View file

@ -65,7 +65,15 @@ impl LocalServerConfig {
}
pub(crate) fn storage_dir_from_toml(source: &str) -> Result<PathBuf> {
storage_dir_from_toml_with_lookup(source, &|name| std::env::var(name).ok())
storage_dir_from_toml_with_lookup(source, &process_env_var)
}
#[expect(
clippy::disallowed_methods,
reason = "Local server config interpolation owns a process-env lookup facade for {{ env.* }} values."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn storage_dir_from_toml_with_lookup(

View file

@ -27,6 +27,7 @@ use args::{
global_args_cli_layer, require_no_json_override,
};
use clap::{CommandFactory, Parser};
use fabro_static::EnvVars;
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
use fabro_util::exit::ExitClass;
use fabro_util::printer::Printer;
@ -79,14 +80,14 @@ async fn main() {
// unscrubbed spawn site cannot leak it. The token flows to `runner::execute`
// through an explicit function argument instead of the environment.
let worker_token = if subcommand == Some("__run-worker") {
let token = std::env::var("FABRO_WORKER_TOKEN").ok();
let token = process_env_var(EnvVars::FABRO_WORKER_TOKEN);
#[expect(
clippy::disallowed_methods,
reason = "Scrub the worker bearer from this process's env before any \
child process is spawned, so no descendant can inherit it."
)]
{
std::env::remove_var("FABRO_WORKER_TOKEN");
std::env::remove_var(EnvVars::FABRO_WORKER_TOKEN);
}
token
} else {
@ -108,7 +109,7 @@ async fn main() {
if !command_name.is_empty() {
let command = sanitize::sanitize_command(&raw_args, &command_name);
let repository = git::repository_identifier();
let ci = std::env::var("CI").is_ok();
let ci = process_env_var(EnvVars::CI).is_some();
if is_error {
fabro_telemetry::track!("CLI Errored", {
"subcommand": command_name,
@ -166,6 +167,14 @@ async fn main() {
}
}
#[expect(
clippy::disallowed_methods,
reason = "CLI main reads documented process-env controls before telemetry and worker dispatch."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
async fn main_inner(worker_token: Option<String>) -> (String, Result<()>) {
let _ = default_provider().install_default();

View file

@ -10,6 +10,7 @@ use fabro_client::{
};
pub(crate) use fabro_client::{Client, RunEventStream};
use fabro_config::bind::Bind;
use fabro_static::EnvVars;
pub(crate) use fabro_types::RunProjection;
use fabro_types::UserSettings;
use fabro_util::dev_token::validate_dev_token_format;
@ -208,10 +209,18 @@ fn local_dev_token_fallback(target: &ServerTarget) -> bool {
}
fn load_cli_dev_token() -> Option<String> {
let env_token = std::env::var("FABRO_DEV_TOKEN").ok();
let env_token = process_env_var(EnvVars::FABRO_DEV_TOKEN);
load_cli_dev_token_from_sources(env_token.as_deref(), &Home::from_env())
}
#[expect(
clippy::disallowed_methods,
reason = "Server client authentication supports the documented local dev-token env source."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn load_cli_dev_token_from_sources(env_token: Option<&str>, home: &Home) -> Option<String> {
if let Some(token) = env_token.filter(|token| validate_dev_token_format(token)) {
return Some(token.to_owned());
@ -307,7 +316,7 @@ fn resolve_local_tcp_credential_with_store(
}
fn resolve_local_tcp_credential(target: &ServerTarget) -> Result<Option<Credential>> {
let env_token = std::env::var("FABRO_DEV_TOKEN").ok();
let env_token = process_env_var(EnvVars::FABRO_DEV_TOKEN);
let store = AuthStore::default();
resolve_local_tcp_credential_with_store(
target,
@ -321,7 +330,7 @@ fn resolve_target_credential(
target: &ServerTarget,
allow_local_dev_token_fallback: bool,
) -> Result<Option<Credential>> {
let env_token = std::env::var("FABRO_DEV_TOKEN").ok();
let env_token = process_env_var(EnvVars::FABRO_DEV_TOKEN);
let store = AuthStore::default();
if let Some(credential) = resolve_local_tcp_credential_with_store(
target,

View file

@ -1,5 +1,6 @@
use anyhow::anyhow;
use fabro_github::GitHubCredentials;
use fabro_static::EnvVars;
use fabro_types::settings::server::GithubIntegrationStrategy;
use fabro_vault::Vault;
@ -27,9 +28,14 @@ pub(crate) fn build_github_credentials(
/// Look up GitHub token: GITHUB_TOKEN env -> vault GITHUB_TOKEN -> GH_TOKEN env
/// -> vault GH_TOKEN
fn lookup_github_token(vault: Option<&Vault>) -> Option<String> {
lookup_env_or_vault("GITHUB_TOKEN", vault).or_else(|| lookup_env_or_vault("GH_TOKEN", vault))
lookup_env_or_vault(EnvVars::GITHUB_TOKEN, vault)
.or_else(|| lookup_env_or_vault(EnvVars::GH_TOKEN, vault))
}
#[expect(
clippy::disallowed_methods,
reason = "GitHub credential resolution intentionally falls back from vault to documented process-env names."
)]
fn lookup_env_or_vault(name: &str, vault: Option<&Vault>) -> Option<String> {
std::env::var(name)
.ok()

View file

@ -116,6 +116,10 @@ fn read_api_key_from_stdin() -> Result<String> {
normalize_api_key_input(&raw)
}
#[expect(
clippy::disallowed_methods,
reason = "The user explicitly selected an API-key env var as the credential source."
)]
fn read_api_key_from_env_var(name: &str) -> Result<String> {
let value =
std::env::var(name).with_context(|| format!("environment variable {name} is not set"))?;

View file

@ -3,11 +3,12 @@ use std::str::FromStr;
use anyhow::Result;
pub(crate) use fabro_client::ServerTarget;
pub(crate) use fabro_config::user::{FABRO_CONFIG_ENV, active_settings_path, default_storage_dir};
pub(crate) use fabro_config::user::{active_settings_path, default_storage_dir};
use fabro_config::user::{default_settings_path, default_socket_path};
use fabro_config::{
CliLayer, ParseError, RunSettingsBuilder, ServerSettingsBuilder, UserSettingsBuilder,
};
use fabro_static::EnvVars;
use fabro_types::settings::cli::CliTargetSettings;
use fabro_types::settings::{CliNamespace, InterpString, RunNamespace};
use fabro_types::{ServerSettings, UserSettings};
@ -52,7 +53,7 @@ pub(crate) fn load_resolved_settings(
}
fn load_settings_document(config_path: Option<&Path>) -> anyhow::Result<toml::Value> {
load_settings_document_with_lookup(config_path, |name| std::env::var_os(name))
load_settings_document_with_lookup(config_path, process_env_var_os)
}
#[expect(
@ -65,7 +66,7 @@ fn load_settings_document_with_lookup(
) -> anyhow::Result<toml::Value> {
let config_path = config_path
.map(Path::to_path_buf)
.or_else(|| lookup(FABRO_CONFIG_ENV).map(PathBuf::from));
.or_else(|| lookup(EnvVars::FABRO_CONFIG).map(PathBuf::from));
let path = if let Some(path) = config_path {
path
@ -125,7 +126,23 @@ fn storage_dir_from_document(
document: &toml::Value,
storage_dir: Option<&Path>,
) -> anyhow::Result<PathBuf> {
storage_dir_from_document_with_lookup(document, storage_dir, &|name| std::env::var(name).ok())
storage_dir_from_document_with_lookup(document, storage_dir, &process_env_var)
}
#[expect(
clippy::disallowed_methods,
reason = "CLI settings loading owns the process-env facade for interpolation."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
#[expect(
clippy::disallowed_methods,
reason = "CLI settings loading owns the process-env facade for config path lookup."
)]
fn process_env_var_os(name: &str) -> Option<std::ffi::OsString> {
std::env::var_os(name)
}
fn storage_dir_from_document_with_lookup(

View file

@ -1,10 +1,10 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
reason = "integration tests stage fixtures and subprocess env with sync test infrastructure"
)]
use fabro_config::{Storage, envfile};
use fabro_test::{fabro_snapshot, test_context};
use fabro_test::{EnvVars, fabro_snapshot, test_context};
use fabro_vault::{SecretType, Vault};
#[test]
@ -270,10 +270,14 @@ mode = "keep-me"
std::fs::set_permissions(&fake_gh, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
let path = format!(
"{}:{}",
fake_bin.display(),
std::env::var(EnvVars::PATH).unwrap()
);
let output = context
.command()
.env("PATH", path)
.env(EnvVars::PATH, path)
.args([
"install",
"github",

View file

@ -1,10 +1,10 @@
#![expect(
clippy::disallowed_methods,
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
reason = "integration tests stage fixtures and subprocess env with sync test infrastructure"
)]
use assert_cmd::Command;
use fabro_test::{TestContext, fabro_snapshot, test_context};
use fabro_test::{EnvVars, TestContext, fabro_snapshot, test_context};
fn hard_link_or_copy(src: &std::path::Path, dest: &std::path::Path) {
if std::fs::hard_link(src, dest).is_ok() {
@ -48,13 +48,13 @@ fn brew_command(context: &TestContext, formula: &str, version: &str) -> Command
}
}
}
cmd.env("NO_COLOR", "1");
cmd.env("HOME", &context.home_dir);
cmd.env("FABRO_NO_UPGRADE_CHECK", "true")
.env("FABRO_HTTP_PROXY_POLICY", "disabled")
.env("FABRO_TELEMETRY", "off")
.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64")
.env("FABRO_TEST_IN_MEMORY_STORE", "1");
cmd.env(EnvVars::NO_COLOR, "1");
cmd.env(EnvVars::HOME, &context.home_dir);
cmd.env(EnvVars::FABRO_NO_UPGRADE_CHECK, "true")
.env(EnvVars::FABRO_HTTP_PROXY_POLICY, "disabled")
.env(EnvVars::FABRO_TELEMETRY, "off")
.env(EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS, "64")
.env(EnvVars::FABRO_TEST_IN_MEMORY_STORE, "1");
cmd
}
@ -177,9 +177,13 @@ esac
"[TARGET]".to_string(),
));
let path = format!("{}:{}", fake_bin.display(), std::env::var("PATH").unwrap());
let path = format!(
"{}:{}",
fake_bin.display(),
std::env::var(EnvVars::PATH).unwrap()
);
let mut cmd = context.command();
cmd.env("PATH", path).args(["upgrade", "--dry-run"]);
cmd.env(EnvVars::PATH, path).args(["upgrade", "--dry-run"]);
fabro_snapshot!(filters, cmd, @"
success: true

View file

@ -3,7 +3,7 @@ mod auth_tokens;
use assert_cmd::Command;
use fabro_store::EventEnvelope;
use fabro_test::{TestContext, preserve_coverage_env};
use fabro_test::{EnvVars, TestContext, preserve_coverage_env};
use fabro_types::RunId;
macro_rules! fabro_json_snapshot {
($context:expr, $value:expr, @$snapshot:literal) => {{
@ -99,17 +99,21 @@ impl LightweightCli {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Lightweight CLI test harness reconstructs a minimal process env for subprocesses."
)]
pub(crate) fn command(&self) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_fabro"));
cmd.env_clear();
preserve_coverage_env!(cmd);
if let Some(path) = std::env::var_os("PATH") {
cmd.env("PATH", path);
if let Some(path) = std::env::var_os(EnvVars::PATH) {
cmd.env(EnvVars::PATH, path);
}
cmd.env("HOME", self.home_dir.path());
cmd.env("NO_COLOR", "1");
cmd.env("FABRO_NO_UPGRADE_CHECK", "true")
.env("FABRO_HTTP_PROXY_POLICY", "disabled");
cmd.env(EnvVars::HOME, self.home_dir.path());
cmd.env(EnvVars::NO_COLOR, "1");
cmd.env(EnvVars::FABRO_NO_UPGRADE_CHECK, "true")
.env(EnvVars::FABRO_HTTP_PROXY_POLICY, "disabled");
cmd.current_dir(self.home_dir.path());
cmd
}

View file

@ -19,6 +19,7 @@ chrono = { workspace = true, features = ["serde"] }
fabro-api = { path = "../fabro-api" }
fabro-http.workspace = true
fabro-model = { path = "../fabro-model" }
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-util = { path = "../fabro-util" }
fs2.workspace = true

View file

@ -13,6 +13,7 @@ use std::io::Write as _;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use fabro_static::EnvVars;
use fs2::FileExt;
use rand::Rng;
use serde::{Deserialize, Serialize};
@ -20,8 +21,6 @@ use thiserror::Error;
use crate::target::ServerTarget;
const AUTH_FILE_ENV: &str = "FABRO_AUTH_FILE";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoredSubject {
pub idp_issuer: String,
@ -83,7 +82,8 @@ pub enum AuthStoreError {
#[derive(Debug, Error)]
pub enum LockError {
#[error(
"the filesystem backing {path} does not support file locking; move the auth store to a local filesystem or set {AUTH_FILE_ENV} to a local path"
"the filesystem backing {path} does not support file locking; move the auth store to a local filesystem or set {env} to a local path",
env = EnvVars::FABRO_AUTH_FILE
)]
FilesystemDoesNotSupportLocking { path: PathBuf },
#[error("failed to lock auth store at {path}: {source}")]
@ -106,7 +106,7 @@ struct AuthFile {
impl Default for AuthStore {
fn default() -> Self {
let path = std::env::var_os(AUTH_FILE_ENV).map_or_else(
let path = std::env::var_os(EnvVars::FABRO_AUTH_FILE).map_or_else(
|| fabro_util::Home::from_env().root().join("auth.json"),
PathBuf::from,
);
@ -362,10 +362,11 @@ mod tests {
use std::thread;
use chrono::Duration;
use fabro_static::EnvVars;
#[cfg(unix)]
use super::{AUTH_FILE_ENV, LockError, classify_lock_error};
use super::{AuthEntry, AuthStore, StoredSubject, key_for_target};
#[cfg(unix)]
use super::{LockError, classify_lock_error};
use crate::target::ServerTarget;
fn entry(login: &str) -> AuthEntry {
@ -552,6 +553,6 @@ mod tests {
err,
LockError::FilesystemDoesNotSupportLocking { path: ref error_path } if error_path == &path
));
assert!(err.to_string().contains(AUTH_FILE_ENV));
assert!(err.to_string().contains(EnvVars::FABRO_AUTH_FILE));
}
}

View file

@ -22,6 +22,7 @@ clap = { workspace = true, optional = true }
chrono.workspace = true
fabro-macros = { path = "../fabro-macros" }
fabro-proc = { path = "../fabro-proc" }
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-util = { path = "../fabro-util" }
dirs.workspace = true

View file

@ -95,7 +95,7 @@ fn resolve_goal_file(
base_dir: &Path,
) -> std::result::Result<ResolvedRunGoal, ResolveRunGoalError> {
let resolved = file
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.map_err(|err| ResolveRunGoalError::EnvLookup { var: err.name })?;
let path = resolve_goal_file_path(&resolved.value, base_dir);
let text = std::fs::read_to_string(&path).map_err(|source| ResolveRunGoalError::Io {
@ -108,6 +108,14 @@ fn resolve_goal_file(
})
}
#[expect(
clippy::disallowed_methods,
reason = "Run config interpolation owns a process-env lookup facade for {{ env.* }} values."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn resolve_layer_goal(
goal: &RunGoalLayer,
base_dir: &Path,

View file

@ -6,12 +6,13 @@
use std::path::{Path, PathBuf};
use fabro_static::EnvVars;
use crate::home::Home;
use crate::load::load_settings_path;
use crate::{Result, SettingsLayer};
pub const SETTINGS_CONFIG_FILENAME: &str = "settings.toml";
pub const FABRO_CONFIG_ENV: &str = "FABRO_CONFIG";
pub fn default_settings_path() -> PathBuf {
Home::from_env().user_config()
@ -25,6 +26,10 @@ pub fn default_socket_path() -> PathBuf {
Home::from_env().root().join("fabro.sock")
}
#[expect(
clippy::disallowed_methods,
reason = "Config loading owns the process-env facade used to resolve user settings paths."
)]
pub fn active_settings_path(path: Option<&Path>) -> PathBuf {
active_settings_path_with_lookup(path, |name| std::env::var_os(name))
}
@ -34,17 +39,21 @@ fn active_settings_path_with_lookup(
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> PathBuf {
path.map(Path::to_path_buf)
.or_else(|| lookup(FABRO_CONFIG_ENV).map(PathBuf::from))
.or_else(|| lookup(EnvVars::FABRO_CONFIG).map(PathBuf::from))
.unwrap_or_else(default_settings_path)
}
/// Load settings config from an explicit path or `~/.fabro/settings.toml`,
/// returning defaults if the default file doesn't exist. An explicit path that
/// doesn't exist is an error.
#[expect(
clippy::disallowed_methods,
reason = "Config loading owns the process-env facade used to resolve user settings paths."
)]
pub(crate) fn load_settings_config(path: Option<&Path>) -> Result<SettingsLayer> {
if let Some(explicit) = path
.map(Path::to_path_buf)
.or_else(|| std::env::var_os(FABRO_CONFIG_ENV).map(PathBuf::from))
.or_else(|| std::env::var_os(EnvVars::FABRO_CONFIG).map(PathBuf::from))
{
return load_v2_layer_from_path(&explicit);
}

View file

@ -13,6 +13,7 @@ doctest = false
workspace = true
[dependencies]
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
fabro-http.workspace = true
serde = { workspace = true }

View file

@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt::Write;
use std::path::Path;
use fabro_static::EnvVars;
use tokio::fs;
use tokio::process::Command;
use tracing::info;
@ -63,6 +64,10 @@ fn dir_name_from_id(feature_id: &str) -> String {
}
/// Ensure `oras` CLI is available, installing it if necessary.
#[expect(
clippy::disallowed_methods,
reason = "OCI feature fetching installs oras under the user's HOME on Linux."
)]
async fn ensure_oras() -> crate::Result<()> {
let check = Command::new("which")
.arg("oras")
@ -92,7 +97,7 @@ async fn ensure_oras() -> crate::Result<()> {
}
} else {
// Linux: download from GitHub releases to ~/.local/bin/
let home = std::env::var("HOME")
let home = std::env::var(EnvVars::HOME)
.map_err(|_| DevcontainerError::OrasInstall("HOME not set".to_string()))?;
let bin_dir = format!("{home}/.local/bin");
@ -1089,8 +1094,12 @@ mod tests {
#[tokio::test]
#[ignore = "requires oras"]
#[expect(
clippy::disallowed_methods,
reason = "Ignored OCI integration test is opt-in via a documented process-env flag."
)]
async fn fetch_feature_oci_integration() {
if std::env::var_os("FABRO_ENABLE_FETCH_FEATURE_OCI_INTEGRATION").is_none() {
if std::env::var_os(EnvVars::FABRO_ENABLE_FETCH_FEATURE_OCI_INTEGRATION).is_none() {
return;
}

View file

@ -16,6 +16,7 @@ workspace = true
serde.workspace = true
serde_json.workspace = true
fabro-http.workspace = true
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
jsonwebtoken.workspace = true
chrono.workspace = true

View file

@ -1,5 +1,6 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use fabro_static::EnvVars;
use fabro_types::PullRequestGithubDetail;
use fabro_types::settings::run::MergeStrategy;
use serde::Deserialize;
@ -9,8 +10,12 @@ pub const GITHUB_API_BASE_URL: &str = "https://api.github.com";
/// Returns the GitHub API base URL, allowing override via `GITHUB_BASE_URL` env
/// var.
#[expect(
clippy::disallowed_methods,
reason = "GitHub API client exposes a documented process-env base URL override."
)]
pub fn github_api_base_url() -> String {
std::env::var("GITHUB_BASE_URL").unwrap_or_else(|_| GITHUB_API_BASE_URL.to_string())
std::env::var(EnvVars::GITHUB_BASE_URL).unwrap_or_else(|_| GITHUB_API_BASE_URL.to_string())
}
/// Bundle of GitHub credentials and the API base URL, threaded through every
@ -94,11 +99,15 @@ pub struct GitHubAppCredentials {
}
impl GitHubAppCredentials {
#[expect(
clippy::disallowed_methods,
reason = "GitHub App credentials support a documented private-key env source."
)]
pub fn private_key_from_env() -> Result<Option<String>, String> {
let Ok(raw) = std::env::var("GITHUB_APP_PRIVATE_KEY") else {
let Ok(raw) = std::env::var(EnvVars::GITHUB_APP_PRIVATE_KEY) else {
return Ok(None);
};
decode_pem_env("GITHUB_APP_PRIVATE_KEY", &raw).map(Some)
decode_pem_env(EnvVars::GITHUB_APP_PRIVATE_KEY, &raw).map(Some)
}
pub fn from_env(app_id: Option<&str>) -> Result<Option<Self>, String> {

View file

@ -13,6 +13,7 @@ doctest = false
workspace = true
[dependencies]
fabro-static.workspace = true
reqwest = { workspace = true, features = ["blocking", "cookies"] }
thiserror.workspace = true

View file

@ -8,6 +8,7 @@
use std::path::Path;
use std::time::Duration;
use fabro_static::EnvVars;
pub use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
pub use reqwest::{
Body, Method, RequestBuilder, Response, StatusCode, Url, header, multipart, tls,
@ -19,8 +20,6 @@ pub type BlockingRequestBuilder = reqwest::blocking::RequestBuilder;
pub type BlockingResponse = reqwest::blocking::Response;
pub type Proxy = reqwest::Proxy;
pub const HTTP_PROXY_POLICY_ENV: &str = "FABRO_HTTP_PROXY_POLICY";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProxyPolicy {
System,
@ -51,7 +50,7 @@ impl ProxyPolicy {
}
fn resolve(explicit: Option<Self>) -> Result<Self, HttpClientBuildError> {
match std::env::var(HTTP_PROXY_POLICY_ENV) {
match std::env::var(EnvVars::FABRO_HTTP_PROXY_POLICY) {
Ok(value) => Self::resolve_with_env_value(explicit, Some(&value)),
Err(std::env::VarError::NotPresent) => Self::resolve_with_env_value(explicit, None),
Err(std::env::VarError::NotUnicode(value)) => Err(
@ -63,7 +62,7 @@ impl ProxyPolicy {
#[derive(Debug, thiserror::Error)]
pub enum HttpClientBuildError {
#[error("invalid {HTTP_PROXY_POLICY_ENV} value `{0}`; expected `system` or `disabled`")]
#[error("invalid {env} value `{0}`; expected `system` or `disabled`", env = EnvVars::FABRO_HTTP_PROXY_POLICY)]
InvalidProxyPolicy(String),
#[error(transparent)]

View file

@ -15,6 +15,7 @@ base64.workspace = true
ring = "0.17"
toml.workspace = true
fabro-config = { path = "../fabro-config" }
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-vault = { path = "../fabro-vault" }

View file

@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use fabro_config::{Storage, envfile};
use fabro_static::EnvVars;
use fabro_vault::{SecretType as VaultSecretType, Vault};
pub struct PendingSettingsWrite<'a> {
@ -16,8 +17,8 @@ pub struct PendingSettingsWrite<'a> {
}
pub const OBJECT_STORE_MANAGED_COMMENT: &str = "managed by fabro-install: object-store";
pub const OBJECT_STORE_ACCESS_KEY_ID_ENV: &str = "AWS_ACCESS_KEY_ID";
pub const OBJECT_STORE_SECRET_ACCESS_KEY_ENV: &str = "AWS_SECRET_ACCESS_KEY";
pub const OBJECT_STORE_ACCESS_KEY_ID_ENV: &str = EnvVars::AWS_ACCESS_KEY_ID;
pub const OBJECT_STORE_SECRET_ACCESS_KEY_ENV: &str = EnvVars::AWS_SECRET_ACCESS_KEY;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VaultSecretWrite {

View file

@ -35,6 +35,7 @@ tracing.workspace = true
fabro-http.workspace = true
fabro-auth = { path = "../fabro-auth" }
fabro-model = { path = "../fabro-model" }
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
[dev-dependencies]

View file

@ -1,6 +1,7 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use fabro_http::HeaderMap;
use fabro_static::EnvVars;
use tokio::{fs, time};
use tracing::warn;
@ -92,11 +93,15 @@ pub fn mime_from_extension(path: &str) -> &str {
///
/// # Errors
/// Returns an error if the file cannot be read.
#[expect(
clippy::disallowed_methods,
reason = "Attachment path expansion supports the conventional HOME env var."
)]
pub async fn load_file_as_base64(path: &str) -> Result<(String, String), std::io::Error> {
let expanded = path.strip_prefix("~/").map_or_else(
|| path.to_string(),
|rest| {
let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
let home = std::env::var(EnvVars::HOME).unwrap_or_else(|_| "/".to_string());
format!("{home}/{rest}")
},
);

View file

@ -1,7 +1,13 @@
#![expect(
clippy::disallowed_methods,
reason = "Live provider integration tests read required API keys from process env."
)]
use fabro_llm::error::ProviderErrorKind;
use fabro_llm::provider::ProviderAdapter;
use fabro_llm::providers::{AnthropicAdapter, GeminiAdapter, OpenAiAdapter};
use fabro_llm::types::{FinishReason, Message, Request};
use fabro_static::EnvVars;
fn make_request(model: &str) -> Request {
Request {
@ -24,7 +30,7 @@ fn make_request(model: &str) -> Request {
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
async fn anthropic_complete() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
let api_key = std::env::var(EnvVars::ANTHROPIC_API_KEY).expect("ANTHROPIC_API_KEY must be set");
let adapter = AnthropicAdapter::new(api_key);
let request = make_request("claude-haiku-4-5");
let response = adapter.complete(&request).await.unwrap();
@ -108,7 +114,7 @@ async fn openai_server_error() {
#[fabro_macros::e2e_test(live("GEMINI_API_KEY"))]
async fn gemini_complete() {
let api_key = std::env::var("GEMINI_API_KEY").expect("GEMINI_API_KEY must be set");
let api_key = std::env::var(EnvVars::GEMINI_API_KEY).expect("GEMINI_API_KEY must be set");
let adapter = GeminiAdapter::new(api_key);
let request = make_request("gemini-2.5-flash");
let response = adapter.complete(&request).await.unwrap();
@ -202,21 +208,21 @@ async fn run_multi_turn_cache_test(
#[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))]
async fn anthropic_multi_turn_cache() {
let api_key = std::env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY must be set");
let api_key = std::env::var(EnvVars::ANTHROPIC_API_KEY).expect("ANTHROPIC_API_KEY must be set");
let adapter = AnthropicAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "claude-haiku-4-5", 0.5).await;
}
#[fabro_macros::e2e_test(live("OPENAI_API_KEY"))]
async fn openai_multi_turn_cache() {
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set");
let api_key = std::env::var(EnvVars::OPENAI_API_KEY).expect("OPENAI_API_KEY must be set");
let adapter = OpenAiAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "gpt-4o-mini", 0.5).await;
}
#[fabro_macros::e2e_test(live("GEMINI_API_KEY"))]
async fn gemini_multi_turn_cache() {
let api_key = std::env::var("GEMINI_API_KEY").expect("GEMINI_API_KEY must be set");
let api_key = std::env::var(EnvVars::GEMINI_API_KEY).expect("GEMINI_API_KEY must be set");
let adapter = GeminiAdapter::new(api_key);
run_multi_turn_cache_test(&adapter, "gemini-2.5-flash", 0.5).await;
}

View file

@ -100,12 +100,21 @@ pub fn e2e_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let env_guards = if env_vars.is_empty() {
quote! {}
} else {
let env_lookup_helper = quote! {
#[expect(
clippy::disallowed_methods,
reason = "e2e_test live-mode guard intentionally checks process env for declared live secrets."
)]
fn __fabro_e2e_env_var_is_missing(name: &str) -> bool {
::std::env::var(name).is_err()
}
};
let guards = env_vars.iter().map(|env_var| {
let env_name = env_var.value();
let strict_message = format!("{env_name} not set (FABRO_TEST_MODE=strict)");
let skip_message = format!("skipping: {env_name} not set");
quote! {
if ::std::env::var(#env_var).is_err() {
if __fabro_e2e_env_var_is_missing(#env_var) {
if __mode == ::fabro_test::TestMode::Strict {
panic!(#strict_message);
}
@ -118,6 +127,8 @@ pub fn e2e_test(attr: TokenStream, item: TokenStream) -> TokenStream {
if has_twin {
// dual-mode: only check env vars when in live/strict
quote! {
#env_lookup_helper
if __mode.is_live() {
#(#guards)*
}
@ -125,6 +136,8 @@ pub fn e2e_test(attr: TokenStream, item: TokenStream) -> TokenStream {
} else {
// live-only: always check env vars (mode guard already skipped twin)
quote! {
#env_lookup_helper
#(#guards)*
}
}

View file

@ -13,6 +13,7 @@ doctest = false
workspace = true
[dependencies]
fabro-static.workspace = true
serde.workspace = true
serde_json.workspace = true
strum.workspace = true

View file

@ -1,3 +1,4 @@
use fabro_static::EnvVars;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
@ -55,13 +56,13 @@ impl Provider {
#[must_use]
pub fn api_key_env_vars(self) -> &'static [&'static str] {
match self {
Self::Anthropic => &["ANTHROPIC_API_KEY"],
Self::OpenAi => &["OPENAI_API_KEY"],
Self::Gemini => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
Self::Kimi => &["KIMI_API_KEY"],
Self::Zai => &["ZAI_API_KEY"],
Self::Minimax => &["MINIMAX_API_KEY"],
Self::Inception => &["INCEPTION_API_KEY"],
Self::Anthropic => &[EnvVars::ANTHROPIC_API_KEY],
Self::OpenAi => &[EnvVars::OPENAI_API_KEY],
Self::Gemini => &[EnvVars::GEMINI_API_KEY, EnvVars::GOOGLE_API_KEY],
Self::Kimi => &[EnvVars::KIMI_API_KEY],
Self::Zai => &[EnvVars::ZAI_API_KEY],
Self::Minimax => &[EnvVars::MINIMAX_API_KEY],
Self::Inception => &[EnvVars::INCEPTION_API_KEY],
Self::OpenAiCompatible => &[],
}
}
@ -69,6 +70,10 @@ impl Provider {
/// Returns `true` if at least one of the provider's API key env vars is
/// set.
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "Provider discovery intentionally checks the process env for known API-key names."
)]
pub fn has_api_key(self) -> bool {
self.api_key_env_vars()
.iter()

View file

@ -23,6 +23,7 @@ hex.workspace = true
tokio.workspace = true
tracing.workspace = true
axum.workspace = true
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
[dev-dependencies]

View file

@ -1,23 +1,27 @@
#![allow(
clippy::print_stdout,
clippy::print_stderr,
reason = "This example intentionally writes normal output and diagnostics to stdio."
clippy::disallowed_methods,
reason = "This example intentionally reads OAuth env vars and writes normal output/diagnostics."
)]
use std::env;
use fabro_oauth::run_browser_flow;
use fabro_static::EnvVars;
#[tokio::main]
async fn main() {
let issuer = env::var("OAUTH_ISSUER").expect("set OAUTH_ISSUER");
let client_id = env::var("OAUTH_CLIENT_ID").expect("set OAUTH_CLIENT_ID");
let scope = env::var("OAUTH_SCOPE").unwrap_or_else(|_| "openid profile email".to_string());
let port: u16 = env::var("OAUTH_PORT")
let issuer = env::var(EnvVars::OAUTH_ISSUER).expect("set OAUTH_ISSUER");
let client_id = env::var(EnvVars::OAUTH_CLIENT_ID).expect("set OAUTH_CLIENT_ID");
let scope =
env::var(EnvVars::OAUTH_SCOPE).unwrap_or_else(|_| "openid profile email".to_string());
let port: u16 = env::var(EnvVars::OAUTH_PORT)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(0);
let callback_path = env::var("OAUTH_CALLBACK_PATH").unwrap_or_else(|_| "/callback".to_string());
let callback_path =
env::var(EnvVars::OAUTH_CALLBACK_PATH).unwrap_or_else(|_| "/callback".to_string());
match run_browser_flow(&issuer, &client_id, &scope, port, &callback_path).await {
Ok(tokens) => {

View file

@ -1,3 +1,8 @@
#![allow(
clippy::disallowed_methods,
reason = "Build scripts run at compile time and read Cargo-provided env vars."
)]
fn main() {
println!("cargo:rerun-if-changed=c/capture_argv.c");

View file

@ -30,6 +30,7 @@ strum.workspace = true
tracing.workspace = true
base64.workspace = true
fabro-proc = { path = "../fabro-proc" }
fabro-static.workspace = true
shlex = "1"
# local

View file

@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use std::time::Instant;
use async_trait::async_trait;
use fabro_static::EnvVars;
use tokio::io::AsyncReadExt;
use tokio::process::{Child, Command};
use tokio::task::spawn_blocking;
@ -41,16 +42,16 @@ impl LocalSandbox {
}
const ENV_SAFELIST: &'static [&'static str] = &[
"PATH",
"HOME",
"USER",
"SHELL",
"LANG",
"TERM",
"TMPDIR",
"GOPATH",
"CARGO_HOME",
"NVM_DIR",
EnvVars::PATH,
EnvVars::HOME,
EnvVars::USER,
EnvVars::SHELL,
EnvVars::LANG,
EnvVars::TERM,
EnvVars::TMPDIR,
EnvVars::GOPATH,
EnvVars::CARGO_HOME,
EnvVars::NVM_DIR,
];
fn should_filter_env_var(key: &str) -> bool {
@ -74,13 +75,17 @@ impl LocalSandbox {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Local sandbox command execution checks PATH/PATHEXT to select optional helpers."
)]
fn binary_on_path(binary: &str) -> bool {
let Some(paths) = std::env::var_os("PATH") else {
let Some(paths) = std::env::var_os(EnvVars::PATH) else {
return false;
};
#[cfg(windows)]
let extensions: Vec<String> = std::env::var_os("PATHEXT")
let extensions: Vec<String> = std::env::var_os(EnvVars::PATHEXT)
.map(|value| {
value
.to_string_lossy()
@ -112,6 +117,14 @@ impl LocalSandbox {
}
}
#[expect(
clippy::disallowed_methods,
reason = "Local sandbox must snapshot the ambient process env before applying its fail-closed filter."
)]
fn process_env_vars() -> Vec<(String, String)> {
std::env::vars().collect()
}
#[async_trait]
impl Sandbox for LocalSandbox {
async fn read_file(
@ -221,7 +234,8 @@ impl Sandbox for LocalSandbox {
) -> Result<ExecResult, String> {
let start = Instant::now();
let mut filtered_env: Vec<(String, String)> = std::env::vars()
let mut filtered_env: Vec<(String, String)> = process_env_vars()
.into_iter()
.filter(|(key, _)| !Self::should_filter_env_var(key))
.collect();

View file

@ -36,6 +36,7 @@ fabro-api = { path = "../fabro-api" }
fabro-store = { path = "../fabro-store" }
fabro-vault = { path = "../fabro-vault" }
fabro-http.workspace = true
fabro-static.workspace = true
chrono.workspace = true
futures-util.workspace = true
axum.workspace = true

View file

@ -6,6 +6,7 @@ use fabro_auth::auth_issue_message;
use fabro_llm::client::Client as LlmClient;
use fabro_llm::types::{Message, Request};
use fabro_model::{Catalog, Provider};
use fabro_static::EnvVars;
use fabro_types::settings::server::GithubIntegrationStrategy;
use fabro_types::settings::{InterpString, ServerAuthMethod};
use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus};
@ -276,10 +277,14 @@ async fn check_github_app(state: &AppState) -> CheckResult {
.slug
.as_ref()
.map(InterpString::as_source);
let private_key_raw = state.server_secret("GITHUB_APP_PRIVATE_KEY");
let private_key_raw = state.server_secret(EnvVars::GITHUB_APP_PRIVATE_KEY);
let client_id = settings.server.integrations.github.client_id.is_some();
let client_secret = state.server_secret("GITHUB_APP_CLIENT_SECRET").is_some();
let webhook_secret = state.server_secret("GITHUB_APP_WEBHOOK_SECRET").is_some();
let client_secret = state
.server_secret(EnvVars::GITHUB_APP_CLIENT_SECRET)
.is_some();
let webhook_secret = state
.server_secret(EnvVars::GITHUB_APP_WEBHOOK_SECRET)
.is_some();
if app_id.is_none()
&& private_key_raw.is_none()
@ -317,7 +322,7 @@ async fn check_github_app(state: &AppState) -> CheckResult {
};
};
let private_key = match decode_pem_value("GITHUB_APP_PRIVATE_KEY", &private_key_raw) {
let private_key = match decode_pem_value(EnvVars::GITHUB_APP_PRIVATE_KEY, &private_key_raw) {
Ok(value) => value,
Err(err) => {
return CheckResult {
@ -378,7 +383,7 @@ async fn check_github_app(state: &AppState) -> CheckResult {
}
fn check_sandbox(state: &AppState) -> CheckResult {
if state.vault_or_env("DAYTONA_API_KEY").is_some() {
if state.vault_or_env(EnvVars::DAYTONA_API_KEY).is_some() {
CheckResult {
name: "Sandbox".to_string(),
status: CheckStatus::Pass,
@ -440,7 +445,7 @@ fn check_storage_dir_path(path: &std::path::Path) -> CheckResult {
}
async fn check_brave_search(state: &AppState) -> CheckResult {
let Some(api_key) = state.vault_or_env("BRAVE_SEARCH_API_KEY") else {
let Some(api_key) = state.vault_or_env(EnvVars::BRAVE_SEARCH_API_KEY) else {
return CheckResult {
name: "Web Search (Brave)".to_string(),
status: CheckStatus::Warning,
@ -507,7 +512,7 @@ fn check_crypto(state: &AppState) -> CheckResult {
let mut errors = Vec::new();
if resolved_server_settings.server.web.enabled {
match state.server_secret("SESSION_SECRET") {
match state.server_secret(EnvVars::SESSION_SECRET) {
Some(secret) => {
if let Err(err) = validate_session_secret(&secret) {
errors.push(err);
@ -519,7 +524,7 @@ fn check_crypto(state: &AppState) -> CheckResult {
let methods = &resolved_server_settings.server.auth.methods;
if methods.contains(&ServerAuthMethod::DevToken) {
match state.server_secret("FABRO_DEV_TOKEN") {
match state.server_secret(EnvVars::FABRO_DEV_TOKEN) {
Some(token) if validate_dev_token_format(&token) => {}
Some(_) => errors.push("FABRO_DEV_TOKEN has invalid format".to_string()),
None => errors.push("FABRO_DEV_TOKEN not set".to_string()),
@ -535,7 +540,10 @@ fn check_crypto(state: &AppState) -> CheckResult {
{
errors.push("server.integrations.github.client_id is not configured".to_string());
}
if state.server_secret("GITHUB_APP_CLIENT_SECRET").is_none() {
if state
.server_secret(EnvVars::GITHUB_APP_CLIENT_SECRET)
.is_none()
{
errors.push("GITHUB_APP_CLIENT_SECRET not set".to_string());
}
}

View file

@ -1,3 +1,4 @@
use fabro_static::EnvVars;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use tokio::process::Command;
@ -6,7 +7,7 @@ use tracing::{info, warn};
type HmacSha256 = Hmac<Sha256>;
/// Name of the server secret holding the GitHub App webhook HMAC key.
pub(crate) const WEBHOOK_SECRET_ENV: &str = "GITHUB_APP_WEBHOOK_SECRET";
pub(crate) const WEBHOOK_SECRET_ENV: &str = EnvVars::GITHUB_APP_WEBHOOK_SECRET;
/// Route path where Fabro receives GitHub App webhook deliveries.
pub(crate) const WEBHOOK_ROUTE: &str = "/api/v1/webhooks/github";

View file

@ -22,6 +22,7 @@ use fabro_install::{
write_github_app_settings, write_object_store_settings, write_token_settings,
};
use fabro_model::Provider;
use fabro_static::EnvVars;
use fabro_store::ArtifactStore;
use fabro_types::ServerSettings;
use fabro_types::settings::interp::InterpString;
@ -79,7 +80,6 @@ const DEFAULT_GEMINI_BASE_URL: &str = "https://generativelanguage.googleapis.com
const REDACTED_SECRET_VALUE: &str = "[REDACTED]";
const VALIDATION_TIMEOUT: Duration = Duration::from_secs(20);
const VALIDATION_CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
const AWS_SESSION_TOKEN_ENV: &str = "AWS_SESSION_TOKEN";
impl InstallAppState {
#[must_use]
@ -122,7 +122,7 @@ impl InstallAppState {
// reachability. Force the in-memory object store shortcut so
// /install/finish can't hang on an unreachable bucket.
unsafe {
std::env::set_var("FABRO_TEST_IN_MEMORY_STORE", "1");
std::env::set_var(EnvVars::FABRO_TEST_IN_MEMORY_STORE, "1");
}
Self {
install_token: Arc::from(token),
@ -921,7 +921,7 @@ fn install_object_store_lookup<'a>(
(Some(credentials), OBJECT_STORE_SECRET_ACCESS_KEY_ENV) => {
Some(credentials.secret_access_key.expose_secret().to_string())
}
(Some(_), AWS_SESSION_TOKEN_ENV) => None,
(Some(_), EnvVars::AWS_SESSION_TOKEN) => None,
_ => server_secrets.get(name),
}
}
@ -1320,7 +1320,7 @@ async fn post_install_finish(
return install_error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
}
vault_secrets.push(VaultSecretWrite {
name: "GITHUB_TOKEN".to_string(),
name: EnvVars::GITHUB_TOKEN.to_string(),
value: github.token,
secret_type: VaultSecretType::Environment,
description: None,
@ -1357,15 +1357,15 @@ async fn post_install_finish(
return install_error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string());
}
server_env_writes.push(make_env_write(
"GITHUB_APP_PRIVATE_KEY",
EnvVars::GITHUB_APP_PRIVATE_KEY,
BASE64_STANDARD.encode(github.pem.as_bytes()),
));
server_env_writes.push(make_env_write(
"GITHUB_APP_CLIENT_SECRET",
EnvVars::GITHUB_APP_CLIENT_SECRET,
github.client_secret,
));
if let Some(secret) = github.webhook_secret {
server_env_writes.push(make_env_write("GITHUB_APP_WEBHOOK_SECRET", secret));
server_env_writes.push(make_env_write(EnvVars::GITHUB_APP_WEBHOOK_SECRET, secret));
}
}
}
@ -1378,9 +1378,9 @@ async fn post_install_finish(
};
let session_secret = session_secret::generate_session_secret();
server_env_writes.push(make_env_write("SESSION_SECRET", session_secret));
server_env_writes.push(make_env_write(EnvVars::SESSION_SECRET, session_secret));
if let Some(token) = dev_token.as_ref() {
server_env_writes.push(make_env_write("FABRO_DEV_TOKEN", token.clone()));
server_env_writes.push(make_env_write(EnvVars::FABRO_DEV_TOKEN, token.clone()));
}
#[expect(
@ -1798,6 +1798,10 @@ async fn validate_llm_provider(
}
}
#[expect(
clippy::disallowed_methods,
reason = "Install flow checks documented provider base-url overrides while building defaults."
)]
fn provider_base_url(state: &InstallAppState, provider: Provider) -> String {
state
.upstreams
@ -1805,11 +1809,11 @@ fn provider_base_url(state: &InstallAppState, provider: Provider) -> String {
.get(&provider)
.cloned()
.or_else(|| match provider {
Provider::Anthropic => std::env::var("ANTHROPIC_BASE_URL").ok(),
Provider::OpenAi => std::env::var("OPENAI_BASE_URL").ok(),
Provider::Gemini => std::env::var("GEMINI_BASE_URL").ok(),
Provider::Anthropic => std::env::var(EnvVars::ANTHROPIC_BASE_URL).ok(),
Provider::OpenAi => std::env::var(EnvVars::OPENAI_BASE_URL).ok(),
Provider::Gemini => std::env::var(EnvVars::GEMINI_BASE_URL).ok(),
Provider::Kimi | Provider::Zai | Provider::Minimax | Provider::Inception => None,
Provider::OpenAiCompatible => std::env::var("OPENAI_COMPATIBLE_BASE_URL").ok(),
Provider::OpenAiCompatible => std::env::var(EnvVars::OPENAI_COMPATIBLE_BASE_URL).ok(),
})
.unwrap_or_else(|| match provider {
Provider::Anthropic => DEFAULT_ANTHROPIC_BASE_URL.to_string(),
@ -1956,13 +1960,14 @@ mod tests {
use axum::http::HeaderMap;
use fabro_install::{OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_SECRET_ACCESS_KEY_ENV};
use fabro_static::EnvVars;
use object_store::Error as ObjectStoreError;
use serde_json::json;
use super::{
AWS_SESSION_TOKEN_ENV, DEFAULT_INSTALL_GITHUB_API_BASE_URL, InstallAppState,
InstallAwsCredentialPair, InstallFinishGuard, InstallObjectStoreCredentialMode,
InstallObjectStoreInput, InstallObjectStoreProvider, PendingInstall, ServerSecrets,
DEFAULT_INSTALL_GITHUB_API_BASE_URL, InstallAppState, InstallAwsCredentialPair,
InstallFinishGuard, InstallObjectStoreCredentialMode, InstallObjectStoreInput,
InstallObjectStoreProvider, PendingInstall, ServerSecrets,
classify_object_store_validation_error, detect_canonical_url, install_object_store_lookup,
lock_unpoisoned, resolve_install_object_store_state, token_is_valid,
write_artifact_store_metadata,
@ -2145,9 +2150,9 @@ AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/fabro-web-identity-token\n",
lookup(OBJECT_STORE_SECRET_ACCESS_KEY_ENV).as_deref(),
Some("submitted-secret")
);
assert_eq!(lookup(AWS_SESSION_TOKEN_ENV), None);
assert_eq!(lookup(EnvVars::AWS_SESSION_TOKEN), None);
assert_eq!(
lookup("AWS_WEB_IDENTITY_TOKEN_FILE").as_deref(),
lookup(EnvVars::AWS_WEB_IDENTITY_TOKEN_FILE).as_deref(),
Some("/tmp/fabro-web-identity-token")
);
}

View file

@ -2,6 +2,7 @@ use anyhow::{Result, anyhow};
use axum::extract::FromRequestParts;
use axum::http::header;
use axum::http::request::Parts;
use fabro_static::EnvVars;
use fabro_types::settings::{ServerAuthMethod, ServerNamespace};
use fabro_types::{IdpIdentity, RunAuthMethod};
use fabro_util::dev_token::validate_dev_token_format;
@ -52,7 +53,15 @@ pub enum AuthMode {
}
pub fn resolve_auth_mode(settings: &ServerNamespace) -> Result<AuthMode> {
resolve_auth_mode_with_lookup(settings, |name| std::env::var(name).ok())
resolve_auth_mode_with_lookup(settings, process_env_var)
}
#[expect(
clippy::disallowed_methods,
reason = "Server auth startup validation intentionally reads process env for server secrets."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
pub fn resolve_auth_mode_with_lookup<F>(settings: &ServerNamespace, lookup: F) -> Result<AuthMode>
@ -78,13 +87,13 @@ where
"Fabro server refuses to start: github auth is enabled but server.integrations.github.client_id is not configured."
));
}
if github_enabled && lookup("GITHUB_APP_CLIENT_SECRET").is_none() {
if github_enabled && lookup(EnvVars::GITHUB_APP_CLIENT_SECRET).is_none() {
return Err(anyhow!(
"Fabro server refuses to start: github auth is enabled but GITHUB_APP_CLIENT_SECRET is not set."
));
}
let session_secret = lookup("SESSION_SECRET");
let session_secret = lookup(EnvVars::SESSION_SECRET);
let secret = session_secret.as_deref().ok_or_else(|| {
anyhow!("Fabro server refuses to start: auth is configured but SESSION_SECRET is not set.")
})?;
@ -93,7 +102,7 @@ where
}
let dev_token = if methods.contains(&ServerAuthMethod::DevToken) {
let token = lookup("FABRO_DEV_TOKEN").ok_or_else(|| {
let token = lookup(EnvVars::FABRO_DEV_TOKEN).ok_or_else(|| {
anyhow!(
"Fabro server refuses to start: dev-token auth is enabled but FABRO_DEV_TOKEN is not set."
)

View file

@ -31,6 +31,7 @@ use fabro_api::types::{
RunFilesMeta, RunFilesMetaDegradedReason, RunFilesMetaToSha,
};
use fabro_sandbox::reconnect::reconnect;
use fabro_static::EnvVars;
use fabro_types::RunId;
use fabro_workflow::sandbox_git::{
DiffError, RawDiffEntry, SubmoduleChange, SymlinkChange, list_binary_paths,
@ -587,7 +588,7 @@ async fn try_reconnect_run_sandbox(
let Some(record) = projection.sandbox.clone() else {
return Ok(None);
};
let daytona_api_key = state.vault_or_env_pub("DAYTONA_API_KEY");
let daytona_api_key = state.vault_or_env_pub(EnvVars::DAYTONA_API_KEY);
match reconnect(&record, daytona_api_key).await {
Ok(sandbox) => Ok(Some(sandbox)),
Err(_) => Ok(None),

View file

@ -18,6 +18,7 @@ use fabro_sandbox::config::{
};
use fabro_sandbox::daytona::DaytonaConfig;
use fabro_sandbox::{DockerSandboxOptions, Sandbox, SandboxProvider, SandboxSpec};
use fabro_static::EnvVars;
use fabro_types::settings::ServerNamespace;
use fabro_types::settings::cli::OutputVerbosity;
use fabro_types::settings::interp::InterpString;
@ -416,7 +417,7 @@ async fn build_preflight_report(
None
};
let daytona_api_key = state.vault_or_env("DAYTONA_API_KEY");
let daytona_api_key = state.vault_or_env(EnvVars::DAYTONA_API_KEY);
let sandbox_ok = run_sandbox_check(
&mut checks,
sandbox_provider,

View file

@ -12,6 +12,7 @@ use fabro_config::{
};
use fabro_install::{OBJECT_STORE_ACCESS_KEY_ID_ENV, OBJECT_STORE_SECRET_ACCESS_KEY_ENV};
use fabro_sandbox::SandboxProvider;
use fabro_static::EnvVars;
use fabro_types::ServerSettings;
use fabro_types::settings::server::{GithubIntegrationStrategy, WebhookStrategy};
use fabro_types::settings::{
@ -40,8 +41,6 @@ use crate::server::{
use crate::server_secrets::{ServerSecrets, process_env_snapshot};
use crate::startup::resolve_startup;
const TEST_IN_MEMORY_STORE_ENV: &str = "FABRO_TEST_IN_MEMORY_STORE";
const AWS_SESSION_TOKEN_ENV: &str = "AWS_SESSION_TOKEN";
pub const DEFAULT_TCP_PORT: u16 = 32276;
type EnvLookup = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
@ -340,9 +339,15 @@ async fn start_webhook_strategy(
}
}
#[expect(
clippy::disallowed_methods,
reason = "Test-only server object-store shortcut reads a documented Fabro env var."
)]
fn use_in_memory_store() -> bool {
!matches!(
std::env::var(TEST_IN_MEMORY_STORE_ENV).ok().as_deref(),
std::env::var(EnvVars::FABRO_TEST_IN_MEMORY_STORE)
.ok()
.as_deref(),
None | Some("" | "0" | "false" | "no")
)
}
@ -374,7 +379,7 @@ where
let access_key_id = env_lookup(OBJECT_STORE_ACCESS_KEY_ID_ENV);
let secret_access_key = env_lookup(OBJECT_STORE_SECRET_ACCESS_KEY_ENV);
let session_token = env_lookup(AWS_SESSION_TOKEN_ENV);
let session_token = env_lookup(EnvVars::AWS_SESSION_TOKEN);
match (access_key_id, secret_access_key) {
(Some(access_key_id), Some(secret_access_key)) => {
builder = builder
@ -394,26 +399,38 @@ where
for (name, key) in [
(
"AWS_WEB_IDENTITY_TOKEN_FILE",
EnvVars::AWS_WEB_IDENTITY_TOKEN_FILE,
AmazonS3ConfigKey::WebIdentityTokenFile,
),
("AWS_ROLE_ARN", AmazonS3ConfigKey::RoleArn),
("AWS_ROLE_SESSION_NAME", AmazonS3ConfigKey::RoleSessionName),
("AWS_ENDPOINT_URL_STS", AmazonS3ConfigKey::StsEndpoint),
(EnvVars::AWS_ROLE_ARN, AmazonS3ConfigKey::RoleArn),
(
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
EnvVars::AWS_ROLE_SESSION_NAME,
AmazonS3ConfigKey::RoleSessionName,
),
(
EnvVars::AWS_ENDPOINT_URL_STS,
AmazonS3ConfigKey::StsEndpoint,
),
(
EnvVars::AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
AmazonS3ConfigKey::ContainerCredentialsRelativeUri,
),
(
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
EnvVars::AWS_CONTAINER_CREDENTIALS_FULL_URI,
AmazonS3ConfigKey::ContainerCredentialsFullUri,
),
(
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
EnvVars::AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE,
AmazonS3ConfigKey::ContainerAuthorizationTokenFile,
),
("AWS_METADATA_ENDPOINT", AmazonS3ConfigKey::MetadataEndpoint),
("AWS_IMDSV1_FALLBACK", AmazonS3ConfigKey::ImdsV1Fallback),
(
EnvVars::AWS_METADATA_ENDPOINT,
AmazonS3ConfigKey::MetadataEndpoint,
),
(
EnvVars::AWS_IMDSV1_FALLBACK,
AmazonS3ConfigKey::ImdsV1Fallback,
),
] {
if let Some(value) = env_lookup(name) {
builder = builder.with_config(key, value);
@ -492,11 +509,19 @@ fn resolved_bind_request(
fn resolve_interp(value: &InterpString) -> anyhow::Result<String> {
value
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.map(|resolved| resolved.value)
.with_context(|| format!("failed to resolve {}", value.as_source()))
}
#[expect(
clippy::disallowed_methods,
reason = "Server settings interpolation owns a process-env lookup facade for {{ env.* }} values."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn resolve_interp_path(value: &InterpString) -> anyhow::Result<PathBuf> {
Ok(PathBuf::from(resolve_interp(value)?))
}
@ -638,7 +663,7 @@ where
&server_secrets,
)?;
let artifact_store = fabro_store::ArtifactStore::new(artifact_object_store, artifact_prefix);
let env_lookup: EnvLookup = Arc::new(|name| std::env::var(name).ok());
let env_lookup: EnvLookup = Arc::new(process_env_var);
resolve_canonical_origin(&resolved_server_settings, &env_lookup).map_err(anyhow::Error::msg)?;
let state = build_app_state(AppStateConfig {
resolved_settings: resolved_app_settings,

View file

@ -62,6 +62,7 @@ use fabro_slack::config::resolve_credentials as resolve_slack_credentials;
use fabro_slack::payload::SlackAnswerSubmission;
use fabro_slack::threads::ThreadRegistry;
use fabro_slack::{blocks as slack_blocks, connection as slack_connection};
use fabro_static::EnvVars;
use fabro_store::{
ArtifactStore, Database, EventEnvelope, EventPayload, PendingInterviewRecord, StageId,
};
@ -734,7 +735,7 @@ impl AppState {
}
pub(crate) fn vault_or_env(&self, name: &str) -> Option<String> {
std::env::var(name).ok().or_else(|| {
process_env_var(name).or_else(|| {
self.vault
.try_read()
.ok()
@ -774,7 +775,7 @@ impl AppState {
}
pub(crate) fn session_key(&self) -> Option<Key> {
self.server_secret("SESSION_SECRET")
self.server_secret(EnvVars::SESSION_SECRET)
.and_then(|value| auth::derive_cookie_key(value.as_bytes()).ok())
}
@ -787,11 +788,11 @@ impl AppState {
let Some(app_id) = settings.app_id.as_ref().map(InterpString::as_source) else {
return Ok(None);
};
let raw = self.server_secret("GITHUB_APP_PRIVATE_KEY");
let raw = self.server_secret(EnvVars::GITHUB_APP_PRIVATE_KEY);
let Some(raw) = raw else {
return Ok(None);
};
let private_key_pem = decode_secret_pem("GITHUB_APP_PRIVATE_KEY", &raw)?;
let private_key_pem = decode_secret_pem(EnvVars::GITHUB_APP_PRIVATE_KEY, &raw)?;
Ok(Some(fabro_github::GitHubCredentials::App(
fabro_github::GitHubAppCredentials {
app_id,
@ -801,8 +802,8 @@ impl AppState {
}
GithubIntegrationStrategy::Token => {
let token = self
.vault_or_env("GITHUB_TOKEN")
.or_else(|| self.vault_or_env("GH_TOKEN"))
.vault_or_env(EnvVars::GITHUB_TOKEN)
.or_else(|| self.vault_or_env(EnvVars::GH_TOKEN))
.as_deref()
.map(str::trim)
.filter(|token| !token.is_empty())
@ -870,11 +871,19 @@ fn decode_secret_pem(name: &str, raw: &str) -> Result<String, String> {
fn resolve_interp_string(value: &InterpString) -> anyhow::Result<String> {
value
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.map(|resolved| resolved.value)
.map_err(anyhow::Error::from)
}
#[expect(
clippy::disallowed_methods,
reason = "Server state owns process-env lookup facades for interpolation and vault fallbacks."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn start_optional_slack_service(state: &Arc<AppState>) {
let Some(service) = state.slack_service.clone() else {
return;
@ -2515,7 +2524,7 @@ pub fn create_app_state_with_runtime_settings_and_options(
server_settings,
manifest_run_defaults,
max_concurrent_runs,
|name| std::env::var(name).ok(),
process_env_var,
&HashMap::new(),
)
}
@ -2710,17 +2719,17 @@ pub fn create_app_state_with_store_and_runtime_settings(
}
fn default_env_lookup() -> EnvLookup {
Arc::new(|name| std::env::var(name).ok())
Arc::new(process_env_var)
}
fn load_test_server_secrets(path: PathBuf, env: HashMap<String, String>) -> ServerSecrets {
let mut env = env;
let file_has_session_secret = envfile::read_env_file(&path)
.ok()
.is_some_and(|entries| entries.contains_key("SESSION_SECRET"));
if !env.contains_key("SESSION_SECRET") && !file_has_session_secret {
.is_some_and(|entries| entries.contains_key(EnvVars::SESSION_SECRET));
if !env.contains_key(EnvVars::SESSION_SECRET) && !file_has_session_secret {
env.insert(
"SESSION_SECRET".to_string(),
EnvVars::SESSION_SECRET.to_string(),
"server-test-session-key-0123456789".to_string(),
);
}
@ -2731,7 +2740,7 @@ fn worker_token_keys_from_server_secrets(
server_secrets: &ServerSecrets,
) -> anyhow::Result<WorkerTokenKeys> {
let session_secret = server_secrets
.get("SESSION_SECRET")
.get(EnvVars::SESSION_SECRET)
.ok_or_else(|| jwt_auth::session_secret_key_error(&auth::KeyDeriveError::Empty))?;
WorkerTokenKeys::from_master_secret(session_secret.as_bytes())
.map_err(|err| jwt_auth::session_secret_key_error(&err))
@ -2772,7 +2781,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
.as_ref()
.map(|value| {
value
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.map(|resolved| resolved.value)
.map_err(anyhow::Error::from)
})
@ -3870,6 +3879,10 @@ async fn append_worker_exit_failure(
}
}
#[expect(
clippy::disallowed_methods,
reason = "Worker subprocess startup resolves Cargo's test binary env override when present."
)]
fn worker_command(
state: &AppState,
run_id: RunId,
@ -3877,7 +3890,7 @@ fn worker_command(
run_dir: &std::path::Path,
) -> anyhow::Result<Command> {
let current_exe = std::env::current_exe().context("reading current executable path")?;
let exe = std::env::var_os("CARGO_BIN_EXE_fabro").map_or(current_exe, PathBuf::from);
let exe = std::env::var_os(EnvVars::CARGO_BIN_EXE_FABRO).map_or(current_exe, PathBuf::from);
let storage_dir = state.server_storage_dir();
let runtime_directory = Storage::new(&storage_dir).runtime_directory();
let daemon = ServerDaemon::read(&runtime_directory)?.with_context(|| {
@ -3906,8 +3919,8 @@ fn worker_command(
.stderr(Stdio::piped());
apply_worker_env(&mut cmd);
cmd.env_remove("FABRO_WORKER_TOKEN");
cmd.env("FABRO_WORKER_TOKEN", worker_token);
cmd.env_remove(EnvVars::FABRO_WORKER_TOKEN);
cmd.env(EnvVars::FABRO_WORKER_TOKEN, worker_token);
#[cfg(unix)]
fabro_proc::pre_exec_setpgid(cmd.as_std_mut());
@ -4639,7 +4652,7 @@ async fn execute_run_in_process(state: Arc<AppState>, run_id: RunId) {
.iter()
.map(|(name, value)| {
let resolved = value
.resolve(|env| std::env::var(env).ok())
.resolve(process_env_var)
.map_or_else(|_| value.as_source(), |resolved| resolved.value);
(name.clone(), resolved)
})
@ -6594,7 +6607,7 @@ async fn reconnect_run_sandbox(
run_id: &RunId,
) -> Result<Box<dyn Sandbox>, Response> {
let record = load_run_sandbox_record(state, run_id).await?;
let daytona_api_key = state.vault_or_env("DAYTONA_API_KEY");
let daytona_api_key = state.vault_or_env(EnvVars::DAYTONA_API_KEY);
reconnect(&record, daytona_api_key)
.await
.map_err(|err| ApiError::new(StatusCode::CONFLICT, format!("{err}")).into_response())
@ -6619,7 +6632,7 @@ async fn reconnect_daytona_sandbox(
)
.into_response());
};
let daytona_api_key = state.vault_or_env("DAYTONA_API_KEY");
let daytona_api_key = state.vault_or_env(EnvVars::DAYTONA_API_KEY);
DaytonaSandbox::reconnect(name, daytona_api_key)
.await
.map_err(|err| ApiError::new(StatusCode::CONFLICT, err.clone()).into_response())
@ -7719,13 +7732,17 @@ async fn create_completion(
}
}
#[expect(
clippy::disallowed_methods,
reason = "Render-graph subprocess startup resolves Cargo's test binary env override when present."
)]
fn render_graph_subprocess_exe(
exe_override: Option<&std::path::Path>,
) -> Result<PathBuf, RenderSubprocessError> {
if let Some(path) = exe_override {
Ok(path.to_path_buf())
} else {
if let Some(path) = std::env::var_os("CARGO_BIN_EXE_fabro").map(PathBuf::from) {
if let Some(path) = std::env::var_os(EnvVars::CARGO_BIN_EXE_FABRO).map(PathBuf::from) {
return Ok(path);
}
@ -7789,7 +7806,7 @@ async fn render_dot_subprocess(
let mut cmd = Command::new(exe);
apply_render_graph_env(&mut cmd);
cmd.arg("__render-graph")
.env("FABRO_TELEMETRY", "off")
.env(EnvVars::FABRO_TELEMETRY, "off")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());

View file

@ -6,6 +6,10 @@ use fabro_config::envfile;
use fabro_llm::client::Client;
use fabro_model::Provider;
#[expect(
clippy::disallowed_methods,
reason = "ServerSecrets snapshots process env once at startup by design."
)]
pub fn process_env_snapshot() -> HashMap<String, String> {
std::env::vars().collect()
}

View file

@ -1,28 +1,35 @@
use std::ffi::OsString;
use fabro_static::EnvVars;
use tokio::process::Command;
const WORKER_ENV_ALLOWLIST: &[&str] = &[
"PATH",
"HOME",
"TMPDIR",
"USER",
"RUST_LOG",
"RUST_BACKTRACE",
"FABRO_HOME",
"FABRO_STORAGE_ROOT",
EnvVars::PATH,
EnvVars::HOME,
EnvVars::TMPDIR,
EnvVars::USER,
EnvVars::RUST_LOG,
EnvVars::RUST_BACKTRACE,
EnvVars::FABRO_HOME,
EnvVars::FABRO_STORAGE_ROOT,
];
const RENDER_GRAPH_ENV_ALLOWLIST: &[&str] = &["PATH", "HOME", "TMPDIR"];
const RENDER_GRAPH_ENV_ALLOWLIST: &[&str] = &[EnvVars::PATH, EnvVars::HOME, EnvVars::TMPDIR];
pub(crate) fn apply_worker_env(cmd: &mut Command) {
apply_allowlist(cmd, WORKER_ENV_ALLOWLIST, &|name| std::env::var_os(name));
apply_allowlist(cmd, WORKER_ENV_ALLOWLIST, &process_env_var_os);
}
pub(crate) fn apply_render_graph_env(cmd: &mut Command) {
apply_allowlist(cmd, RENDER_GRAPH_ENV_ALLOWLIST, &|name| {
std::env::var_os(name)
});
apply_allowlist(cmd, RENDER_GRAPH_ENV_ALLOWLIST, &process_env_var_os);
}
#[expect(
clippy::disallowed_methods,
reason = "Subprocess env allowlists intentionally copy a narrow process-env subset."
)]
fn process_env_var_os(name: &str) -> Option<OsString> {
std::env::var_os(name)
}
fn apply_allowlist(cmd: &mut Command, keys: &[&str], lookup: &dyn Fn(&str) -> Option<OsString>) {

View file

@ -29,6 +29,7 @@ mod tests {
use std::collections::HashMap;
use fabro_config::ServerSettingsBuilder;
use fabro_static::EnvVars;
use fabro_types::settings::ServerNamespace;
use super::validate_startup;
@ -56,11 +57,11 @@ methods = [{}]
let dir = tempfile::tempdir().unwrap();
let env = HashMap::from([
(
"SESSION_SECRET".to_string(),
EnvVars::SESSION_SECRET.to_string(),
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
),
(
"FABRO_DEV_TOKEN".to_string(),
EnvVars::FABRO_DEV_TOKEN.to_string(),
"fabro_dev_abababababababababababababababababababababababababababababababab"
.to_string(),
),

View file

@ -7,6 +7,7 @@ use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use cookie::time::Duration;
use cookie::{Cookie, CookieJar, Key, SameSite};
use fabro_static::EnvVars;
use fabro_types::settings::ServerAuthMethod;
use fabro_types::{IdpIdentity, RunAuthMethod};
use fabro_util::dev_token::validate_dev_token_format;
@ -293,10 +294,18 @@ fn session_cookie_secure(state: &AppState) -> bool {
.server
.web
.url
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.is_ok_and(|resolved| resolved.value.starts_with("https://"))
}
#[expect(
clippy::disallowed_methods,
reason = "Web auth resolves configured {{ env.* }} URLs through this process-env facade."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
async fn login_dev_token(
State(state): State<Arc<AppState>>,
Extension(auth_mode): Extension<AuthMode>,
@ -534,7 +543,7 @@ async fn callback_github(
);
}
};
let Some(client_secret) = state.server_secret("GITHUB_APP_CLIENT_SECRET") else {
let Some(client_secret) = state.server_secret(EnvVars::GITHUB_APP_CLIENT_SECRET) else {
error!("OAuth callback failed: GITHUB_APP_CLIENT_SECRET not configured");
return json_response(
StatusCode::CONFLICT,

View file

@ -16,6 +16,7 @@ workspace = true
fabro-interview = { path = "../fabro-interview" }
fabro-workflow = { path = "../fabro-workflow" }
fabro-http.workspace = true
fabro-static.workspace = true
futures-util.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -1,3 +1,4 @@
use fabro_static::EnvVars;
use serde_json::{Value, json};
use tracing::debug;
@ -17,9 +18,13 @@ pub struct SlackClient {
}
impl SlackClient {
#[expect(
clippy::disallowed_methods,
reason = "Slack client supports a documented process-env API base URL override."
)]
pub fn new(bot_token: String) -> Self {
let api_base =
std::env::var("SLACK_BASE_URL").unwrap_or_else(|_| SLACK_API_BASE.to_string());
std::env::var(EnvVars::SLACK_BASE_URL).unwrap_or_else(|_| SLACK_API_BASE.to_string());
Self {
bot_token,
api_base,

View file

@ -1,3 +1,4 @@
use fabro_static::EnvVars;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
@ -11,13 +12,17 @@ pub struct SlackCredentials {
pub app_token: String,
}
#[expect(
clippy::disallowed_methods,
reason = "Slack credential resolution intentionally reads documented token env vars."
)]
fn non_empty_env(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|s| !s.is_empty())
}
pub fn resolve_credentials() -> Option<SlackCredentials> {
let bot_token = non_empty_env("FABRO_SLACK_BOT_TOKEN")?;
let app_token = non_empty_env("FABRO_SLACK_APP_TOKEN")?;
let bot_token = non_empty_env(EnvVars::FABRO_SLACK_BOT_TOKEN)?;
let app_token = non_empty_env(EnvVars::FABRO_SLACK_APP_TOKEN)?;
Some(SlackCredentials {
bot_token,
app_token,

View file

@ -1,5 +1,6 @@
use std::sync::Arc;
use fabro_static::EnvVars;
use futures_util::{SinkExt, StreamExt};
use tokio::time::sleep;
use tokio_tungstenite::tungstenite::Message;
@ -56,6 +57,10 @@ pub fn process_message(
}
/// Fetch a WebSocket URL from Slack's `apps.connections.open` endpoint.
#[expect(
clippy::disallowed_methods,
reason = "Slack socket setup supports a documented process-env API base URL override."
)]
pub async fn open_socket_url(
http: &fabro_http::HttpClient,
app_token: &str,
@ -63,7 +68,8 @@ pub async fn open_socket_url(
let resp = http
.post(format!(
"{}/apps.connections.open",
std::env::var("SLACK_BASE_URL").unwrap_or_else(|_| "https://slack.com/api".to_string())
std::env::var(EnvVars::SLACK_BASE_URL)
.unwrap_or_else(|_| "https://slack.com/api".to_string())
))
.bearer_auth(app_token)
.header("Content-Type", "application/x-www-form-urlencoded")

View file

@ -0,0 +1,13 @@
[package]
name = "fabro-static"
edition.workspace = true
version.workspace = true
publish = false
license.workspace = true
description = "Static string registries for shared Fabro conventions"
[lib]
doctest = false
[lints]
workspace = true

View file

@ -0,0 +1,256 @@
//! Fixed process environment variable names used or supported by Fabro.
/// Declares the environment variable names used throughout Fabro and its
/// crates.
pub struct EnvVars;
impl EnvVars {
// Fabro core
pub const FABRO_AUTH_FILE: &'static str = "FABRO_AUTH_FILE";
pub const FABRO_BUILD_DATE: &'static str = "FABRO_BUILD_DATE";
pub const FABRO_BUILD_PROFILE: &'static str = "FABRO_BUILD_PROFILE";
pub const FABRO_BUILD_PROFILE_SUFFIX: &'static str = "FABRO_BUILD_PROFILE_SUFFIX";
pub const FABRO_CONFIG: &'static str = "FABRO_CONFIG";
pub const FABRO_DEBUG: &'static str = "FABRO_DEBUG";
pub const FABRO_DEV_TOKEN: &'static str = "FABRO_DEV_TOKEN";
pub const FABRO_ENABLE_FETCH_FEATURE_OCI_INTEGRATION: &'static str =
"FABRO_ENABLE_FETCH_FEATURE_OCI_INTEGRATION";
pub const FABRO_GIT_SHA: &'static str = "FABRO_GIT_SHA";
pub const FABRO_HOME: &'static str = "FABRO_HOME";
pub const FABRO_HTTP_PROXY_POLICY: &'static str = "FABRO_HTTP_PROXY_POLICY";
pub const FABRO_JSON: &'static str = "FABRO_JSON";
pub const FABRO_NO_UPGRADE_CHECK: &'static str = "FABRO_NO_UPGRADE_CHECK";
pub const FABRO_QUIET: &'static str = "FABRO_QUIET";
pub const FABRO_SERVER: &'static str = "FABRO_SERVER";
pub const FABRO_SERVER_MAX_CONCURRENT_RUNS: &'static str = "FABRO_SERVER_MAX_CONCURRENT_RUNS";
pub const FABRO_SLACK_APP_TOKEN: &'static str = "FABRO_SLACK_APP_TOKEN";
pub const FABRO_SLACK_BOT_TOKEN: &'static str = "FABRO_SLACK_BOT_TOKEN";
pub const FABRO_STORAGE_DIR: &'static str = "FABRO_STORAGE_DIR";
pub const FABRO_STORAGE_ROOT: &'static str = "FABRO_STORAGE_ROOT";
pub const FABRO_SUPPRESS_OPEN_BROWSER: &'static str = "FABRO_SUPPRESS_OPEN_BROWSER";
pub const FABRO_TELEMETRY: &'static str = "FABRO_TELEMETRY";
pub const FABRO_TEST_IN_MEMORY_STORE: &'static str = "FABRO_TEST_IN_MEMORY_STORE";
pub const FABRO_TEST_MODE: &'static str = "FABRO_TEST_MODE";
pub const FABRO_VERBOSE: &'static str = "FABRO_VERBOSE";
pub const FABRO_WEB_URL: &'static str = "FABRO_WEB_URL";
pub const FABRO_WORKER_TOKEN: &'static str = "FABRO_WORKER_TOKEN";
// LLM providers and tool integrations
pub const ANTHROPIC_API_KEY: &'static str = "ANTHROPIC_API_KEY";
pub const ANTHROPIC_BASE_URL: &'static str = "ANTHROPIC_BASE_URL";
pub const BRAVE_SEARCH_API_KEY: &'static str = "BRAVE_SEARCH_API_KEY";
pub const CHATGPT_ACCOUNT_ID: &'static str = "CHATGPT_ACCOUNT_ID";
pub const GEMINI_API_KEY: &'static str = "GEMINI_API_KEY";
pub const GEMINI_BASE_URL: &'static str = "GEMINI_BASE_URL";
pub const GOOGLE_API_KEY: &'static str = "GOOGLE_API_KEY";
pub const GOPATH: &'static str = "GOPATH";
pub const INCEPTION_API_KEY: &'static str = "INCEPTION_API_KEY";
pub const KIMI_API_KEY: &'static str = "KIMI_API_KEY";
pub const MINIMAX_API_KEY: &'static str = "MINIMAX_API_KEY";
pub const OPENAI_API_KEY: &'static str = "OPENAI_API_KEY";
pub const OPENAI_BASE_URL: &'static str = "OPENAI_BASE_URL";
pub const OPENAI_COMPATIBLE_BASE_URL: &'static str = "OPENAI_COMPATIBLE_BASE_URL";
pub const OPENAI_ORGANIZATION: &'static str = "OPENAI_ORGANIZATION";
pub const OPENAI_PROJECT: &'static str = "OPENAI_PROJECT";
pub const OPENAI_ORG_ID: &'static str = "OPENAI_ORG_ID";
pub const OPENAI_PROJECT_ID: &'static str = "OPENAI_PROJECT_ID";
pub const ZAI_API_KEY: &'static str = "ZAI_API_KEY";
// GitHub, OAuth, and Slack
pub const GH_TOKEN: &'static str = "GH_TOKEN";
pub const GITHUB_APP_CLIENT_SECRET: &'static str = "GITHUB_APP_CLIENT_SECRET";
pub const GITHUB_APP_PRIVATE_KEY: &'static str = "GITHUB_APP_PRIVATE_KEY";
pub const GITHUB_APP_WEBHOOK_SECRET: &'static str = "GITHUB_APP_WEBHOOK_SECRET";
pub const GITHUB_BASE_URL: &'static str = "GITHUB_BASE_URL";
pub const GITHUB_TOKEN: &'static str = "GITHUB_TOKEN";
pub const OAUTH_CALLBACK_PATH: &'static str = "OAUTH_CALLBACK_PATH";
pub const OAUTH_CLIENT_ID: &'static str = "OAUTH_CLIENT_ID";
pub const OAUTH_ISSUER: &'static str = "OAUTH_ISSUER";
pub const OAUTH_PORT: &'static str = "OAUTH_PORT";
pub const OAUTH_SCOPE: &'static str = "OAUTH_SCOPE";
pub const SLACK_BASE_URL: &'static str = "SLACK_BASE_URL";
// Server, sandbox, and cloud provider integration
pub const AWS_ACCESS_KEY_ID: &'static str = "AWS_ACCESS_KEY_ID";
pub const AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: &'static str =
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE";
pub const AWS_CONTAINER_CREDENTIALS_FULL_URI: &'static str =
"AWS_CONTAINER_CREDENTIALS_FULL_URI";
pub const AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: &'static str =
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
pub const AWS_ENDPOINT: &'static str = "AWS_ENDPOINT";
pub const AWS_ENDPOINT_URL_S3: &'static str = "AWS_ENDPOINT_URL_S3";
pub const AWS_ENDPOINT_URL_STS: &'static str = "AWS_ENDPOINT_URL_STS";
pub const AWS_IMDSV1_FALLBACK: &'static str = "AWS_IMDSV1_FALLBACK";
pub const AWS_METADATA_ENDPOINT: &'static str = "AWS_METADATA_ENDPOINT";
pub const AWS_ROLE_ARN: &'static str = "AWS_ROLE_ARN";
pub const AWS_ROLE_SESSION_NAME: &'static str = "AWS_ROLE_SESSION_NAME";
pub const AWS_SECRET_ACCESS_KEY: &'static str = "AWS_SECRET_ACCESS_KEY";
pub const AWS_SESSION_TOKEN: &'static str = "AWS_SESSION_TOKEN";
pub const AWS_WEB_IDENTITY_TOKEN_FILE: &'static str = "AWS_WEB_IDENTITY_TOKEN_FILE";
pub const DAYTONA_API_KEY: &'static str = "DAYTONA_API_KEY";
pub const DAYTONA_API_URL: &'static str = "DAYTONA_API_URL";
pub const DAYTONA_SERVER_URL: &'static str = "DAYTONA_SERVER_URL";
pub const SESSION_SECRET: &'static str = "SESSION_SECRET";
// Platform and test harness
pub const CARGO_BIN_EXE_FABRO: &'static str = "CARGO_BIN_EXE_fabro";
pub const CARGO_CFG_TARGET_OS: &'static str = "CARGO_CFG_TARGET_OS";
pub const CARGO_HOME: &'static str = "CARGO_HOME";
pub const CARGO_MANIFEST_DIR: &'static str = "CARGO_MANIFEST_DIR";
pub const CI: &'static str = "CI";
pub const HOME: &'static str = "HOME";
pub const KUBERNETES_SERVICE_HOST: &'static str = "KUBERNETES_SERVICE_HOST";
pub const LANG: &'static str = "LANG";
pub const LLVM_PROFILE_FILE: &'static str = "LLVM_PROFILE_FILE";
pub const NEXTEST_PROFILE: &'static str = "NEXTEST_PROFILE";
pub const NEXTEST_RUN_ID: &'static str = "NEXTEST_RUN_ID";
pub const NO_COLOR: &'static str = "NO_COLOR";
pub const NVM_DIR: &'static str = "NVM_DIR";
pub const OUT_DIR: &'static str = "OUT_DIR";
pub const PATH: &'static str = "PATH";
pub const PATHEXT: &'static str = "PATHEXT";
pub const PROFILE: &'static str = "PROFILE";
pub const RAILWAY_ENVIRONMENT: &'static str = "RAILWAY_ENVIRONMENT";
pub const RAILWAY_PUBLIC_DOMAIN: &'static str = "RAILWAY_PUBLIC_DOMAIN";
pub const RUST_BACKTRACE: &'static str = "RUST_BACKTRACE";
pub const RUST_LOG: &'static str = "RUST_LOG";
pub const SHELL: &'static str = "SHELL";
pub const TERM: &'static str = "TERM";
pub const TMPDIR: &'static str = "TMPDIR";
pub const TWIN_OPENAI_BIND_ADDR: &'static str = "TWIN_OPENAI_BIND_ADDR";
pub const TWIN_OPENAI_ENABLE_ADMIN: &'static str = "TWIN_OPENAI_ENABLE_ADMIN";
pub const TWIN_OPENAI_LIVE_BASE_URL: &'static str = "TWIN_OPENAI_LIVE_BASE_URL";
pub const TWIN_OPENAI_LIVE_MODEL: &'static str = "TWIN_OPENAI_LIVE_MODEL";
pub const TWIN_OPENAI_REQUIRE_AUTH: &'static str = "TWIN_OPENAI_REQUIRE_AUTH";
pub const USER: &'static str = "USER";
pub const ZDOTDIR: &'static str = "ZDOTDIR";
}
#[cfg(test)]
mod tests {
use super::EnvVars;
#[test]
fn env_var_constants_match_their_names() {
assert_eq!(EnvVars::FABRO_CONFIG, "FABRO_CONFIG");
}
#[test]
fn env_var_constants_are_non_empty_and_single_tokens() {
let values = [
EnvVars::FABRO_AUTH_FILE,
EnvVars::FABRO_BUILD_DATE,
EnvVars::FABRO_BUILD_PROFILE,
EnvVars::FABRO_BUILD_PROFILE_SUFFIX,
EnvVars::FABRO_CONFIG,
EnvVars::FABRO_DEBUG,
EnvVars::FABRO_DEV_TOKEN,
EnvVars::FABRO_ENABLE_FETCH_FEATURE_OCI_INTEGRATION,
EnvVars::FABRO_GIT_SHA,
EnvVars::FABRO_HOME,
EnvVars::FABRO_HTTP_PROXY_POLICY,
EnvVars::FABRO_JSON,
EnvVars::FABRO_NO_UPGRADE_CHECK,
EnvVars::FABRO_QUIET,
EnvVars::FABRO_SERVER,
EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS,
EnvVars::FABRO_SLACK_APP_TOKEN,
EnvVars::FABRO_SLACK_BOT_TOKEN,
EnvVars::FABRO_STORAGE_DIR,
EnvVars::FABRO_STORAGE_ROOT,
EnvVars::FABRO_SUPPRESS_OPEN_BROWSER,
EnvVars::FABRO_TELEMETRY,
EnvVars::FABRO_TEST_IN_MEMORY_STORE,
EnvVars::FABRO_TEST_MODE,
EnvVars::FABRO_VERBOSE,
EnvVars::FABRO_WEB_URL,
EnvVars::FABRO_WORKER_TOKEN,
EnvVars::ANTHROPIC_API_KEY,
EnvVars::ANTHROPIC_BASE_URL,
EnvVars::BRAVE_SEARCH_API_KEY,
EnvVars::CHATGPT_ACCOUNT_ID,
EnvVars::GEMINI_API_KEY,
EnvVars::GEMINI_BASE_URL,
EnvVars::GOOGLE_API_KEY,
EnvVars::GOPATH,
EnvVars::INCEPTION_API_KEY,
EnvVars::KIMI_API_KEY,
EnvVars::MINIMAX_API_KEY,
EnvVars::OPENAI_API_KEY,
EnvVars::OPENAI_BASE_URL,
EnvVars::OPENAI_COMPATIBLE_BASE_URL,
EnvVars::OPENAI_ORGANIZATION,
EnvVars::OPENAI_PROJECT,
EnvVars::OPENAI_ORG_ID,
EnvVars::OPENAI_PROJECT_ID,
EnvVars::ZAI_API_KEY,
EnvVars::GH_TOKEN,
EnvVars::GITHUB_APP_CLIENT_SECRET,
EnvVars::GITHUB_APP_PRIVATE_KEY,
EnvVars::GITHUB_APP_WEBHOOK_SECRET,
EnvVars::GITHUB_BASE_URL,
EnvVars::GITHUB_TOKEN,
EnvVars::OAUTH_CALLBACK_PATH,
EnvVars::OAUTH_CLIENT_ID,
EnvVars::OAUTH_ISSUER,
EnvVars::OAUTH_PORT,
EnvVars::OAUTH_SCOPE,
EnvVars::SLACK_BASE_URL,
EnvVars::AWS_ACCESS_KEY_ID,
EnvVars::AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE,
EnvVars::AWS_CONTAINER_CREDENTIALS_FULL_URI,
EnvVars::AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
EnvVars::AWS_ENDPOINT,
EnvVars::AWS_ENDPOINT_URL_S3,
EnvVars::AWS_ENDPOINT_URL_STS,
EnvVars::AWS_IMDSV1_FALLBACK,
EnvVars::AWS_METADATA_ENDPOINT,
EnvVars::AWS_ROLE_ARN,
EnvVars::AWS_ROLE_SESSION_NAME,
EnvVars::AWS_SECRET_ACCESS_KEY,
EnvVars::AWS_SESSION_TOKEN,
EnvVars::AWS_WEB_IDENTITY_TOKEN_FILE,
EnvVars::DAYTONA_API_KEY,
EnvVars::DAYTONA_API_URL,
EnvVars::DAYTONA_SERVER_URL,
EnvVars::SESSION_SECRET,
EnvVars::CARGO_BIN_EXE_FABRO,
EnvVars::CARGO_CFG_TARGET_OS,
EnvVars::CARGO_HOME,
EnvVars::CARGO_MANIFEST_DIR,
EnvVars::CI,
EnvVars::HOME,
EnvVars::KUBERNETES_SERVICE_HOST,
EnvVars::LANG,
EnvVars::LLVM_PROFILE_FILE,
EnvVars::NEXTEST_PROFILE,
EnvVars::NEXTEST_RUN_ID,
EnvVars::NO_COLOR,
EnvVars::NVM_DIR,
EnvVars::OUT_DIR,
EnvVars::PATH,
EnvVars::PATHEXT,
EnvVars::PROFILE,
EnvVars::RAILWAY_ENVIRONMENT,
EnvVars::RAILWAY_PUBLIC_DOMAIN,
EnvVars::RUST_BACKTRACE,
EnvVars::RUST_LOG,
EnvVars::SHELL,
EnvVars::TERM,
EnvVars::TMPDIR,
EnvVars::TWIN_OPENAI_BIND_ADDR,
EnvVars::TWIN_OPENAI_ENABLE_ADMIN,
EnvVars::TWIN_OPENAI_LIVE_BASE_URL,
EnvVars::TWIN_OPENAI_LIVE_MODEL,
EnvVars::TWIN_OPENAI_REQUIRE_AUTH,
EnvVars::USER,
EnvVars::ZDOTDIR,
];
for value in values {
assert!(!value.is_empty());
assert!(!value.chars().any(char::is_whitespace));
}
}
}

View file

@ -0,0 +1,8 @@
#![allow(
clippy::disallowed_methods,
reason = "This crate owns the process environment variable name registry."
)]
mod env_vars;
pub use env_vars::EnvVars;

View file

@ -19,6 +19,7 @@ chrono.workspace = true
dirs.workspace = true
exec.workspace = true
fabro-http.workspace = true
fabro-static.workspace = true
fabro-util = { path = "../fabro-util" }
fork.workspace = true
git2.workspace = true

View file

@ -1,3 +1,4 @@
use fabro_static::EnvVars;
use serde_json::{Value, json};
pub fn build_context() -> Value {
@ -9,8 +10,12 @@ pub fn build_context() -> Value {
})
}
#[expect(
clippy::disallowed_methods,
reason = "Telemetry context captures the conventional LANG locale from process env."
)]
fn current_locale() -> String {
let lang = std::env::var("LANG").unwrap_or_default();
let lang = std::env::var(EnvVars::LANG).unwrap_or_default();
parse_locale(&lang)
}

View file

@ -13,6 +13,7 @@ use std::thread::JoinHandle;
use chrono::Utc;
use event::{Track, User};
use fabro_static::EnvVars;
use serde_json::Value;
use uuid::Uuid;
@ -186,8 +187,12 @@ macro_rules! track {
};
}
#[expect(
clippy::disallowed_methods,
reason = "Telemetry initialization reads the documented FABRO_TELEMETRY process-env control."
)]
pub fn telemetry_level() -> TelemetryLevel {
telemetry_level_from(std::env::var("FABRO_TELEMETRY").ok().as_deref())
telemetry_level_from(std::env::var(EnvVars::FABRO_TELEMETRY).ok().as_deref())
}
pub fn telemetry_level_from(env_value: Option<&str>) -> TelemetryLevel {

View file

@ -3,6 +3,8 @@
reason = "sync pre-fork filesystem interaction; the whole module runs before fork/exec"
)]
use fabro_static::EnvVars;
/// Spawn a fully detached subprocess that survives parent exit and terminal
/// close.
///
@ -150,8 +152,8 @@ pub fn spawn_fabro_subcommand(subcommand: &str, filename: &str, json: &[u8]) {
spawn_detached(
&[&exe, subcommand, &path_str],
&[("FABRO_TELEMETRY", "off")],
&["FABRO_JSON"],
&[(EnvVars::FABRO_TELEMETRY, "off")],
&[EnvVars::FABRO_JSON],
);
}

View file

@ -17,6 +17,7 @@ assert_cmd = "2"
axum = { workspace = true }
fabro-config = { path = "../fabro-config" }
fabro-proc = { path = "../fabro-proc" }
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-util = { path = "../fabro-util" }
fabro-http.workspace = true

View file

@ -1,8 +1,8 @@
#![expect(
clippy::disallowed_methods,
reason = "fabro-test: shared test infrastructure; sync std::fs throughout is intentional for \
test fixtures, snapshots, and scratch directories. Tokio-path code under test sits \
in other crates."
test fixtures, snapshots, scratch directories, and process-env harnessing. \
Tokio-path code under test sits in other crates."
)]
use std::collections::HashMap;
@ -15,8 +15,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use assert_cmd::Command;
use fabro_config::daemon::ServerDaemon;
use fabro_config::{RuntimeDirectory, Storage, envfile};
pub use fabro_static::EnvVars;
use fabro_types::RunId;
use fabro_util::browser;
use regex::Regex;
use serde_json::{Map, Value, json};
use toml::Value as TomlValue;
@ -39,8 +39,8 @@ pub use http_assert::{
#[macro_export]
macro_rules! preserve_coverage_env {
($cmd:expr) => {{
if let Some(val) = ::std::env::var_os("LLVM_PROFILE_FILE") {
$cmd.env("LLVM_PROFILE_FILE", val);
if let Some(val) = ::std::env::var_os($crate::EnvVars::LLVM_PROFILE_FILE) {
$cmd.env($crate::EnvVars::LLVM_PROFILE_FILE, val);
}
}};
}
@ -77,7 +77,6 @@ static INSTA_FILTERS: &[(&str, &str)] = &[
];
const MANAGED_STORAGE_MARKER: &str = "# fabro-test managed storage_dir";
const TEST_IN_MEMORY_STORE_ENV: &str = "FABRO_TEST_IN_MEMORY_STORE";
const SESSION_LOCK_TIMEOUT: Duration = Duration::from_secs(20);
const TEST_SESSION_SECRET: &str =
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
@ -95,10 +94,10 @@ pub enum TestMode {
impl TestMode {
#[must_use]
pub fn from_env() -> Self {
match std::env::var("FABRO_TEST_MODE").as_deref() {
match std::env::var(EnvVars::FABRO_TEST_MODE).as_deref() {
Ok("live") => Self::Live,
Ok("strict") => Self::Strict,
_ => match std::env::var("NEXTEST_PROFILE").as_deref() {
_ => match std::env::var(EnvVars::NEXTEST_PROFILE).as_deref() {
Ok("e2e") => Self::Strict,
_ => Self::Twin,
},
@ -157,20 +156,20 @@ fn apply_test_isolation_with_lookup(
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
) {
cmd.env_clear();
if let Some(coverage) = lookup("LLVM_PROFILE_FILE") {
cmd.env("LLVM_PROFILE_FILE", coverage);
if let Some(coverage) = lookup(EnvVars::LLVM_PROFILE_FILE) {
cmd.env(EnvVars::LLVM_PROFILE_FILE, coverage);
}
if let Some(path) = lookup("PATH") {
cmd.env("PATH", path);
if let Some(path) = lookup(EnvVars::PATH) {
cmd.env(EnvVars::PATH, path);
}
cmd.env("NO_COLOR", "1");
cmd.env("HOME", home_dir);
cmd.env("FABRO_NO_UPGRADE_CHECK", "true")
.env("FABRO_HTTP_PROXY_POLICY", "disabled")
.env("FABRO_TELEMETRY", "off")
.env(browser::SUPPRESS_ENV_VAR, "1");
cmd.env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64");
cmd.env(TEST_IN_MEMORY_STORE_ENV, "1");
cmd.env(EnvVars::NO_COLOR, "1");
cmd.env(EnvVars::HOME, home_dir);
cmd.env(EnvVars::FABRO_NO_UPGRADE_CHECK, "true")
.env(EnvVars::FABRO_HTTP_PROXY_POLICY, "disabled")
.env(EnvVars::FABRO_TELEMETRY, "off")
.env(EnvVars::FABRO_SUPPRESS_OPEN_BROWSER, "1");
cmd.env(EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS, "64");
cmd.env(EnvVars::FABRO_TEST_IN_MEMORY_STORE, "1");
}
/// Create a fresh tempdir containing an empty `storage/` subdirectory, for
@ -355,7 +354,7 @@ fn current_pid() -> u32 {
}
fn session_paths() -> (SessionMode, String, SessionPaths) {
let run_id = std::env::var("NEXTEST_RUN_ID").ok();
let run_id = std::env::var(EnvVars::NEXTEST_RUN_ID).ok();
session_paths_for_run_id(run_id.as_deref())
}
@ -1778,8 +1777,8 @@ impl TwinGitHub {
impl TwinOpenAi {
pub fn configure_command(&self, cmd: &mut Command, namespace: &str) {
cmd.env("OPENAI_BASE_URL", &self.base_url);
cmd.env("OPENAI_API_KEY", namespace);
cmd.env(EnvVars::OPENAI_BASE_URL, &self.base_url);
cmd.env(EnvVars::OPENAI_API_KEY, namespace);
}
#[must_use]
@ -2077,9 +2076,9 @@ macro_rules! e2e_openai {
let api_key = format!("{}::{}", module_path!(), line!());
(twin.base_url.clone(), api_key)
} else {
let base_url = std::env::var("OPENAI_BASE_URL")
let base_url = std::env::var($crate::EnvVars::OPENAI_BASE_URL)
.unwrap_or_else(|_| "https://api.openai.com/v1".to_string());
let api_key = std::env::var("OPENAI_API_KEY")
let api_key = std::env::var($crate::EnvVars::OPENAI_API_KEY)
.expect("OPENAI_API_KEY must be set in live/strict mode");
(base_url, api_key)
}
@ -2108,11 +2107,11 @@ mod tests {
let envs = cmd.get_envs().collect::<Vec<_>>();
assert!(envs.iter().any(|(key, value)| {
*key == std::ffi::OsStr::new("OPENAI_BASE_URL")
*key == std::ffi::OsStr::new(EnvVars::OPENAI_BASE_URL)
&& *value == Some(std::ffi::OsStr::new("http://127.0.0.1:3000/v1"))
}),);
assert!(envs.iter().any(|(key, value)| {
*key == std::ffi::OsStr::new("OPENAI_API_KEY")
*key == std::ffi::OsStr::new(EnvVars::OPENAI_API_KEY)
&& *value == Some(std::ffi::OsStr::new("test-namespace"))
}),);
}
@ -2127,10 +2126,12 @@ mod tests {
let home = tempfile::tempdir().expect("temp home should be created");
let mut cmd = std::process::Command::new("/usr/bin/env");
apply_test_isolation_with_lookup(&mut cmd, home.path(), |name| match name {
"PATH" => Some(std::ffi::OsString::from("/usr/bin:/bin")),
"LLVM_PROFILE_FILE" => Some(std::ffi::OsString::from("/tmp/coverage.profraw")),
"GITHUB_TOKEN" => Some(std::ffi::OsString::from("sentinel-should-not-leak")),
"ANTHROPIC_API_KEY" => Some(std::ffi::OsString::from("sentinel-also-should-not-leak")),
EnvVars::PATH => Some(std::ffi::OsString::from("/usr/bin:/bin")),
EnvVars::LLVM_PROFILE_FILE => Some(std::ffi::OsString::from("/tmp/coverage.profraw")),
EnvVars::GITHUB_TOKEN => Some(std::ffi::OsString::from("sentinel-should-not-leak")),
EnvVars::ANTHROPIC_API_KEY => {
Some(std::ffi::OsString::from("sentinel-also-should-not-leak"))
}
_ => None,
});
let output = cmd.output().expect("/usr/bin/env should execute");

View file

@ -14,6 +14,7 @@ workspace = true
[dependencies]
console.workspace = true
fabro-static.workspace = true
rand.workspace = true
regex.workspace = true
termimad.workspace = true

View file

@ -1,11 +1,15 @@
use fabro_static::EnvVars;
/// When this environment variable is set to any value, [`try_open`] returns
/// `Ok(())` without launching a browser. Test harnesses set it so spawned
/// `fabro` subprocesses do not pop real browser windows during CI or local
/// runs.
pub const SUPPRESS_ENV_VAR: &str = "FABRO_SUPPRESS_OPEN_BROWSER";
#[expect(
clippy::disallowed_methods,
reason = "Browser launching checks the documented process-env escape hatch."
)]
pub fn try_open(url: &str) -> std::io::Result<()> {
if std::env::var_os(SUPPRESS_ENV_VAR).is_some() {
if std::env::var_os(EnvVars::FABRO_SUPPRESS_OPEN_BROWSER).is_some() {
return Ok(());
}
open::that(url)

View file

@ -12,6 +12,10 @@ pub trait Env: Send + Sync {
pub struct SystemEnv;
impl Env for SystemEnv {
#[expect(
clippy::disallowed_methods,
reason = "SystemEnv is the intentional process-env facade behind the Env trait."
)]
fn var(&self, key: &str) -> Result<String, std::env::VarError> {
std::env::var(key)
}

View file

@ -1,5 +1,7 @@
use std::path::{Path, PathBuf};
use fabro_static::EnvVars;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Home {
root: PathBuf,
@ -12,6 +14,10 @@ impl Home {
}
#[must_use]
#[expect(
clippy::disallowed_methods,
reason = "Home::from_env is the process-env facade for resolving Fabro home paths."
)]
pub fn from_env() -> Self {
Self::from_env_with_lookup(|name| std::env::var_os(name), dirs::home_dir())
}
@ -21,11 +27,11 @@ impl Home {
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
fallback_home: Option<PathBuf>,
) -> Self {
if let Some(root) = lookup("FABRO_HOME") {
if let Some(root) = lookup(EnvVars::FABRO_HOME) {
return Self::new(root);
}
if let Some(home) = lookup("HOME") {
if let Some(home) = lookup(EnvVars::HOME) {
return Self::new(PathBuf::from(home).join(".fabro"));
}
@ -77,7 +83,7 @@ impl Home {
#[cfg(test)]
mod tests {
use super::Home;
use super::{EnvVars, Home};
#[test]
fn accessors_are_relative_to_root() {
@ -115,7 +121,7 @@ mod tests {
fn from_env_prefers_home_env_when_fabro_home_is_absent() {
let home = Home::from_env_with_lookup(
|name| match name {
"HOME" => Some(std::ffi::OsString::from("/tmp/fabro-home-env")),
EnvVars::HOME => Some(std::ffi::OsString::from("/tmp/fabro-home-env")),
_ => None,
},
None,

View file

@ -37,6 +37,7 @@ fabro-model = { path = "../fabro-model" }
fabro-retro = { path = "../fabro-retro" }
fabro-core = { path = "../fabro-core" }
fabro-store = { path = "../fabro-store" }
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-http.workspace = true
thiserror.workspace = true

View file

@ -563,7 +563,7 @@ impl CodergenBackend for AgentCliBackend {
} else {
let mut env = HashMap::new();
for name in provider.api_key_env_vars() {
if let Ok(val) = std::env::var(name) {
if let Some(val) = process_env_var(name) {
env.insert((*name).to_string(), val);
}
}
@ -740,6 +740,14 @@ impl CodergenBackend for AgentCliBackend {
}
}
#[expect(
clippy::disallowed_methods,
reason = "CLI agent fallback credentials intentionally read provider API-key env vars."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
/// Routes codergen invocations to either the API backend or CLI backend
/// based on node attributes and model type.
pub struct BackendRouter {

View file

@ -16,6 +16,7 @@ use fabro_sandbox::config::{
};
use fabro_sandbox::daytona::{DaytonaConfig, detect_repo_info};
use fabro_sandbox::{SandboxProvider, SandboxSpec};
use fabro_static::EnvVars;
use fabro_types::RunId;
use fabro_types::settings::InterpString;
use fabro_types::settings::run::{
@ -362,7 +363,11 @@ impl RunSession {
},
SandboxProvider::Daytona => {
let api_key = match &services.vault {
Some(v) => v.read().await.get("DAYTONA_API_KEY").map(str::to_string),
Some(v) => v
.read()
.await
.get(EnvVars::DAYTONA_API_KEY)
.map(str::to_string),
None => None,
};
SandboxSpec::Daytona {
@ -451,10 +456,18 @@ impl RunSession {
fn resolve_interp(value: &InterpString) -> String {
value
.resolve(|name| std::env::var(name).ok())
.resolve(process_env_var)
.map_or_else(|_| value.as_source(), |resolved| resolved.value)
}
#[expect(
clippy::disallowed_methods,
reason = "Run startup interpolation owns a process-env lookup facade for {{ env.* }} values."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
async fn load_accepted_run_definition(
run_store: &RunStoreHandle,
blob_id: fabro_types::RunBlobId,

View file

@ -13,7 +13,7 @@
)]
#![expect(
clippy::disallowed_methods,
reason = "These Daytona integration tests use the real git CLI to prepare remote-repo fixtures for workflow runs."
reason = "These Daytona integration tests use real process env and git CLI fixtures for workflow runs."
)]
use std::collections::HashMap;
@ -26,6 +26,7 @@ use fabro_agent::Sandbox;
use fabro_graphviz::graph::{AttrValue, Edge, Graph, Node};
use fabro_llm::provider::Provider;
use fabro_sandbox::daytona::{DaytonaConfig, DaytonaSandbox, DaytonaSnapshotConfig};
use fabro_static::EnvVars;
use fabro_store::{ArtifactStore, Database};
use fabro_types::{RunId, StageId, WorkflowSettings};
use fabro_workflow::artifact::sync_artifacts_to_env;
@ -194,7 +195,8 @@ fn load_github_app_credentials() -> fabro_github::GitHubCredentials {
.app_id
.expect("app_id not set in settings.toml [git] section");
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").expect("GITHUB_APP_PRIVATE_KEY not set");
let raw =
std::env::var(EnvVars::GITHUB_APP_PRIVATE_KEY).expect("GITHUB_APP_PRIVATE_KEY not set");
let private_key_pem = if raw.starts_with("-----") {
raw
} else {
@ -1719,13 +1721,13 @@ async fn daytona_toolbox_idle_diagnostic() {
eprintln!("[t=+{sleep_secs}s] FAILED: {e}");
// Diagnose with raw HTTP calls
let api_key = std::env::var("DAYTONA_API_KEY").unwrap_or_default();
let api_key = std::env::var(EnvVars::DAYTONA_API_KEY).unwrap_or_default();
let client = fabro_http::HttpClientBuilder::new()
.timeout(std::time::Duration::from_secs(15))
.build()
.unwrap();
let api_url = std::env::var("DAYTONA_API_URL")
.or_else(|_| std::env::var("DAYTONA_SERVER_URL"))
let api_url = std::env::var(EnvVars::DAYTONA_API_URL)
.or_else(|_| std::env::var(EnvVars::DAYTONA_SERVER_URL))
.unwrap_or_else(|_| "https://app.daytona.io/api".to_string());
// Check sandbox state

View file

@ -17,6 +17,7 @@ anyhow.workspace = true
async-stream = "0.3"
axum = { workspace = true, features = ["macros"] }
fabro-http.workspace = true
fabro-static.workspace = true
futures-util.workspace = true
http = "1"
serde.workspace = true

View file

@ -1,6 +1,7 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use anyhow::{Context, Result};
use fabro_static::EnvVars;
#[derive(Clone, Debug)]
pub struct Config {
@ -11,22 +12,22 @@ pub struct Config {
impl Config {
pub fn from_env() -> Result<Self> {
Self::from_lookup(&|name| std::env::var(name).ok())
Self::from_lookup(&process_env_var)
}
pub fn from_lookup(lookup: &dyn Fn(&str) -> Option<String>) -> Result<Self> {
let bind_addr = lookup("TWIN_OPENAI_BIND_ADDR")
let bind_addr = lookup(EnvVars::TWIN_OPENAI_BIND_ADDR)
.map(|value| value.parse().context("invalid TWIN_OPENAI_BIND_ADDR"))
.transpose()?
.unwrap_or_else(|| SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3000));
let require_auth = lookup("TWIN_OPENAI_REQUIRE_AUTH")
.map(|value| parse_bool_env(&value, "TWIN_OPENAI_REQUIRE_AUTH"))
let require_auth = lookup(EnvVars::TWIN_OPENAI_REQUIRE_AUTH)
.map(|value| parse_bool_env(&value, EnvVars::TWIN_OPENAI_REQUIRE_AUTH))
.transpose()?
.unwrap_or(true);
let enable_admin = lookup("TWIN_OPENAI_ENABLE_ADMIN")
.map(|value| parse_bool_env(&value, "TWIN_OPENAI_ENABLE_ADMIN"))
let enable_admin = lookup(EnvVars::TWIN_OPENAI_ENABLE_ADMIN)
.map(|value| parse_bool_env(&value, EnvVars::TWIN_OPENAI_ENABLE_ADMIN))
.transpose()?
.unwrap_or(true);
@ -38,6 +39,14 @@ impl Config {
}
}
#[expect(
clippy::disallowed_methods,
reason = "twin-openai config owns a process-env lookup facade for its test server settings."
)]
fn process_env_var(name: &str) -> Option<String> {
std::env::var(name).ok()
}
impl Default for Config {
fn default() -> Self {
Self::from_env().unwrap_or(Self {

View file

@ -6,6 +6,7 @@
mod common;
use anyhow::{Context, Result, anyhow, bail, ensure};
use fabro_static::EnvVars;
use serde_json::{Value, json};
const DEFAULT_LIVE_BASE_URL: &str = "https://api.openai.com";
@ -210,15 +211,15 @@ async fn live_openai_contract_smoke_suite() {
impl LiveOptions {
fn from_env() -> Result<Option<Self>> {
let Some(api_key) = non_empty_env("OPENAI_API_KEY") else {
let Some(api_key) = non_empty_env(EnvVars::OPENAI_API_KEY) else {
return Ok(None);
};
let base_url = non_empty_env("TWIN_OPENAI_LIVE_BASE_URL")
let base_url = non_empty_env(EnvVars::TWIN_OPENAI_LIVE_BASE_URL)
.unwrap_or_else(|| DEFAULT_LIVE_BASE_URL.to_owned());
let model = non_empty_env("TWIN_OPENAI_LIVE_MODEL")
let model = non_empty_env(EnvVars::TWIN_OPENAI_LIVE_MODEL)
.unwrap_or_else(|| DEFAULT_LIVE_MODEL.to_owned());
let organization = non_empty_env("OPENAI_ORGANIZATION");
let project = non_empty_env("OPENAI_PROJECT");
let organization = non_empty_env(EnvVars::OPENAI_ORGANIZATION);
let project = non_empty_env(EnvVars::OPENAI_PROJECT);
Ok(Some(Self {
api: common::ApiClient::new(base_url, Some(api_key), organization, project)?,
@ -1836,6 +1837,10 @@ where
Ok(())
}
#[expect(
clippy::disallowed_methods,
reason = "Live twin-openai contract tests read documented OpenAI env inputs."
)]
fn non_empty_env(name: &str) -> Option<String> {
std::env::var(name)
.ok()