Skip to content

环境上下文生成与差分

从 Step 快照追踪 cwd、shell、日期、时区、网络、文件系统权限和多环境状态如何进入模型上下文。

基于rust-v0.150.0
CodexRustContext

环境上下文生成与差分 ​

模型看到的 <environment_context> 不是把进程环境变量或操作系统信息全部转储出来。当前版本由 EnvironmentsState 选择性投影 Step 中的 environment cwd、启动状态、shell、主环境权限、workspace roots、日期、时区、网络域名规则和子 agent 摘要,再通过 WorldState 只发送变化部分。

这与旧版单一 EnvironmentContext 已有明显差异:现在一个 Turn 可以选择多个 environment,也可以在远端环境尚未 ready 时先记录 starting;环境移除后发送 unavailable,而不是用新的完整 XML 覆盖历史。本文沿真实源码从 StepContext 追到最终 user-role fragment,并解释哪些字段没有进入该结构。

阅读前可先看 StepContext模型请求,理解为什么一次请求要冻结 environment snapshot;WorldState 的通用差分机制见 Context变更语义。

1. 当前数据模型 ​

旧实现常把环境上下文描述成一个含 cwd、shell、current_date 的扁平结构。当前实现中负责模型可见状态的是 EnvironmentsState:环境自身保存在按 ID 排序的 BTreeMap,Turn 级事实保存在旁边。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: EnvironmentsState。

