一. 加载环境变量
一共分为两个阶段,一个是Tokio 启动前(单线程,std::env::set_var 安全),另一个是Tokio 启动后(多线程,不能再碰 set_var)。
1. 阶段一:Tokio 启动前(单线程,std::env::set_var 安全)
fn main() -> anyhow::Result<()> {
let _ = dotenvy::dotenv(); ./env
ironclaw::bootstrap::load_ironclaw_env();~/./env
这步结束后,OS 环境变量就冻结了——后面只读不写。
let result = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(async_main());2. 阶段二:Tokio 启动后(多线程,不能再碰 set_var)
ironclaw_common::env_helpers下有一个static RUNTIME_ENV_OVERRIDES: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();只能被加载一次的。 数据来自CLI 标志覆盖(如 --auto-approve)、libsql 自动检测
以及主crate下 src/config/mod.rs(main crate):INJECTED_VARS 数据来自加密 DB secrets 表里的 API key、token;OS 凭据存储(Keychain/credentials.json)(Anthropic 的 OAuth token 有效期只有 8-12 小时。IronClaw 虽然也会把这个 token 存进加密 DB secrets 表,但 DB 里的副本可能已过期。每次启动时从 OS 凭据存储重新提取一次,覆盖掉 DB 里可能过
期的副本——因为 claude CLI 会自动刷新 Keychain/credentials.json 里的 token。)3. 读取优先级
优先级 高 → 低
───────────────────────────────────────────────────────
(0) DB settings 表 ← 仅 db_first_* 系列函数会查这层
(1) OS 环境变量 ← dotenvy 阶段写入的,std::env::var()
(2) RUNTIME_ENV_OVERRIDES ← set_runtime_env() 写入的 overlay
(3) INJECTED_VARS ← DB secrets / OS 凭据 overlay
(4) 硬编码默认值 ← 代码里的 fallback
第 0 层(DB settings)只在 db_first_* 辅助函数中参与,不在通用路径里。
通用的 optional_env() / env_or_override() 从第 1 层开始查。DB settings 表 ← 仅 db_first_* 系列函数会查这层
用户ui或cli设置的字段,但是是局部合并。
同一个 AgentConfig::resolve() 函数里,字段和字段就不一样。看 /E:/codes/Rust/projects/ironclaw/src/config/agent.rs:82 的完整 resolve():
pub(crate) fn resolve(settings: &Settings) -> Result<Self, ConfigError> {
let defaults = crate::settings::AgentSettings::default();
Ok(Self {
// ✅ DB + env 两路:有 DB 对应字段,用户在 Web UI 设了就生效
name: db_first_or_default(&settings.agent.name, ...)?,
max_parallel_jobs: db_first_or_default(&settings.agent.max_parallel_jobs, ...)?,
use_planning: db_first_bool(settings.agent.use_planning, ...)?,
auto_approve_tools:db_first_bool(settings.agent.auto_approve_tools, ...)?,
// ❌ 只有 env 一路:DB 的 Settings struct 里根本没这些字段
allow_local_tools: parse_bool_env("ALLOW_LOCAL_TOOLS", false)?,
multi_tenant: parse_bool_env("AGENT_MULTI_TENANT", false)?,
engine_v2: parse_bool_env("ENGINE_V2", false)?,
max_cost_per_day_cents: parse_option_env("MAX_COST_PER_DAY_CENTS")?,
max_jobs_per_user: parse_option_env("MAX_JOBS_PER_USER")?,
max_llm_concurrent_per_user: parse_option_env("TENANT_MAX_LLM_CONCURRENT")?,
})
}
意思是:不存在一个全局的 "查所有变量 → 合并 → 产出配置" 的管道。每个配置子模块在 resolve 自己的 struct 时,逐个字段选择路径:
- 这个字段在 Settings struct 里有对应成员 → 用 db_first_*,数据源是 DB + env 双路
- 这个字段在 Settings struct 里没有对应成员 → 只能用 parse_*_env(),纯 envpub(crate) fn optional_env(key: &str) -> Result<Option<String>, ConfigError> {
match std::env::var(key) {//只读环境变量
if let Some(val) = ironclaw_common::env_helpers::env_or_override(key) {//运行时变量
if let Some(val) = INJECTED_VARS// Fall back to thread-safe overlay (secrets injected from DB)4. 跨 crate 穿透
ironclaw_llm 不依赖 main crate,但它也需要看到 INJECTED_VARS 里的 API key。main crate 启动时调用 register_secondary_fallback() 向 ironclaw_common 注册一个闭包,这样 LLM crate 调 env_or_override() 时 会自动穿透到第 3 层。