Skip to content

跨平台能力矩阵

从交付入口、沙箱后端、进程控制、终端通知和路径模型五个层次比较 Codex 在 macOS、Linux 与 Windows 上的真实能力及降级边界。

基于rust-v0.150.0
CodexRustCross-platformSandbox

跨平台能力矩阵 ​

Codex 同时为 macOS、Linux 和 Windows 交付原生可执行文件,但“能够安装”不等于“每项能力完全 等价”。跨平台差异分散在 npm launcher、Rust 条件编译、沙箱后端、PTY、shell 探测、终端协议和 路径转换中。只看顶层 CLI 是否启动,容易把后端缺失、条件降级和真正的功能缺口混成一句“支持”。

本文用三种状态描述能力:

  • 原生支持:当前平台存在专用实现,正常路径不依赖另一操作系统;
  • 条件支持:实现存在,但需要系统版本、内核能力、终端协议、辅助程序或显式配置;
  • 不提供:入口在编译期不存在,或当前实现明确拒绝这一平台/能力组合。

这三个状态不能直接代表安全强度。例如 Windows 有原生沙箱实现,但默认配置仍可能是 Disabled;Linux 有内建 helper,但缺少可用的 bubblewrap 时会在准备阶段失败。正确的判断单位 不是“操作系统名称”,而是“入口是否交付、后端是否选中、前置条件是否满足、失败时如何处理”。

阅读本文前,建议先理解 源码仓库目录地图 中 npm launcher 与 Rust workspace 的关系,以及 Codex信任边界 中 policy 与 OS enforcement 的区别。本文是一张能力选择与排障矩阵,不替代 Seatbelt、bubblewrap、Windows token 或 ConPTY 的平台实现专题。

读完后,应能区分“代码被当前 target 编译”“纯逻辑 fixture 验证某个平台规则”和“测试真的在该平台 启动后端进程”三种证据,并能解释 skip 是环境前提未满足,还是产品明确不提供该能力。

1. 平台分流层次 ​

Codex 的公共逻辑主要位于 codex-core,平台差异并没有集中到一个抽象接口中,而是在四个层次 逐步收敛:

  1. npm launcher 按 Node 的 platform + arch 选择原生包;
  2. Rust CLI 用 cfg(target_os) 决定子命令和模块是否存在;
  3. sandbox、PTY、进程树和终端层选择具体 OS 后端;
  4. PathUri 在执行边界才转换成当前主机的原生路径。

图中的失败分支有两类:不满足正确性或安全前提时直接报错;可以保持语义但损失部分能力时,才选择 降级。例如外来路径不能在本机执行,必须拒绝;旧版 Windows 没有 ConPTY 时,可以把 unified exec 降为一次性 shell command。

2. 交付入口 ​

codex-cli/bin/codex.js 是 npm 安装后的统一入口。它不实现 agent runtime,只负责把当前主机映射 到原生包,再启动其中的 Rust 二进制:

源码位置:codex-cli/bin/codex.js :: PLATFORM_PACKAGE_BY_TARGET

javascript
// JS launcher 用 Rust target triple 精确映射六个原生发布包。
const PLATFORM_PACKAGE_BY_TARGET = {
  "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
  "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
  "x86_64-apple-darwin": "@openai/codex-darwin-x64",
  "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
  "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
  "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
};

源码位置:codex-cli/bin/codex.js :: PLATFORM_PACKAGE_BY_TARGET。

平台x86_64arm64原生文件名launcher 行为
macOScodex-darwin-x64codex-darwin-arm64codex启动 Darwin 二进制
Linuxcodex-linux-x64codex-linux-arm64codex启动 musl Linux 二进制
Windowscodex-win32-x64codex-win32-arm64codex.exe启动 MSVC Windows 二进制

launcher 会继承 stdin/stdout/stderr 和环境变量,向子进程转发 SIGINT、SIGTERM、SIGHUP,并 镜像其退出码或退出信号。目标组合无法映射时,它抛出 Unsupported platform;平台 optional dependency 缺失时,它给出重新安装提示。这里没有“找不到 Windows 包就改跑 Linux 包”一类跨平台 兜底。

Node 分支还把 Android 的 x64/arm64 映射为 Linux musl target,但这只说明 launcher 的包选择规则, 不能据此推导 TUI、剪贴板、沙箱和系统集成已经形成完整 Android 产品支持。本文的能力矩阵因此只 覆盖源码明确作为主机后端处理的 macOS、Linux 和 Windows。

2.1 产品入口跨平台 ​

Rust CLI、TUI、非交互 exec 和 App Server 都建立在共享 Core 上。明显的编译期例外是 codex app:desktop_app 模块和 App 子命令只在 macOS、Windows 编译,Linux 二进制中没有这一 入口。