rust
#[derive(Clone, Debug, Default)]
pub(crate) struct EnvironmentsState {
    environments: BTreeMap<String, EnvironmentState>,
    current_date: Option<String>,
    timezone: Option<String>,
    network: Option<NetworkContext>,
    filesystem: Option<FileSystemContext>,
    subagents: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct EnvironmentState {
    cwd: PathUri,
    status: EnvironmentStatus,
    shell: Option<String>,
    is_primary: bool,
}

BTreeMap 让 snapshot 和 XML 按 environment ID 稳定输出,而“谁是 primary”仍由原 selection 顺序计算并存入值中。当前结构没有 os、platform、CPU 架构或进程环境变量字段;模型若知道这些信息,来源也不是本 section。

2. 输入所有权 ​

环境上下文不是从一个对象整体克隆出来。from_turn_context_with_environments 同时消费 TurnContext 和本 Step 的 TurnEnvironmentSnapshot:cwd、shell、ready/starting 来自 Step;timezone 和网络规则来自 Turn;文件系统只取 primary environment 的权限与 workspace roots。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: from_turn_context_with_environments。

rust
pub(crate) fn from_turn_context_with_environments(
    turn_context: &TurnContext,
    environments: &TurnEnvironmentSnapshot,
    current_date: Option<String>,
) -> Self {
    Self {
        environments: environment_states(environments),
        current_date,
        timezone: turn_context.timezone.clone(),
        network: network_from_turn_context(turn_context),
        filesystem: environments.primary().map(|environment| {
            FileSystemContext::from_permission_profile(
                environment.permission_profile(),
                environment.workspace_roots(),
            )
        }),
        subagents: None,
    }
}

下表给出所有者、消费者和捕获时机,避免把相似字段混成“当前机器信息”。

字段真实来源生效时机关键边界
environment IDselectionStep snapshotXML 顺序按 ID,不按 selection 顺序
cwdready/starting environmentStep capture使用 PathUri,支持外部 Windows 路径
shellready environmentreadiness 完成后starting 时为 None
primaryready environment 顺序Step capture多环境时才渲染属性
current_dateTimeProvider构造 WorldState 时不是直接使用 Turn 中旧日期
timezoneTurnContextTurn 构造时IANA 获取失败回退 Etc/UTC
networkrequirements configTurn 配置生效后仅允许/拒绝域名列表
filesystemprimary environmentStep capture不合并所有 environment 权限
subagentsAgentControl构造 WorldState 时空字符串不注入

3. 状态归并 ​

environment_states 先遍历 ready environment,并把第一个 ready 项标为 primary;随后补入 starting 项。相同 ID 如果已经 ready,entry(...).or_insert_with(...) 不会让 starting 状态覆盖它。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: environment_states。

rust
fn environment_states(snapshot: &TurnEnvironmentSnapshot) -> BTreeMap<String, EnvironmentState> {
    let mut environments = snapshot
        .turn_environments()
        .enumerate()
        .map(|(index, environment)| {
            (
                environment.environment_id.clone(),
                EnvironmentState {
                    cwd: environment.cwd().clone(),
                    status: EnvironmentStatus::Available,
                    shell: environment
                        .shell
                        .as_ref()
                        .map(|shell| shell.name().to_string()),
                    is_primary: index == 0,
                },
            )
        })
        .collect::<BTreeMap<_, _>>();
    for environment in snapshot.starting() {
        environments
            .entry(environment.selection.environment_id.clone())
            .or_insert_with(|| EnvironmentState {
                cwd: environment.selection.cwd.clone(),
                status: EnvironmentStatus::Starting,
                shell: None,
                is_primary: false,
            });
    }
    environments
}

这段代码建立了三个不变量:只有 ready environment 能成为 primary;starting environment 没有 shell;同一 ID 在一次 snapshot 中只输出一条状态。selection 顺序只决定 primary,最终 XML 列举顺序则由 ID 排序。

Unavailable 不是持久状态结构里的枚举值,而是 diff renderer 为“上一份 snapshot 有、当前没有”的 ID 生成的更新标记。下一份完整 snapshot 不会继续保存这个幽灵条目。

4. 两种 XML 布局 ​

只有一个且已 ready 的 environment 时,代码保持旧版兼容布局,直接输出 <cwd> 和 <shell>;多环境或存在 starting 环境时,才引入 <environments> 容器、ID 和 primary 属性。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: RenderedEnvironments::body、is_legacy_single。

rust
fn is_legacy_single(environments: &BTreeMap<String, EnvironmentState>) -> bool {
    environments.len() == 1
        && environments
            .values()
            .all(|environment| environment.status == EnvironmentStatus::Available)
}

if self.legacy_single {
    if let Some(EnvironmentUpdate::Current(environment)) = self.updates.values().next() {
        push_environment_values(&mut rendered, environment, "  ");
    }
} else if !self.updates.is_empty() {
    rendered.push_str("  <environments>\n");
    for (id, update) in &self.updates {
        match update {
            EnvironmentUpdate::Current(environment) => {
                rendered.push_str("    <environment id=\"");
                push_xml_escaped_text(&mut rendered, id);
                rendered.push('"');
                if self.include_primary {
                    rendered.push_str(if environment.is_primary {
                        " primary=\"true\""
                    } else {
                        " primary=\"false\""
                    });
                }
                rendered.push_str(">\n");
                push_environment_values(&mut rendered, environment, "      ");
                rendered.push_str("    </environment>\n");
            }
            EnvironmentUpdate::Unavailable => {
                rendered.push_str("    <environment id=\"");
                push_xml_escaped_text(&mut rendered, id);
                rendered.push_str("\" status=\"unavailable\" />\n");
            }
        }
    }
    rendered.push_str("  </environments>\n");
}

单环境完整输出形如:

xml
<environment_context>
  <cwd>/repo</cwd>
  <shell>bash</shell>
  <current_date>2026-02-26</current_date>
  <timezone>America/Los_Angeles</timezone>
</environment_context>

多环境完整输出则显式标出边界:

xml
<environment_context>
  <environments>
    <environment id="local" primary="true">
      <cwd>/repo/local</cwd>
      <shell>bash</shell>
    </environment>
    <environment id="remote" primary="false">
      <cwd>/repo/remote</cwd>
      <status>starting</status>
    </environment>
  </environments>
</environment_context>

从单环境跨到多环境时,即使 local 本身没有改变,也必须重述所有当前 environment。原因是模型需要从“隐含唯一环境”切换到“显式 ID + primary”语义;只追加 remote 无法补齐 local 的 ID 和 primary 属性。

5. 差分算法 ​

EnvironmentsState::snapshot 把网络和文件系统先渲染成稳定字符串,再与上一份 typed snapshot 比较。日期、时区、网络和文件系统任何一项变化都会设置 turn_context_values_changed;environment 则逐 ID 比较 cwd、status、primary 和 shell。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: snapshot、render_diff。

rust
let turn_context_values_changed = current.current_date != previous.current_date
    || current.timezone != previous.timezone
    || current.network != previous.network
    || current.filesystem != previous.filesystem;
let multiple_environments = self.environments.len() > 1;
let previous_multiple_environments = previous.environments.len() > 1;
let mut updates = self
    .environments
    .iter()
    .filter(|(id, _)| {
        let environment = &current.environments[*id];
        previous.environments.get(*id).is_none_or(|previous| {
            multiple_environments != previous_multiple_environments
                || !environment.has_same_diff_value(previous)
        })
    })
    .map(|(id, environment)| (id.clone(), EnvironmentUpdate::Current(environment.clone())))
    .collect::<BTreeMap<_, _>>();
updates.extend(
    previous
        .environments
        .keys()
        .filter(|id| !self.environments.contains_key(*id))
        .map(|id| (id.clone(), EnvironmentUpdate::Unavailable)),
);

5.1 Shell 特例 ​

shell 比较有一个刻意的非对称规则:只有前后两边都有 shell 时才比较字符串;一边缺失时视为没有差异。

源码位置:codex-rs/core/src/context/world_state/environment.rs :: EnvironmentSnapshot::has_same_diff_value。

rust
fn has_same_diff_value(&self, other: &Self) -> bool {
    self.cwd == other.cwd
        && self.status == other.status
        && self.is_primary == other.is_primary
        && self
            .shell
            .as_ref()
            .zip(other.shell.as_ref())
            .is_none_or(|(current, previous)| current == previous)
}

因此 ready environment 的 shell 从未知变成 zsh,如果 cwd、status 和 primary 都不变,不会单独产生环境更新;但 starting 变 available 会因 status 改变而发出完整当前项,同时带上已知 shell。这个规则避免仅因旧 snapshot 缺少 shell 字段而反复补发兼容信息。

6. 权限投影 ​

环境上下文中的 <filesystem> 不是执行器权限对象的原样序列化。FileSystemContext::from_permission_profile 先用 primary environment 的 workspace roots 展开 :workspace_roots,再把权限降维为模型需要理解的 managed/disabled/external 和 restricted/unrestricted 结构。

源码位置:codex-rs/core/src/context/environment_context.rs :: FileSystemContext::from_permission_profile。

rust
let materialized_workspace_roots = workspace_roots
    .iter()
    .filter_map(|workspace_root| workspace_root.to_abs_path().ok())
    .collect::<Vec<_>>();
let permission_profile = permission_profile
    .clone()
    .materialize_project_roots_with_workspace_roots(&materialized_workspace_roots);
let workspace_roots = workspace_roots
    .iter()
    .map(PathUri::inferred_native_path_string)
    .collect();
let permission_profile = match permission_profile {
    PermissionProfile::Managed { file_system, .. } => {
        FileSystemPermissionProfileContext::Managed(
            ManagedFileSystemContext::from(file_system),
        )
    }
    PermissionProfile::Disabled => FileSystemPermissionProfileContext::Disabled,
    PermissionProfile::External { .. } => FileSystemPermissionProfileContext::External,
};

restricted entries 在渲染前去重;deny entry 固定写出 escalatable="false"。这只是模型可见说明,不是操作系统强制本身,真正的授权与沙箱执行链属于 权限与Sandbox指令。

7. 时间与开关 ​

TurnContext::new 通过 local_time_context 捕获日期和 IANA timezone,失败时同时回退 UTC 日期与 Etc/UTC。不过构造模型可见 EnvironmentsState 时,日期会再次从 Session 的 TimeProvider 读取并格式化;timezone 仍使用 Turn 快照。

相关源码:

