一. 加载环境变量

  • 一共分为两个阶段,一个是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(),纯 env
pub(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 层。