入口macOSLinuxWindows差异点
codex TUI原生支持原生支持原生支持终端输入、焦点和剪贴板后端不同
codex exec原生支持原生支持原生支持shell、PTY、进程树和沙箱不同
codex app-server原生支持原生支持原生支持executor 所在平台决定实际执行能力
codex sandboxSeatbelt 子命令Linux sandbox 子命令Windows sandbox 子命令参数和强制机制不同
codex app打开或安装 Codex.app不提供打开或引导安装 Windows AppLinux 在编译期无此子命令

macOS 实现会在 /Applications 和用户 Applications 目录查找 App,通过 codex:// URL 打开工作区, 必要时下载对应架构的 DMG。Windows 实现通过 PowerShell 查询 Start Apps,使用 Start-Process 打开 同一 URL scheme,未安装时转到 Microsoft installer。二者是两套系统集成,不是一个跨平台 GUI launcher。

3. 沙箱矩阵 ​

Core 先计算 PermissionProfile 和是否需要平台沙箱,SandboxManager 再选择后端。选择函数很短, 却定义了最关键的平台边界:

源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxType, get_platform_sandbox

rust
pub enum SandboxType {
    // None 是明确的“无内建平台后端”,不能解释为策略没有要求沙箱。
    None,
    MacosSeatbelt,
    LinuxSeccomp,
    WindowsRestrictedToken,
}

impl SandboxType {
    pub fn as_metric_tag(self) -> &'static str {
        match self {
            SandboxType::None => "none",
            SandboxType::MacosSeatbelt => "seatbelt",
            SandboxType::LinuxSeccomp => "seccomp",
            SandboxType::WindowsRestrictedToken => "windows_sandbox",
        }
    }
}

pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
    // 编译目标先决定可用后端;Windows 还需要运行时显式启用。
    if cfg!(target_os = "macos") {
        Some(SandboxType::MacosSeatbelt)
    } else if cfg!(target_os = "linux") {
        Some(SandboxType::LinuxSeccomp)
    } else if cfg!(target_os = "windows") {
        if windows_sandbox_enabled {
            Some(SandboxType::WindowsRestrictedToken)
        } else {
            None
        }
    } else {
        None
    }
}

源码位置:codex-rs/sandboxing/src/manager.rs :: get_platform_sandbox。

维度macOSLinuxWindows
SandboxTypeMacosSeatbeltLinuxSeccomp启用时为 WindowsRestrictedToken,否则 None
文件系统后端Seatbelt profile默认 bubblewrap mount namespacerestricted-token 或 elevated backend 的 token、ACL 与 capability 规则
网络后端Seatbelt 网络规则与受管代理路径namespace/seccomp 与受管代理路径Windows 网络强制;受管代理会选择 elevated backend
额外程序/usr/bin/sandbox-execcodex-linux-sandbox 与可用的 system/bundled bwrapWindows sandbox helper、已配置身份和权限
默认失败语义后端不可用则拒绝转换helper/bwrap/内核条件不足则拒绝转换权限形状无法强制时拒绝准备,不静默无沙箱执行

3.1 Seatbelt策略 ​

macOS 路径由 seatbelt.rs 生成 profile,再通过 /usr/bin/sandbox-exec 包装原命令。读写 roots、网络 策略、临时目录和受管代理所需的 Unix socket 都进入 profile。codex sandbox macos 还暴露 --allow-unix-socket、--log-denials 等平台参数。

Seatbelt 是 macOS 专用机制。在其他系统请求这一后端会得到 SeatbeltUnavailable,而不是用同名 策略模拟一个较弱实现。

3.2 Bubblewrap视图 ​

LinuxSeccomp 这个枚举名不能完整描述当前实现。codex-linux-sandbox 的实际默认顺序是:

  1. 外层进程用 bubblewrap 构造只读为主、按 policy 开放写 roots 的文件系统视图;
  2. 在隔离环境中重新进入 helper;
  3. 设置 no_new_privs,按网络策略安装 seccomp filter;
  4. execvp 进入用户命令。

LandlockCommand 类型名仍因兼容性保留,但 --use-legacy-landlock 才会进入旧文件系统后端。默认 bubblewrap 路径失败时不会自动回退到 Landlock。系统 bwrap 必须支持 Codex 需要的能力;否则尝试 随包交付的 bundled bwrap,两者都不存在才报告明确错误。

WSL 也会走 Linux 分支。WSL1 无法提供 bubblewrap 所需能力:只要当前权限需要 bubblewrap, ensure_linux_bubblewrap_is_supported() 就返回 Wsl1UnsupportedForBubblewrap。这不是 Windows sandbox 的降级入口。WSL2 仍需要可用 helper、bwrap 和相应内核能力,不能只凭“WSL2”三个字判定沙箱成功。

3.3 Windows ​

Windows 配置有三个级别:

源码位置:codex-rs/protocol/src/config_types.rs :: WindowsSandboxLevel

rust
// Windows sandbox level 是额外平台配置,不改变 macOS/Linux 后端枚举。
pub enum WindowsSandboxLevel {
    Disabled,
    RestrictedToken,
    Elevated,
}