  • codex-rs/core/src/session/turn_context.rs :: local_time_context
  • codex-rs/core/src/session/world_state.rs :: build_world_state_for_step
rust
fn local_time_context() -> (String, String) {
    match iana_time_zone::get_timezone() {
        Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone),
        Err(_) => (
            Utc::now().format("%Y-%m-%d").to_string(),
            "Etc/UTC".to_string(),
        ),
    }
}

let current_date = self
    .services
    .time_provider
    .current_time(self.thread_id())
    .await
    .map_err(|err| CodexErr::Fatal(format!("failed to read current time: {err:#}")))?
    .with_timezone(&chrono::Local)
    .format("%Y-%m-%d")
    .to_string();

这使跨午夜的后续 Step 可以更新 <current_date>,即使 Turn 构造时保存的 current_date 没有改变。测试也明确断言 timezone 改变时,环境更新中使用调用时的本地日期。

include_environment_context 默认值是 true。关闭后,Session 不创建 EnvironmentsState,也不查询 subagent 文本;相关 cwd 或 timezone 变化不会产生 <environment_context>。这不是发送“环境已移除”的 XML,而是该 section 根本不参与当前 WorldState。

环境选择在 Step 内保持固定:capture_step_context_with_required_mcp_servers 对 TurnContext 中的 snapshot 调用 refresh_readiness(),只晋升已经完成启动的环境,不采纳更新后的 thread selection。随后 build_world_state_for_step 使用同一份 StepContext.environments 构造 EnvironmentsState。因此一次请求内 不会出现 cwd 来自旧 selection、权限来自新 selection 的混合快照。

