跨平台能力矩阵
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,平台差异并没有集中到一个抽象接口中,而是在四个层次 逐步收敛:
- npm launcher 按 Node 的
platform + arch选择原生包; - Rust CLI 用
cfg(target_os)决定子命令和模块是否存在; - sandbox、PTY、进程树和终端层选择具体 OS 后端;
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
// 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_64 | arm64 | 原生文件名 | launcher 行为 |
|---|---|---|---|---|
| macOS | codex-darwin-x64 | codex-darwin-arm64 | codex | 启动 Darwin 二进制 |
| Linux | codex-linux-x64 | codex-linux-arm64 | codex | 启动 musl Linux 二进制 |
| Windows | codex-win32-x64 | codex-win32-arm64 | codex.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 二进制中没有这一 入口。
| 入口 | macOS | Linux | Windows | 差异点 |
|---|---|---|---|---|
codex TUI | 原生支持 | 原生支持 | 原生支持 | 终端输入、焦点和剪贴板后端不同 |
codex exec | 原生支持 | 原生支持 | 原生支持 | shell、PTY、进程树和沙箱不同 |
codex app-server | 原生支持 | 原生支持 | 原生支持 | executor 所在平台决定实际执行能力 |
codex sandbox | Seatbelt 子命令 | Linux sandbox 子命令 | Windows sandbox 子命令 | 参数和强制机制不同 |
codex app | 打开或安装 Codex.app | 不提供 | 打开或引导安装 Windows App | Linux 在编译期无此子命令 |
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
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。
| 维度 | macOS | Linux | Windows |
|---|---|---|---|
SandboxType | MacosSeatbelt | LinuxSeccomp | 启用时为 WindowsRestrictedToken,否则 None |
| 文件系统后端 | Seatbelt profile | 默认 bubblewrap mount namespace | restricted-token 或 elevated backend 的 token、ACL 与 capability 规则 |
| 网络后端 | Seatbelt 网络规则与受管代理路径 | namespace/seccomp 与受管代理路径 | Windows 网络强制;受管代理会选择 elevated backend |
| 额外程序 | /usr/bin/sandbox-exec | codex-linux-sandbox 与可用的 system/bundled bwrap | Windows 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 的实际默认顺序是:
- 外层进程用 bubblewrap 构造只读为主、按 policy 开放写 roots 的文件系统视图;
- 在隔离环境中重新进入 helper;
- 设置
no_new_privs,按网络策略安装 seccomp filter; 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
// 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
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
#[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 / Linux | Windows |
|---|---|---|
| PTY | native PTY | ConPTY |
| 最低系统条件 | OS PTY API | Windows build ≥ 17763 |
| resize | PTY resize | ResizePseudoConsole |
| 中断 | 向 process group 发送 interrupt | 受后端支持的 Windows 信号/控制路径 |
| 取消整棵进程树 | kill process group | terminate 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
// 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 | 用户 shell | zsh → bash → /bin/sh | -lc 或 -c |
| Linux | 用户 shell | bash → zsh → /bin/sh | -lc 或 -c |
| Windows | PowerShell | pwsh → 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
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 / Windows | arboard 原生剪贴板 | 终端 OSC 52;tmux 优先其 clipboard path |
| SSH | tmux clipboard 或 OSC 52 | 不写远端主机的本地剪贴板 |
| WSL | Linux arboard | 调 Windows PowerShell,再回退终端协议 |
| Linux X11/部分 Wayland | arboard | 保留 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.rs | file:///workspace/src/main.rs |
WindowsDrive\\workspace\\src\\main.rs | file:///C:/workspace/src/main.rs |
\\server\share\src\main.rs | file://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. 能力总表
把各层合并后,可以得到更接近运行事实的矩阵:
| 能力 | macOS | Linux | Windows |
|---|---|---|---|
| npm 原生包 x64/arm64 | 原生支持 | 原生支持 | 原生支持 |
| TUI / exec / App Server | 原生支持 | 原生支持 | 原生支持 |
codex app | 原生支持 | 不提供 | 原生支持 |
| 内建文件沙箱 | Seatbelt | bubblewrap,条件支持 | 需启用 restricted/elevated sandbox |
| 内建网络强制 | Seatbelt + proxy | namespace/seccomp + proxy | 需 Windows sandbox;managed proxy 选择 elevated backend |
| PTY | native PTY | native PTY | ConPTY,要求 build ≥ 17763 |
| unified exec | 原生支持 | 原生支持 | ConPTY 可用时支持,否则降为 shell command |
| 进程树取消 | process group | process group,另有 parent-death signal | Job Object;部分 pipe 路径有纳管竞态降级 |
| 默认 shell | 用户 shell,偏好 zsh | 用户 shell,偏好 bash | PowerShell,最终回退 Cmd |
| shell snapshot | 本地 Posix 支持 | 本地 Posix 支持 | PowerShell/Cmd 不提供 |
| OSC 9 / BEL 通知 | 取决于终端 | 取决于终端 | 取决于终端;Windows Terminal 的 Auto 路径为 BEL |
| unfocused 焦点通知 | 终端报告焦点时支持 | 终端报告焦点时支持 | focus change 被禁用,不能按同等语义假设 |
PathUri native 执行 | Posix convention | Posix convention | Windows 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
// 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
/// 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分支
// 分派分支与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
// :: 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
// :: 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
// :: 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
// 只有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
// :: 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
// :: 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 dependency | Node 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 是否仍为 Disabled | core/src/windows_sandbox.rs |
| Windows 权限配置被拒绝 | restricted token 是否无法表达 deny-read/split-read | sandboxing/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 主线落回源码:
rg -n "cfg\(|SandboxManager|ConPTY|bubblewrap|Seatbelt" codex-rs