// proxy settings 的持久化协调是 Windows launcher 的另一条独立平台轴。
pub enum WindowsSandboxProxySettingsMode {
    Reconcile,
    Preserve,
}

源码位置:codex-rs/protocol/src/config_types.rs :: WindowsSandboxLevel。

Disabled 是类型默认值;只有配置或 feature 启用后,平台选择才返回 WindowsRestrictedToken。RestrictedToken 是非提权后端,Elevated 使用预配置的 sandbox identity 与更完整的 ACL/网络能力。受管网络需要 Windows firewall 与 sandbox identity 配合,即使配置原本是 restricted token,也会选择 elevated backend 执行。

非提权后端并不假装支持所有权限形状。它不能直接强制 split filesystem read 或 deny-read 时, resolve_windows_restricted_token_filesystem_overrides() 会返回“refusing to run unsandboxed”。这条 fail-closed 路径比“Windows 有沙箱”这个笼统结论更重要。

elevated 模式需要先执行 setup/refresh,为 sandbox identity 准备凭据、ACL、网络和 helper。 sandbox_private_desktop 默认是 true,允许命令在隔离 desktop 中启动;setup 未完成、用户取消提权 或权限准备失败都会产生可观察错误。

同一份权限配置在不同主机上会进入不同 transform 和 launcher。下面的时序图展示共享策略如何在平台 选择点之后分流,同时保留统一的成功或准备失败结果。

分流发生在强制层而不是协议层:Core 仍接收同一种 PermissionProfile,平台后端只负责把它转换为 本机可以执行的隔离机制。

4. PTY 与进程树 ​

交互命令的跨平台难点不只是“有没有终端”,还包括 resize、输入规范化、Ctrl-C、取消时清理后代 进程,以及正常退出后是否允许后台后代继续存在。

codex-utils-pty 在 Unix 使用 portable_pty::native_pty_system(),在 Windows 使用自有 ConPtySystem:

源码位置:codex-rs/utils/pty/src/pty.rs :: platform_native_pty_system

rust
fn platform_native_pty_system() -> Box<dyn portable_pty::PtySystem + Send> {
    // cfg 让每个目标只编译自己的 PTY 实现,避免运行时探测不存在的后端。
    #[cfg(windows)]
    {
        Box::new(crate::win::ConPtySystem::default())
    }

    #[cfg(not(windows))]
    {
        native_pty_system()
    }
}

源码位置:codex-rs/utils/pty/src/pty.rs :: platform_native_pty_system。

ConPTY capability 与 backend factory 分开:Windows 要实际探测系统 API,非 Windows 则恒为 true,供 上层保持统一 gate。

源码位置:codex-rs/utils/pty/src/pty.rs :: conpty_supported, PtyChildTerminator::kill

rust
#[cfg(windows)]
pub fn conpty_supported() -> bool {
    // Windows 不能因编译成功就假设当前系统具备可用 ConPTY。
    crate::win::conpty_supported()
}

#[cfg(not(windows))]
pub fn conpty_supported() -> bool {
    true
}

impl ChildTerminator for PtyChildTerminator {
    fn signal(&mut self, signal: ProcessSignal) -> std::io::Result<()> {
        match signal {
            ProcessSignal::Interrupt => {
                #[cfg(unix)]
                if let Some(process_group_id) = self.process_group_id {
                    // Unix interrupt 发给 process group,覆盖 shell 派生的子进程。
                    return crate::process_group::interrupt_process_group(process_group_id);
                }
                Err(crate::process::unsupported_signal(signal))
            }
        }
    }

    fn kill(&mut self) -> std::io::Result<()> {
        #[cfg(unix)]
        if let Some(process_group_id) = self.process_group_id {
            // 同时 kill process group 与 direct child,处理 descendant 和陈旧 PGID 两类风险。
            let process_group_kill_result =
                crate::process_group::kill_process_group(process_group_id);
            let child_kill_result = self.killer.kill();
            return match child_kill_result {
                Ok(()) => Ok(()),
                Err(err) if err.kind() == ErrorKind::NotFound => process_group_kill_result,
                Err(err) => process_group_kill_result.or(Err(err)),
            };
        }
        self.killer.kill()
    }
}

Unix 的终止对象同时追踪 child killer 与 process group;Windows 的具体后端则使用自己的进程树机制。 因此跨平台抽象统一的是“中断/终止意图”,不是底层 syscall。

能力macOS / LinuxWindows
PTYnative PTYConPTY
最低系统条件OS PTY APIWindows build ≥ 17763
resizePTY resizeResizePseudoConsole
中断向 process group 发送 interrupt受后端支持的 Windows 信号/控制路径
取消整棵进程树kill process groupterminate Job Object
正常根进程退出可按 session/process group 语义处理可撤销 kill-on-close,保留被允许的 descendants
环境继承spawn 前 env_clear() 后显式注入同样先清空,再显式注入