源码位置:codex-rs/core/src/session/mod.rs :: capture_step_context_with_required_mcp_servers

rust
let environments = turn_context.environments.refresh_readiness();
let step_context = StepContext {
    turn: Arc::clone(&turn_context),
    environments,
    // mcp、capability 和 tool router 也从本次快照继续构造
    ..
};

8. 转义边界 ​

cwd、shell、environment ID、日期和时区通过 push_xml_escaped_text 转义五个 XML 特殊字符。路径中的 & 或 ID 中的引号不会破坏标签结构。

源码位置:codex-rs/core/src/context/environment_context.rs :: push_xml_escaped_text。

rust
pub(crate) fn push_xml_escaped_text(rendered: &mut String, value: &str) {
    for ch in value.chars() {
        match ch {
            '&' => rendered.push_str("&amp;"),
            '<' => rendered.push_str("&lt;"),
            '>' => rendered.push_str("&gt;"),
            '"' => rendered.push_str("&quot;"),
            '\'' => rendered.push_str("&apos;"),
            _ => rendered.push(ch),
        }
    }
}

网络域名当前由 domains.join(",") 直接写入 <allowed> 和 <denied>,没有再次调用 XML escape helper。requirements 层通常约束其为域名模式,但从这个 renderer 本身不能外推“任意字符串都安全转义”;这是字段输入契约与通用 XML renderer 的区别。

9. 渲染测试 ​

environment_render_tests.rs 以完整字符串断言单环境、多环境、Windows 外部路径、网络、workspace roots、managed 权限和子 agent 布局。下面的多环境测试输入两个 cwd,断言 local 为 primary、remote 为非 primary,并验证每个 shell 位于自己的 environment 内。

源码位置:codex-rs/core/src/context/world_state/environment_render_tests.rs :: serialize_environment_context_with_multiple_selected_environments。

rust
let local_cwd = test_path_buf("/repo/local");
let remote_cwd = test_path_buf("/repo/remote");
let context = environment_state(
    [
        environment("local", PathUri::from_abs_path(&local_cwd.abs()), "bash"),
        environment("remote", PathUri::from_abs_path(&remote_cwd.abs()), "bash"),
    ],
    Some("2026-02-26".to_string()),
    Some("America/Los_Angeles".to_string()),
    /*network*/ None,
    /*subagents*/ None,
);