Unix 子进程通过 setsid 建立 session/process group,PTY child 的 PID 可作为 PGID;中断和 hard kill 面向整个 group,避免交互 shell 的后代在取消后残留。Linux pipe 路径还设置 parent-death signal, 进一步约束父进程异常退出。

Windows 用 Job Object 表达进程树所有权。安全的创建路径先以 CREATE_SUSPENDED 启动 root process, 把它原子加入 Job,再用 NtResumeProcess 恢复执行,避免子进程在纳管之前逃逸。Job 默认启用 kill-on-close;正常退出如果允许 descendants 存活,会先修改 limit flag。普通 Tokio pipe spawn 后 再分配 Job 仍存在一个很小的竞态,因此源码把失败后的“只能终止 root process”保留为显式降级,而 不是宣称两条路径完全等价。

PTY 与进程树抽象只统一上层操作意图,具体 owner 仍是平台实现。下面的类图标出 portable PTY、Unix process group 与 Windows Job Object 的对应关系。

PtySystem 统一创建与 resize 能力,终止整棵进程树却分别落到 process group 和 Job Object。跨平台 代码只能依赖公共意图,不能假设两端共享 PID、signal 或 session 语义。

4.1 ConPTY 能力 ​

conpty_supported() 通过 RtlGetVersion 读取 Windows build number,要求至少 17763。工具配置选择 unified exec 时会先检查这一条件:

源码位置:codex-rs/tools/src/tool_config.rs :: shell_type_for_model_and_features

rust
// Windows 只有 ConPTY 探测成功才选择 UnifiedExec,否则显式降级。
if codex_utils_pty::conpty_supported() {
    ConfigShellToolType::UnifiedExec
} else {
    ConfigShellToolType::ShellCommand
}

源码位置:codex-rs/tools/src/tool_config.rs :: shell_type_for_model_and_features。

因此旧 Windows 的结果是回退到普通 shell command,不是用一个不工作的伪终端继续暴露 unified exec。代价是失去可持续写入 stdin、PTY resize 和统一进程会话等能力;一次性命令仍可运行。

5. Shell 能运行 ​

Codex 把 shell 归一为 Zsh、Bash、Sh、PowerShell 和 Cmd。默认探测顺序体现平台惯例:

平台默认首选后备顺序命令参数
macOS用户 shellzsh → bash → /bin/sh-lc 或 -c
Linux用户 shellbash → zsh → /bin/sh-lc 或 -c
WindowsPowerShellpwsh → Windows PowerShell → cmd.exe-Command,Cmd 使用 /c

PowerShell 非 login 调用会加 -NoProfile;Posix shell 用 -c/-lc 区分普通与 login 环境。这些分支 由 shell-command/src/shell_detect.rs 和 core/src/shell.rs::derive_exec_args 共同完成。

所有平台最终都经过同一个环境过滤策略,避免把宿主环境无条件透传给子进程:

源码位置:codex-rs/protocol/src/config_types.rs :: ShellEnvironmentPolicy

rust
pub enum ShellEnvironmentPolicyFilter {
    Include,
    Exclude,
}

pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;

pub struct ShellEnvironmentPolicy {
    // inherit 只决定初始集合,后续 exclude/set/include_only 仍会继续收敛。
    pub inherit: ShellEnvironmentPolicyInherit,
    pub ignore_default_excludes: bool,
    pub exclude: Vec<EnvironmentVariablePattern>,
    // 显式 set 位于过滤流程中间,最后仍受 include_only 约束。
    pub r#set: HashMap<String, String>,
    pub include_only: Vec<EnvironmentVariablePattern>,
    // use_profile 改变 shell 初始化语义,因此不是普通环境变量开关。
    pub use_profile: bool,
}

impl Default for ShellEnvironmentPolicy {
    fn default() -> Self {
        Self {
            inherit: ShellEnvironmentPolicyInherit::All,
            // 默认排除规则是否启用由这里明确给出,不依赖宿主 shell 默认行为。
            ignore_default_excludes: true,
            exclude: Vec::new(),
            r#set: HashMap::new(),
            include_only: Vec::new(),
            use_profile: false,
        }
    }
}

这套结构统一策略形状,但变量名大小写、profile 脚本和 shell quoting 仍服从各平台实现;共享 struct 不意味着最终环境字节级一致。

shell snapshot 是一项明确的不对称能力。它只在本地 Posix shell 捕获环境;remote environment 直接 返回 None,PowerShell 和 Cmd 当前都在入口处 bail!。所以 Windows 上“命令能执行”不能推出 “Codex 能预先捕获并复用完整 shell 初始化环境”。基于 zsh fork 的执行优化也由 cfg!(unix) 保护, 不是 Windows 的隐藏路径。

命令安全分析同样按 shell 分流。Windows PowerShell 使用 AST/safelist 识别一小部分明确只读调用, 不能可靠证明安全的命令保持保守审批;Unix shell 则使用自己的命令解析与安全规则。跨平台脚本若只 替换路径分隔符,仍可能因为 quoting、profile、pipeline 和可执行文件解析差异产生不同策略结果。

6. TUI 与通知 ​

TUI 使用 crossterm/ratatui 共享渲染层。启动时三端都启用 raw mode 与 bracketed paste;Windows 还 显式打开 ENABLE_VIRTUAL_TERMINAL_PROCESSING,并切换 console input record mode。旧终端不支持 keyboard enhancement 时,初始化会继续运行,而不是让整个 TUI 失败。

通知实现尤其容易被误认为 OS notification。DesktopNotificationBackend 实际只有两种终端后端:

  • Osc9:向终端写 OSC 9 escape sequence;tmux 下套 DCS passthrough;
  • Bel:向终端写 BEL 字节,由终端决定响铃、闪烁还是生成桌面通知。

Auto 仅为 Ghostty、iTerm2、Kitty、Warp 和 WezTerm 选择 OSC 9;Apple Terminal、Alacritty、 GNOME Terminal、Konsole、VS Code、VTE、Windows Terminal 和未知终端都回退 BEL。这里没有 notify-send、osascript 或 Windows toast 的内建统一实现。因此通知表现主要取决于终端 emulator 和其设置,而不是只取决于 macOS/Linux/Windows。

还有一个 Windows 特例:TUI 初始化时主动 DisableFocusChange,而非 Windows 才 EnableFocusChange。如果通知条件设为“仅 unfocused”,就不能假设 Windows 与能报告焦点事件的 终端有相同触发效果;需要“始终通知”时应使用相应 condition,而不是依赖不可见的 OS 窗口状态。

剪贴板采用分层降级,进一步说明“终端环境”有时比“主机 OS”更重要:

环境首选路径失败后的路径
本地 macOS / Linux / Windowsarboard 原生剪贴板终端 OSC 52;tmux 优先其 clipboard path
SSHtmux clipboard 或 OSC 52不写远端主机的本地剪贴板
WSLLinux arboard调 Windows PowerShell,再回退终端协议
Linux X11/部分 Waylandarboard保留 ClipboardLease,避免 owner 过早退出导致内容消失

图像粘贴也优先 arboard;WSL 失败时调用 Windows PowerShell 把图像保存为临时 PNG,再将 Windows 路径转换为 /mnt/<drive>/...。这是专门的 WSL 桥接,不意味着普通 Linux 会依赖 PowerShell。

7. 路径模型 ​

如果 App Server、client 和 executor 不在同一系统,直接传 PathBuf 会立即失去语义: Windows drive path 在 Linux 是普通含冒号和反斜杠的文本,/workspace/src 在 Windows 也不是同一种绝对路径。 Codex 用 PathUri 把协议路径规范化为 file: URI:

原生路径PathUri
/workspace/src/main.rsfile:///workspace/src/main.rs
WindowsDrive\\workspace\\src\\main.rsfile:///C:/workspace/src/main.rs
\\server\share\src\main.rsfile://server/share/src/main.rs

PathUri 不只是字符串包装。它会规范 Windows drive letter;Windows 路径比较采用 ASCII case-insensitive,Posix 路径保持 case-sensitive;UNC share root 不允许 parent() 穿越;包含编码 分隔符的 containment 检查 fail closed。无法正常表示的非 UTF-8 或特殊 Windows namespace 路径会 进入保留的 opaque fallback URI,确保序列化仍可往返而不冒充普通路径。

最重要的约束在 to_abs_path():只有 URI 推断出的 path convention 与当前 executor 主机一致时, 才转换为可执行路径。Windows URI 可以在 Linux 上展示为 Windows 风格字符串,但不能直接变成 Linux 绝对路径交给 sandbox。SandboxManager 在转换 command cwd 和 policy cwd 时会传播这个错误。

当前 PathUri 还没有携带 environment identifier,path convention 主要从 URI spelling 推断;源码 中的 TODO 计划将来优先使用 executor 声明的 convention。现阶段写远端客户端时,不能用“本机能够 parse 这个 URI”替代“executor 能把它转换为自己的 native path”。

7.1 WSL 架构 ​

WSL 进程的 target_os 是 Linux,因此 shell、sandbox、PTY 和 package 选择首先遵循 Linux 规则。 只有少数边界显式跨到 Windows:

  • launcher 参数中的 Windows drive path 可由 normalize_for_wsl() 转为 WSL mount path;
  • 剪贴板原生访问失败时可调用 powershell.exe;
  • Windows 临时图片路径再映射回 WSL mount path;
  • WSL1 在需要 bubblewrap 时被明确拒绝。

所以“在 Windows 机器上运行 WSL Codex”与“运行 win32 Codex”是两种执行环境。前者不能使用 Windows restricted-token sandbox,后者也不会自动使用 Linux bubblewrap。