let expected = format!(
    r#"<environment_context>
  <environments>
    <environment id="local" primary="true">
      <cwd>{}</cwd>
      <shell>bash</shell>
    </environment>
    <environment id="remote" primary="false">
      <cwd>{}</cwd>
      <shell>bash</shell>
    </environment>
  </environments>
  <current_date>2026-02-26</current_date>
  <timezone>America/Los_Angeles</timezone>
</environment_context>"#,
    local_cwd.display(),
    remote_cwd.display()
);

assert_eq!(
    context.render(),
    expected
);

changing_primary_environment_updates_model_context_and_persisted_state 进一步证明 primary 变化同时影响两处:模型 fragment 重述两个 environment,WorldState merge patch 则把旧 primary 的 is_primary 删除、给新 primary 写入 true。

源码位置:codex-rs/core/src/context/world_state/environment_tests.rs :: changing_primary_environment_updates_model_context_and_persisted_state。

rust
let previous = before.snapshot();
let rendered = after
    .render_diff(PreviousSectionState::Known(&previous))
    .expect("primary change should update the model")
    .render();

assert_eq!(
    rendered,
    format!(
        "<environment_context>\n  <environments>\n    <environment id=\"local\" primary=\"false\">\n      <cwd>{}</cwd>\n      <shell>bash</shell>\n    </environment>\n    <environment id=\"remote\" primary=\"true\">\n      <cwd>{}</cwd>\n      <shell>zsh</shell>\n    </environment>\n  </environments>\n</environment_context>",
        PathUri::parse("file:///local")?.inferred_native_path_string(),
        PathUri::parse("file:///remote")?.inferred_native_path_string(),
    )
);

let mut previous_world_state = super::super::WorldState::default();
previous_world_state.add_section(before);
let mut current_world_state = super::super::WorldState::default();
current_world_state.add_section(after);
assert_eq!(
    current_world_state
        .snapshot()
        .merge_patch_from(&previous_world_state.snapshot()),
    Some(json!({
        "environments": {
            "environments": {
                "local": { "is_primary": null },
                "remote": { "is_primary": true },
            },
        },
    }))
);

这些测试证明 XML 布局、差分选择和持久化 patch,不证明 remote environment 的连接一定成功,也不证明模型正确理解 primary 或权限说明。record_context_updates_omits_environment_item_when_disabled 则证明开关关闭后不发送 fragment,但没有证明历史中旧环境文本会被物理删除。

10. 定位顺序 ​

遇到模型使用错误 cwd、把 remote 当 local,或环境状态长期停留 starting 时,可以按所有权边界排查:

  1. 在 capture_step_context 检查本 Step 的 TurnEnvironmentSnapshot,不要先看 Session 的 deprecated cwd。
  2. 在 environment_states 检查 ready/starting 分类和第一个 ready environment;primary 不是按字典序选取。
  3. 在 EnvironmentsState::snapshot 检查模型比较值,确认单环境/多环境边界是否变化。
  4. 在 render_diff 检查目标 ID 是 Current 还是 Unavailable,以及 turn metadata 是否真的变化。
  5. 若问题是 workspace roots 或权限,检查 primary environment 的 permission_profile;其他 environment 的权限不会合并进 <filesystem>。
  6. 若完全没有环境 fragment,先检查 include_environment_context,再检查 WorldState baseline 是否判定无变化。

可执行的源码导航命令:

bash
rg -n "struct EnvironmentsState|fn environment_states|fn render_diff" \
  codex-rs/core/src/context/world_state/environment.rs
rg -n "FileSystemContext|NetworkContext|push_xml_escaped_text" \
  codex-rs/core/src/context/environment_context.rs
rg -n "include_environment_context|current_date|from_turn_context_with_environments" \
  codex-rs/core/src/session/world_state.rs

沿这条链可以得到一个可验证结论:<environment_context> 是请求级 environment snapshot 与 Turn/Session 元数据的模型投影,不是宿主机环境转储;它用完整 snapshot 持久化比较基线,用增量 XML 告诉模型本次真正变化的 environment 和运行约束。