8. 能力总表 ​

把各层合并后,可以得到更接近运行事实的矩阵:

能力macOSLinuxWindows
npm 原生包 x64/arm64原生支持原生支持原生支持
TUI / exec / App Server原生支持原生支持原生支持
codex app原生支持不提供原生支持
内建文件沙箱Seatbeltbubblewrap,条件支持需启用 restricted/elevated sandbox
内建网络强制Seatbelt + proxynamespace/seccomp + proxy需 Windows sandbox;managed proxy 选择 elevated backend
PTYnative PTYnative PTYConPTY,要求 build ≥ 17763
unified exec原生支持原生支持ConPTY 可用时支持,否则降为 shell command
进程树取消process groupprocess group,另有 parent-death signalJob Object;部分 pipe 路径有纳管竞态降级
默认 shell用户 shell,偏好 zsh用户 shell,偏好 bashPowerShell,最终回退 Cmd
shell snapshot本地 Posix 支持本地 Posix 支持PowerShell/Cmd 不提供
OSC 9 / BEL 通知取决于终端取决于终端取决于终端;Windows Terminal 的 Auto 路径为 BEL
unfocused 焦点通知终端报告焦点时支持终端报告焦点时支持focus change 被禁用,不能按同等语义假设
PathUri native 执行Posix conventionPosix conventionWindows drive/UNC convention
WSL 特殊桥接不适用WSL 中条件支持原生 win32 进程不走此路径

“条件支持”后面的条件应当进入故障报告。只写“Windows unified exec 失败”信息不足;至少还需要 Windows build、conpty_supported() 结果和实际选中的 tool type。只写“Linux sandbox 失败”也不足; 需要区分 helper 缺失、bwrap 不可用、WSL1、namespace/proc 限制和 policy 本身不被 legacy Landlock 表达。

9. 平台矩阵校准 ​

跨平台结论需要三种证据配合:cfg 决定入口能否存在,纯函数测试验证平台规则的输入输出,平台集成 测试才验证 helper、内核或系统 API。只引用其中一种,都容易把“能编译”“逻辑正确”和“当前机器可运行” 混为一谈。

9.1 codex app构建 ​

CLI 对 desktop 模块、枚举 variant 和 match arm 使用相同的 cfg。Linux 构建不是解析到 App 后再 返回 unsupported,而是根本没有这些 Rust 项;因此文档矩阵中的“不提供”来自编译边界。

源码位置:codex-rs/cli/src/main.rs :: desktop模块与Subcommand::App

rust
// cfg为false时模块不进入当前target的编译图,不是运行时关闭。
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod app_cmd;
#[cfg(any(target_os = "macos", target_os = "windows"))]
mod desktop_app;

枚举中的平台专属 variant 也由同一条件控制:

源码位置:codex-rs/cli/src/main.rs :: Subcommand::App

rust
/// Launch the Desktop app (opens the app installer if missing).
// Linux target的clap命令模型中不存在这个variant。
#[cfg(any(target_os = "macos", target_os = "windows"))]
App(app_cmd::AppCommand),

分派处再次使用同一条件,保证 enum 与处理分支不会在目标之间错位。

源码位置:codex-rs/cli/src/main.rs :: run_main Subcommand::App分支

rust
// 分派分支与variant共享同一cfg,避免某个target出现悬空match arm。
#[cfg(any(target_os = "macos", target_os = "windows"))]
Some(Subcommand::App(app_cli)) => {
    reject_remote_mode_for_subcommand(
        root_remote.as_deref(),
        root_remote_auth_token_env.as_deref(),
        "app",
    )?;
    app_cmd::run_app(app_cli).await?;
}

这里的验证方式应是分别对 Linux 与 Darwin/Windows target 编译或检查 CLI help,而不是只在 macOS 上 运行一次 codex app。单一 host 的运行结果不能证明另一个 target 的 variant 是否存在。

9.2 WSL1 失败测试 ​

wsl1_rejects_linux_bubblewrap_path 只在 Linux target 编译,但通过参数显式注入 is_wsl1=true,因此是 平台规则 fixture,而不是实际 WSL 内核集成测试。三组断言分别覆盖受限文件系统、代理网络和 legacy Landlock 加代理;只要仍需要 bubblewrap,就必须返回同一错误。

源码位置:codex-rs/sandboxing/src/manager_tests.rs

rust
// :: wsl1_rejects_linux_bubblewrap_path(关键断言)
#[cfg(target_os = "linux")]
#[test]
fn wsl1_rejects_linux_bubblewrap_path() {
    // 测试显式注入WSL1事实,验证选择规则,不要求runner本身位于WSL1。
    let restricted_policy = FileSystemSandboxPolicy::restricted(vec![
        FileSystemSandboxEntry {
            path: FileSystemPath::Special {
                value: FileSystemSpecialPath::Root,
            },
            access: FileSystemAccessMode::Read,
            missing_path_behavior: None,
        },
    ]);

    assert!(matches!(
        super::ensure_linux_bubblewrap_is_supported(
            &restricted_policy,
            /*use_legacy_landlock*/ false,
            /*allow_network_for_proxy*/ false,
            /*is_wsl1*/ true,
        ),
        Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
    ));
    assert!(matches!(
        super::ensure_linux_bubblewrap_is_supported(
            &FileSystemSandboxPolicy::unrestricted(),
            /*use_legacy_landlock*/ false,
            /*allow_network_for_proxy*/ true,
            /*is_wsl1*/ true,
        ),
        Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
    ));
}

真正启动 bubblewrap 的 Linux suite 还有额外环境门槛。should_skip_bwrap_tests() 先运行探针;缺少 helper/user namespace、/proc mount 被容器禁止,或者探针超时,都会跳过依赖真实 bwrap 的断言。

源码位置:codex-rs/linux-sandbox/tests/suite/landlock.rs

rust
// :: should_skip_bwrap_tests(完整判定)
async fn should_skip_bwrap_tests() -> bool {
    match run_cmd_result_with_writable_roots(
        &["bash", "-lc", "true"],
        &[],
        NETWORK_TIMEOUT_MS,
        /*use_legacy_landlock*/ false,
        /*network_access*/ true,
    )
    .await
    {
        Ok(output) => is_bwrap_unavailable_output(&output),
        Err(err) => match err.details() {
            CodexErrorDetails::Sandbox(SandboxErr::Denied { output, .. }) => {
                is_bwrap_unavailable_output(output)
            }
            // 探针超时说明fixture不可用,不把它误判为策略实现失败。
            CodexErrorDetails::Sandbox(SandboxErr::Timeout { .. }) => true,
            details => panic!("bwrap availability probe failed unexpectedly: {details:?}"),
        },
    }
}

因此 CI 中这组测试为绿,可能表示真实 bwrap 断言通过,也可能表示前提不可用而跳过。判断平台能力时还 必须查看测试日志中的 skipping bwrap test,不能只看最终 exit code。

9.3 Windows拒绝测试 ​

exec_tests.rs 中的 permission-shape 测试可在普通 host 验证纯函数决策;例如 deny-read 不能交给 unelevated restricted-token 时,结果必须是明确错误,而不是 None。但这仍没有启动 Windows helper。

源码位置:codex-rs/core/src/exec_tests.rs

rust
// :: windows_restricted_token_rejects_unreadable_split_carveouts(关键断言)
// 这是permission-shape纯逻辑测试,没有启动Windows helper。
assert_eq!(
    resolve_windows_restricted_token_filesystem_overrides(
        SandboxType::WindowsRestrictedToken,
        &permission_profile,
        &cwd,
        WindowsSandboxLevel::RestrictedToken,
    ),
    Err(
        "windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
            .to_string()
    )
);

进程级 suite 则只在 Windows target 被模块级 cfg 纳入,并用 cmd.exe 走真实 process_exec_tool_call()。它断言错误穿过执行入口,且没有把 unsupported profile 静默改成无沙箱命令。

源码位置:codex-rs/core/tests/suite/mod.rs

rust
// 只有Windows runner会编译并执行进程级sandbox suite。
#[cfg(target_os = "windows")]
mod windows_sandbox;

模块中的对应集成断言如下。它验证的是 restricted-token 的 fail-closed 路径;elevated backend 的成功测试 还需要 staged helper、sandbox identity、ACL 和串行化的 CODEX_HOME fixture。

源码位置:codex-rs/core/tests/suite/windows_sandbox.rs

rust
// :: windows_restricted_token_rejects_exact_and_glob_deny_read_policy(结果断言)
let err = process_exec_tool_call(
    ExecParams {
        command: vec![
            "cmd.exe".to_string(),
            "/D".to_string(),
            "/C".to_string(),
            "type secret.env >NUL 2>NUL & echo exact secret 1>future.env 2>NUL & type future.env 2>NUL & type public.txt & exit /B 0"
                .to_string(),
        ],
        cwd: cwd.clone(),
        expiration: 10_000.into(),
        capture_policy: ExecCapturePolicy::ShellTool,
        env: HashMap::new(),
        network: None,
        network_environment_id: None,
        sandbox_permissions: SandboxPermissions::UseDefault,
        windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
        windows_sandbox_private_desktop: false,
        justification: None,
        arg0: None,
    },
    &permission_profile,
    &cwd,
    std::slice::from_ref(&cwd),
    &None,
    /*use_legacy_landlock*/ false,
    /*stdout_stream*/ None,
)
.await
.expect_err("restricted-token sandbox should reject deny-read restrictions");

// 平台能力不足必须显式失败,不能退化成宿主权限执行。
assert_eq!(
    err.to_string(),
    "unsupported operation: windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
);

9.4 PathUri平台选择 ​

non_native_uri_io_conversion_is_invalid_input 在 Unix 上用 Windows drive/UNC URI,在 Windows 上用 Posix URI。两端都要求解析成功但 to_abs_path() 失败为 InvalidInput:传输层识别 foreign path,与 执行层拒绝把它投影成本机路径是两个连续断言。

源码位置:codex-rs/utils/path-uri/src/tests.rs

rust
// :: non_native_uri_io_conversion_is_invalid_input(完整测试)
#[test]
fn non_native_uri_io_conversion_is_invalid_input() {
    // 当前target选择相反的路径约定作为foreign fixture。
    #[cfg(unix)]
    let uris = ["file://server/share/file.txt", "file:///C:/workspace"];
    #[cfg(windows)]
    let uris = ["file:///usr/local/file.txt"];

    for uri in uris {
        let uri = PathUri::parse(uri).expect("valid file URI");
        let error = uri
            .to_abs_path()
            .expect_err("URI should not be host-native");

        assert_eq!(
            (error.kind(), error.to_string()),
            (
                io::ErrorKind::InvalidInput,
                format!("'{uri}' is invalid on '{}'", std::env::consts::OS),
            )
        );
    }
}

这类单个测试可以在两类 target 复用,是因为 foreign 输入由 cfg 选择;它仍要求 CI 真正构建并运行 相应 target。交叉编译但不执行,只能证明代码可编译,不能证明 host-specific assertion 通过。

9.5 测试适用范围 ​

测试材料能支持什么结论不能支持什么结论
#[cfg(target_os = ...)]项是否进入该 target 的编译图后端在当前机器可运行
纯函数 fixture给定平台条件时选择或拒绝结果正确helper、内核、ACL、终端 API 正常
带 skip 的集成测试前提满足时真实后端行为被 skip 的环境也支持
平台专属进程测试该 runner 上真实命令与副作用其他 OS 或版本具有相同行为
跨平台协议测试共享数据模型保持约定每个平台执行器都能消费全部输入

没有一条测试能在单一 runner 上证明整张 macOS/Linux/Windows 矩阵;这种测试本来也不现实。 可信结论来自目标矩阵、平台专属 runner、skip 日志和共享协议测试的组合,而不是把 Linux CI 的绿色结果 外推到 Windows Job Object 或 macOS Seatbelt。

以 PathUri 的 foreign fixture 为例,测试输入是当前 target 反向选择的 drive/Posix URI,动作是调用 to_abs_path,断言是返回 InvalidInput;它证明共享路径模型拒绝宿主不认识的 convention,不证明真实 Windows filesystem 或 Linux sandbox 已启动。WSL1 fixture 的输入则显式注入 is_wsl1、权限和 proxy 条件,断言是 bubblewrap 路径被拒绝;该测试只证明选择规则。真正启动 bwrap、restricted token 或 ConPTY 的测试必须在对应 OS runner 上执行,若日志显示 skip,只能说明前置条件不可用,不能当作后端通过。

10. 平台边界定位 ​

现象首先检查关键源码入口
npm 启动即报 unsupported/missing dependencyNode platform、arch、optional package 是否安装codex-cli/bin/codex.js
Linux 命令在 sandbox 准备前失败helper 路径、system/bundled bwrap、WSL 版本linux-sandbox/src/launcher.rs、sandboxing/src/manager.rs
Windows 显示需要沙箱却没有后端WindowsSandboxLevel 是否仍为 Disabledcore/src/windows_sandbox.rs
Windows 权限配置被拒绝restricted token 是否无法表达 deny-read/split-readsandboxing/src/windows.rs
unified exec 退化成一次性命令Windows build 与 ConPTY 探测utils/pty/src/win/psuedocon.rs、tools/src/tool_config.rs
Ctrl-C 后仍有后代进程Unix PGID 或 Windows Job 纳管是否建立utils/pty/src/pty.rs、utils/pty/src/win/job.rs
TUI 完成任务却没有桌面通知notification condition、终端识别、OSC 9/BEL 设置tui/src/notifications、tui/src/tui.rs
远端 cwd 看似合法却无法执行URI convention 是否与 executor 一致utils/path-uri/src/lib.rs::to_abs_path
WSL 剪贴板或安装路径失败PowerShell bridge 与 drive mount 映射tui/src/clipboard_*、cli/src/wsl_paths.rs
Linux 找不到 codex app不是安装损坏;该子命令未在 Linux 编译cli/src/main.rs、cli/src/desktop_app

最后可以把跨平台判断压缩成一条顺序:先确认交付 target,再确认编译期入口,然后查看 runtime 后端 与前置条件,最后检查执行边界的 native path 和终端能力。任何一层失败,都不应该由上一层的“已 支持”来掩盖;而只有保持语义仍成立的情况,才适合称为降级。

可以用下面的只读搜索把本文的 platform selection 主线落回源码:

bash
rg -n "cfg\(|SandboxManager|ConPTY|bubblewrap|Seatbelt" codex-rs