Skip to content

Codex威胁模型

以 Unified Exec 为主线,区分模型输入、审批、权限配置、文件与网络强制、远程执行器和非进程工具各自承担的安全责任。

基于rust-v0.150.0
CodexRustSecuritySandbox

Codex威胁模型 ​

Codex 的安全问题不能简化成“有没有开启 sandbox”。一次 exec_command 至少包含五类独立决定:模型请求执行什么、当前 environment 绑定哪组权限、审批系统是否授权、谁负责落实文件与网络限制、结果又通过哪条通道返回。任何一层缺失,都不能由另一层自动补齐。

威胁模型首先要回答三件事:保护什么、信任谁、在哪个边界强制。对 Codex 而言,主要资产包括工作区与工作区之外的文件、进程环境中的凭据、网络访问能力、用户批准所表达的意图、对话与工具输出,以及远程执行环境中的资源。输入则可能来自模型、仓库中的指令和文件、插件或 MCP server、网络服务、用户配置与远程 executor;它们的可信度并不相同。

本文承接Codex信任边界与ToolOrchestrator执行流程,使用 Unified Exec 的真实调用链建立安全地图。下一篇会继续拆解审批、策略与操作系统强制三层;本文先回答更基础的问题:每层能阻止什么,不能替谁做决定。

图中最重要的分叉是“执行所有者”。Core 可以选择安全策略,但本地主机与远程 executor 的强制位置不同;MCP、图片读取等非进程工具又有自己的边界,不能拿 shell sandbox 的结论直接套用。

1. 资产与攻击面 ​

以一次模型发起的 exec_command 为例,攻击面不是只有命令字符串。

输入或资产可能的威胁主要责任边界
命令与参数命令注入、破坏性操作、越权读写exec policy、审批、进程 sandbox
cwd 与 workspace roots路径逃逸、错误主机路径解释TurnEnvironment、PathUri、executor
文件权限读取凭据、修改源码或仓库元数据PermissionProfile、文件 sandbox
环境变量凭据泄露、代理与 PATH 劫持shell environment policy、启动代码
网络能力数据外传、访问内网或未批准域名network policy、managed proxy
用户批准提示欺骗、批准范围被扩大或复用centralized approval、cache key
MCP/插件结果外部副作用、恶意内容回流模型MCP policy、审批、server 信任
远程 executor策略未落实、能力谎报、连接被替换exec-server 协议、executor sandbox、认证层

模型是请求来源,不是授权主体。即使模型明确写出 sandbox_permissions: require_escalated,它表达的也只是升级请求;是否允许、允许到什么范围,仍取决于环境权限、审批策略和执行所有者。

2. 权限绑定环境 ​

rust-v0.150.0 的安全上下文首先绑定到选中的 TurnEnvironment,而不是只从 Session 的旧全局 cwd 与 SandboxPolicy 读取。一个 environment 同时带有 ID、cwd、workspace roots、resolved config、执行后端与 shell。

源码位置:codex-rs/core/src/session/turn_context.rs :: TurnEnvironment

rust
pub(crate) struct TurnEnvironment {
    pub(crate) selection: TurnEnvironmentSelection,
    pub(crate) config_origin: EnvironmentConfigOrigin,
    pub(crate) environment: Arc<Environment>,
    pub(crate) shell: Option<shell::Shell>,
    pub(crate) shell_snapshot: ShellSnapshotTask,
    pub(crate) shell_snapshot_v2_supported: bool,
}

pub(crate) fn cwd(&self) -> &PathUri {
    &self.selection.cwd
}

pub(crate) fn workspace_roots(&self) -> &[PathUri] {
    &self.selection.workspace_roots
}

pub(crate) fn permission_profile(&self) -> &PermissionProfile {
    self.config().permission_profile.permission_profile()
}

这组字段共同定义一次工具调用的安全语境。environment_id 防止把为本地环境批准的命令误用到另一个 executor;PathUri 允许远程 Windows 路径在本地 Unix Core 中保持 URI 语义;workspace roots 则为符号化 :project_roots 提供实际绑定。

本地由 Core 构造 sandbox 时,会把 workspace roots 物化为具体路径;直接文件工具则生成 FileSystemSandboxContext,把权限、cwd、workspace roots 与平台选项一起交给文件系统实现。

源码位置:codex-rs/core/src/session/turn_context.rs :: TurnEnvironment::sandbox_context, permission_profile_with_workspace_roots

rust
pub(crate) fn sandbox_context(
    &self,
    additional_permissions: Option<AdditionalPermissionProfile>,
) -> FileSystemSandboxContext {
    let config = self.config();
    let permissions = effective_permission_profile(
        self.permission_profile(),
        additional_permissions.as_ref(),
    );
    FileSystemSandboxContext {
        permissions: permissions.into(),
        cwd: Some(self.cwd().clone()),
        workspace_roots: self.workspace_roots().to_vec(),
        windows_sandbox_level: executor_windows_sandbox_level(
            config.windows_sandbox_level,
            self.cwd(),
        ),
        windows_sandbox_private_desktop: config.windows_sandbox_private_desktop,
        windows_sandbox_proxy_settings_mode: None,
        use_legacy_landlock: config.use_legacy_landlock,
    }
}

pub(crate) fn permission_profile_with_workspace_roots(&self) -> PermissionProfile {
    let workspace_roots = self
        .workspace_roots()
        .iter()
        .filter_map(|workspace_root| workspace_root.to_abs_path().ok())
        .collect::<Vec<_>>();
    self.permission_profile()
        .clone()
        .materialize_project_roots_with_workspace_roots(&workspace_roots)
}

因此“当前工作区可写”不是一个脱离 environment 的全局事实。相同工具名在两个 environment 上可以对应不同 cwd、根目录、网络策略和强制后端。

3. 审批链路 ​

审批回答“这次动作是否获准继续”,并不直接创建文件权限或网络连接。ToolOrchestrator 先从 tool/runtime 得到 ExecApprovalRequirement;Skip、NeedsApproval、Forbidden 三种结果决定是否进入 centralized approval。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run

rust
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
    default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});
match &requirement {
    ExecApprovalRequirement::Skip { .. } => {
        if strict_auto_review {
            let action = tool
                .approval_action(req, &tool_ctx.call_id)
                .map_err(|err| {
                    ToolError::Rejected(format!("could not prepare approval action: {err}"))
                })?;
            let approval_ctx = ApprovalContext {
                review_context: GuardianReviewContext::from(&tool_ctx.step_context),
                cancellation_token: Some(tool_ctx.cancellation_token.clone()),
                call_id: tool_ctx.call_id.clone(),
                tool_name: tool_ctx.tool_name.clone(),
                strict_auto_review,
                approval_reason: None,
                retry_reason: None,
                network_approval_context: None,
            };
            tool_ctx
                .session
                .request_approval(action, approval_ctx)
                .await?;
            already_approved = true;
        } else {
            otel.tool_decision(
                &tool_ctx.tool_name,
                otel_ci,
                &ReviewDecision::Approved,
                Some(ToolDecisionSource::Config),
            );
        }
    }
    ExecApprovalRequirement::Forbidden { reason } => {
        return Err(ToolError::Rejected(reason.clone()));
    }
    ExecApprovalRequirement::NeedsApproval { reason, .. } => {
        let action = tool
            .approval_action(req, &tool_ctx.call_id)
            .map_err(|err| {
                ToolError::Rejected(format!("could not prepare approval action: {err}"))
            })?;
        let approval_ctx = ApprovalContext {
            review_context: GuardianReviewContext::from(&tool_ctx.step_context),
            cancellation_token: Some(tool_ctx.cancellation_token.clone()),
            call_id: tool_ctx.call_id.clone(),
            tool_name: tool_ctx.tool_name.clone(),
            strict_auto_review,
            approval_reason: reason.clone(),
            retry_reason: None,
            network_approval_context: None,
        };
        tool_ctx
            .session
            .request_approval(action, approval_ctx)
            .await?;
        already_approved = true;
    }
}

strict auto review 可以让原本 Skip 的动作仍接受审查;Forbidden 在运行前失败;NeedsApproval 必须得到成功决定。

所有审批动作进入同一个 Session::request_approval。决策优先级是 Permission Request Hook,其次才是 Guardian 或用户;Hook 的 Allow/Deny 会直接形成 resolution,未返回决定才路由 reviewer。

源码位置:codex-rs/core/src/tools/approvals.rs :: Session::request_approval

rust
let resolution = match run_permission_request_hooks(
    self,
    ctx.review_context.turn(),
    &permission_request_run_id,
    action.permission_request_payload(),
)
.await
{
    Some(PermissionRequestDecision::Allow) => ApprovalResolution {
        decision: ReviewDecision::Approved,
        source: ApprovalResolutionSource::Hook,
    },
    Some(PermissionRequestDecision::Deny { message }) => ApprovalResolution {
        decision: ReviewDecision::denied(message),
        source: ApprovalResolutionSource::Hook,
    },
    None => self.request_reviewer_approval(action, &ctx).await,
};

Guardian 是 reviewer,不是操作系统 sandbox。它可以批准、拒绝、超时或中止,但批准后的动作仍要经过 PermissionProfile、sandbox transform 和执行后端。把 Guardian 结果当成强制边界,会混淆风险判断与实际资源隔离。

4. 权限配置语义 ​

PermissionProfile 是当前运行时权限的权威模型;旧 SandboxPolicy 仍有兼容投影,但不应再作为新代码阅读的唯一入口。它明确区分三种 enforcement owner。

源码位置:codex-rs/protocol/src/models.rs :: PermissionProfile

rust
pub enum PermissionProfile {
    Managed {
        file_system: ManagedFileSystemPermissions,
        network: NetworkSandboxPolicy,
    },
    Disabled,
    External {
        network: NetworkSandboxPolicy,
    },
}

pub fn enforcement(&self) -> SandboxEnforcement {
    match self {
        Self::Managed { .. } => SandboxEnforcement::Managed,
        Self::Disabled => SandboxEnforcement::Disabled,
        Self::External { .. } => SandboxEnforcement::External,
    }
}

三种 profile 的含义不能只按“宽松程度”排序:

  • Managed:Codex 或 executor 必须根据文件与网络权限构造强制机制。
  • Disabled:不要求外层文件 sandbox,网络也被投影为 enabled。
  • External:Codex 不再添加外层文件 sandbox,但这不表示无限权限;调用方声明自己负责文件隔离,网络仍有独立策略。

文件策略同样不再只是 read-only/workspace-write 两档。FileSystemSandboxEntry 可以对 path、glob 或 special path 分别设置 Read、Write、Deny;相同精度冲突时,Deny 的优先级最高。

源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemAccessMode, FileSystemSandboxPolicy

rust
pub enum FileSystemAccessMode {
    Read,
    Write,
    #[serde(alias = "none")]
    Deny,
}

pub enum FileSystemSandboxKind {
    Restricted,
    Unrestricted,
    ExternalSandbox,
}

pub struct FileSystemSandboxPolicy {
    pub kind: FileSystemSandboxKind,
    pub glob_scan_max_depth: Option<usize>,
    pub entries: Vec<FileSystemSandboxEntry>,
}

这允许策略表达“工作区可写,但某些目录不可读”或“根目录可读,但特定凭据目录拒绝读取”。安全结论必须基于最终匹配后的权限,而不是看到某个 writable root 就推断整个子树可读写。

5. Deny约束 ​

读权限 Deny 是一个特殊安全不变量。无 sandbox 执行无法表达“允许进程运行,但禁止读取某些路径”,所以存在 denied-read 时,即使模型请求 elevated 权限或 exec policy 允许命令,也不能通过简单绕过 sandbox 来升级。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: unsandboxed_execution_allowed, sandbox_permissions_preserving_denied_reads

rust
pub(crate) fn unsandboxed_execution_allowed(
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> bool {
    !file_system_sandbox_policy.has_denied_read_restrictions()
}

pub(crate) fn sandbox_permissions_preserving_denied_reads(
    sandbox_permissions: SandboxPermissions,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> SandboxPermissions {
    if sandbox_permissions.requires_escalated_permissions()
        && !unsandboxed_execution_allowed(file_system_sandbox_policy)
    {
        SandboxPermissions::UseDefault
    } else {
        sandbox_permissions
    }
}

SandboxPermissions::RequireEscalated 通常意味着尝试绕过文件 sandbox;这里会被降回 UseDefault,保留 Deny 的唯一强制机制。同一约束既应用于 Core first attempt,也应用于 Unified Exec 的 network 与环境准备。

read deny matcher 还会同时比较原路径、规范化路径与 canonical target,阻止通过 symlink 别名绕过精确 deny。无效 glob 在直接读取检查中 fail closed,而不是把配置错误解释为 allow。

源码位置:codex-rs/protocol/src/permissions.rs :: ReadDenyMatcher::is_read_denied

rust
pub fn is_read_denied(&self, path: &Path) -> bool {
    if self.invalid_pattern {
        return true;
    }

    let path_candidates = normalized_and_canonical_candidates(path);
    if self.denied_candidates.iter().any(|denied_candidates| {
        path_candidates.iter().any(|candidate| {
            denied_candidates.iter().any(|denied_candidate| {
                candidate == denied_candidate || candidate.starts_with(denied_candidate)
            })
        })
    }) {
        return true;
    }

    self.deny_read_matchers.iter().any(|matcher| {
        path_candidates
            .iter()
            .any(|candidate| matcher.is_match(candidate))
    })
}

写权限还有另一类默认保护:restricted writable roots 下的 .git、.agents、.codex 被视为受保护元数据,除非存在明确 write entry。这样可以防止“工作区可写”自动等价为“可以修改 Git hooks 或 Codex 配置”。

6. 本地与远程强制 ​

ToolOrchestrator 先区分是否由 executor 管理进程 sandbox。Unified Exec 在远程 environment 或使用 shell snapshot 时返回 true;此时 Core 保留符号化权限,不把远程路径错误物化为本地主机路径。

源码位置:codex-rs/core/src/tools/runtimes/unified_exec.rs :: UnifiedExecRuntime::uses_executor_managed_process_sandbox

rust
fn uses_executor_managed_process_sandbox(&self, req: &UnifiedExecRequest) -> bool {
    req.turn_environment.environment.is_remote() || req.shell_snapshot.is_some()
}

本地普通执行由 SandboxManager::select_initial 根据 permission profile、tool preference、Windows level 与 managed network requirement 选择平台 wrapper。策略需要 sandbox,但当前 host 没有对应实现时,具体类型可能回落到 None;sandbox_requested 因此必须与 SandboxType 分开保存。

源码位置:codex-rs/sandboxing/src/manager.rs :: SandboxManager::select_initial, should_sandbox

rust
pub fn select_initial(
    &self,
    permission_profile: &PermissionProfile,
    pref: SandboxablePreference,
    windows_sandbox_level: WindowsSandboxLevel,
    has_managed_network_requirements: bool,
) -> SandboxType {
    if self.should_sandbox(permission_profile, pref, has_managed_network_requirements) {
        get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
            .unwrap_or(SandboxType::None)
    } else {
        SandboxType::None
    }
}

远程或 snapshot-backed 路径恰好相反:Core 必须把原始 argv 交给 exec-server,不能套用本地主机 Seatbelt/Landlock wrapper;但当 sandbox_requested 为 true 时,会附带 canonical permission profile、cwd、workspace roots 与平台选项,让 executor 在自己的主机上实施 sandbox。

源码位置:codex-rs/core/src/tools/sandboxing.rs :: SandboxAttempt::env_for_exec_server

rust
let request = self
    .manager
    .transform(SandboxTransformRequest {
        command,
        permissions: self.permissions,
        sandbox: SandboxType::None,
        enforce_managed_network: self.enforce_managed_network,
        environment_id: None,
        network: None,
        sandbox_policy_cwd: self.sandbox_cwd,
        codex_linux_sandbox_exe: None,
        use_legacy_landlock: self.use_legacy_landlock,
        windows_sandbox_level: self.windows_sandbox_level,
        windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
    })
    .map_err(CodexErr::from)?;

if self.sandbox_requested {
    exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
        permissions: exec_server_permissions.into(),
        cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
        workspace_roots: self.workspace_roots.to_vec(),
        windows_sandbox_level: executor_windows_sandbox_level(
            self.windows_sandbox_level,
            self.sandbox_cwd,
        ),
        windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
        windows_sandbox_proxy_settings_mode: None,
        use_legacy_landlock: self.use_legacy_landlock,
    });
}

这里的信任假设发生了迁移:Core 负责发送正确策略,exec-server 负责在目标 OS 上执行。远程 environment 的认证、能力声明和 executor 实现因此属于安全边界,不能用“Core 选择了 sandbox”代替对 executor 的信任判断。

7. 网络是独立能力 ​

文件 sandbox 与网络 authorization 是两个维度。environment 自带 network policy 时,显式 elevated sandbox 请求不能绕过 owner policy,Orchestrator 会在审批前直接拒绝。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run

rust
let environment = tool.turn_environment(req);
let owner_network_policy = environment.config().network_policy.is_some();
if owner_network_policy
    && tool
        .sandbox_permissions(req)
        .requires_escalated_permissions()
{
    return Err(ToolError::Rejected(
        "attachment-owned network policy cannot be bypassed by sandbox escalation"
            .to_string(),
    ));
}

允许网络时,Orchestrator 不是简单设置 network=true。NetworkApprovalSpec 保留 environment ID、permission profile、命令、cwd、附加权限和 justification;active approval 再向执行 attempt 注入 proxy 与 denial cancellation token。网络策略后续拒绝请求时,可以取消与该 execution 关联的进程。

源码位置:codex-rs/core/src/tools/runtimes/unified_exec.rs :: UnifiedExecRuntime::network_approval_spec

rust
Some(NetworkApprovalSpec {
    network,
    tool_name: ctx.tool_name.clone(),
    trigger: GuardianNetworkAccessTrigger {
        call_id: ctx.call_id.clone(),
        tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
        command: req.command.clone(),
        cwd: req.cwd.clone(),
        sandbox_permissions: req.sandbox_permissions,
        additional_permissions: req.additional_permissions.clone(),
        justification: req.justification.clone(),
        tty: Some(req.tty),
    },
    command: req.hook_command.clone(),
    environment_id: req.turn_environment.selection.environment_id.clone(),
    permission_profile: req.turn_environment.permission_profile().clone(),
    network_policy: req.turn_environment.config().network_policy.clone(),
})

network request 的归属同样需要精确关联。带 execution ID 时必须找到对应 active call,且 environment ID 要一致;缺少 execution ID 且存在多个 active call 时,归属是 ambiguous,后续 policy request 会 fail closed,而不是猜测由哪个命令负责。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::resolve_active_call_attribution

rust
match calls.active_calls.len() {
    0 => ActiveNetworkApprovalAttribution::None,
    1 => calls.active_calls.values().next().cloned().map_or(
        ActiveNetworkApprovalAttribution::None,
        ActiveNetworkApprovalAttribution::Single,
    ),
    _ => ActiveNetworkApprovalAttribution::Ambiguous,
}

这条设计保护的是“批准作用于正确 execution”。即使两个并发命令访问同一 host,也不能把其中一个命令的上下文、取消 token 或批准结果默认归给另一个命令。

8. 环境变量边界 ​

文件和网络限制不能阻止进程读取已经注入自身环境的秘密。Codex 先按 ShellEnvironmentPolicy 构造完整 env map,随后启动进程时配合 env_clear() 使用,避免系统环境在 spawn 时被隐式补回。

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

rust
pub fn create_env(
    policy: &ShellEnvironmentPolicy,
    thread_id: Option<ThreadId>,
) -> HashMap<String, String> {
    let thread_id = thread_id.map(|thread_id| thread_id.to_string());
    shell_environment::create_env(policy, thread_id.as_deref())
}

ShellEnvironmentPolicy 决定继承 All、Core 或 None,并可再做 include/exclude 与显式 set。它提供的是可配置的数据最小化,不是“默认一定移除所有凭据”的保证;实际暴露范围取决于生效 policy 与后续显式覆盖。

Codex 还注入当前 permission profile 的名字,但源码明确把它定义为 informational。子进程可以自行覆盖环境变量,所以它不能作为 sandbox 已成功强制的证明。

源码位置:codex-rs/core/src/exec_env.rs :: CODEX_PERMISSION_PROFILE_ENV_VAR, inject_permission_profile_env

rust
pub const CODEX_PERMISSION_PROFILE_ENV_VAR: &str = "CODEX_PERMISSION_PROFILE";

pub(crate) fn inject_permission_profile_env(
    env: &mut HashMap<String, String>,
    active_permission_profile: Option<&ActivePermissionProfile>,
) {
    if cfg!(windows) {
        env.retain(|key, _| !key.eq_ignore_ascii_case(CODEX_PERMISSION_PROFILE_ENV_VAR));
    } else {
        env.remove(CODEX_PERMISSION_PROFILE_ENV_VAR);
    }
    if let Some(active_permission_profile) = active_permission_profile {
        env.insert(
            CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
            active_permission_profile.id.clone(),
        );
    }
}

显式 escalation 还会移除 Codex 注入的 managed proxy 标记与代理变量,防止无 sandbox 命令意外继续经过只为受管 attempt 准备的代理;用户自己提供且没有 Codex marker 的 proxy env 则被保留。安全分析必须区分“Codex 管理的代理状态”和“用户原本的环境配置”。

9. 非进程工具 ​

进程 sandbox 只保护进程执行路径。view_image 不会先启动一个 shell 再读取文件,而是从选定 environment 获取 filesystem,并显式传入 FileSystemSandboxContext 执行 metadata 与 read 操作。

源码位置:codex-rs/core/src/tools/handlers/view_image.rs :: ViewImageHandler::handle_call

rust
let path_uri = turn_environment.cwd().join(&path).map_err(|err| {
    FunctionCallError::RespondToModel(format!(
        "unable to resolve image path `{path}` against environment cwd `{}`: {err}",
        turn_environment.cwd(),
    ))
})?;
let sandbox = turn_environment.sandbox_context(/*additional_permissions*/ None);
let fs = turn_environment.environment.get_filesystem();

let metadata = fs
    .get_metadata(&path_uri, GetMetadataOptions::default(), Some(&sandbox))
    .await
    .map_err(|error| {
        FunctionCallError::RespondToModel(format!(
            "unable to locate image at `{model_visible_path}`: {error}"
        ))
    })?;

if !metadata.is_file {
    return Err(FunctionCallError::RespondToModel(format!(
        "image path `{model_visible_path}` is not a file"
    )));
}
let file_bytes = fs
    .read_file(&path_uri, ReadFileOptions::default(), Some(&sandbox))
    .await
    .map_err(|error| {
        FunctionCallError::RespondToModel(format!(
            "unable to read image at `{model_visible_path}`: {error}"
        ))
    })?;

这证明直接文件工具需要自己携带 sandbox context,不能依赖 ToolOrchestrator 的进程 wrapper。具体 handler 如果省略 context,风险属于该工具实现边界。

MCP 工具又不同:副作用发生在 MCP server 或连接器拥有的系统中,文件 sandbox 通常无法限制远端 API。Codex 根据 server/tool metadata、approval mode、annotation 和缓存键构造 ApprovalAction::McpToolCall,然后进入同一个 centralized approval;批准解决的是调用授权,不代表 Codex 能约束 server 内部实现。

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

rust
let action = ApprovalAction::McpToolCall {
    id: call_id.to_string(),
    server: invocation.server.clone(),
    tool_name: invocation.tool.clone(),
    arguments: invocation.arguments.clone(),
    connector_id: metadata.connector_id.clone(),
    connector_name: metadata.connector_name.clone(),
    connector_description: metadata.connector_description.clone(),
    connected_account_email: (invocation.server == CODEX_APPS_MCP_SERVER_NAME)
        .then(|| metadata.connected_account_email.clone())
        .flatten(),
    tool_title: metadata.tool_title.clone(),
    tool_description: metadata.tool_description.clone(),
    annotations: metadata.annotations.as_ref().map(|annotations| GuardianMcpAnnotations {
        destructive_hint: annotations.destructive_hint,
        open_world_hint: annotations.open_world_hint,
        read_only_hint: annotations.read_only_hint,
    }),
    hook_tool_name: hook_tool_name.clone(),
    approval_policy: config.approval_policy.value(),
    reviewer: approvals_reviewer,
    approval_mode: policy.mode,
    allow_session_remember: session_approval_key.is_some(),
    allow_persistent_approval: persistent_approval_key.is_some(),
};

MCP annotations 是 reviewer 的风险输入,不是远端副作用的强制证明。一个声称 read_only_hint=true 的第三方 server 仍位于外部信任边界;配置来源、连接认证与 server 实现本身必须另行评估。

10. 失败与升级 ​

初始 sandbox 返回 denial 后,Orchestrator 不会自动改成无 sandbox 重试。它依次检查:网络 denial 是否能形成可审批上下文、工具是否允许 escalation、approval policy 是否允许再次询问、denied-read 是否允许无 sandbox,以及是否需要新的 reviewer 决定。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: sandbox denial handling

rust
if network_policy_decision.is_some() && network_approval_context.is_none() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}
if !tool.escalate_on_failure() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}
// Under `Never` or `OnRequest`, do not retry without sandbox;
// surface a concise sandbox denial that preserves the
// original output.
if !tool.wants_no_sandbox_approval(approval_policy) {
    let allow_on_request_network_prompt =
        matches!(approval_policy, AskForApproval::OnRequest)
            && network_approval_context.is_some()
            && matches!(
                default_exec_approval_requirement(
                    approval_policy,
                    &file_system_sandbox_policy
                ),
                ExecApprovalRequirement::NeedsApproval { .. }
            );
    if !allow_on_request_network_prompt {
        otel.sandbox_outcome(
            &otel_tn,
            otel_ci,
            "denied",
            initial_duration,
            /*escalated_duration*/ None,
        );
        return Err(ToolError::Codex(err));
    }
}
if !unsandboxed_allowed && network_approval_context.is_none() {
    otel.sandbox_outcome(
        &otel_tn,
        otel_ci,
        "denied",
        initial_duration,
        /*escalated_duration*/ None,
    );
    return Err(ToolError::Codex(err));
}

strict auto review 对初始 sandboxed attempt 的批准不会自动覆盖无 sandbox retry;升级会重新进入 reviewer。否则一次低风险 sandboxed 批准可能被复用为更高权限的执行授权。

11. 能保证什么 ​

机制源码层可得出的结论不能据此推出的结论
centralized approvalHook、Guardian、用户按固定优先级形成决定UI 一定让用户完整理解风险
PermissionProfile::ManagedCodex/executor 被要求实施文件与网络权限任意平台 backend 都已正确部署并实测
PermissionProfile::External文件强制责任明确交给外部 caller外部 caller 实际提供了足够隔离
denied readsCore 禁止用无 sandbox escalation 丢弃 Deny所有第三方工具都会调用相同 matcher
platform sandbox当前 host 可选择 Seatbelt、Linux sandbox 或 Windows tokensandbox 能修复被允许程序自身的逻辑漏洞
managed networkproxy、environment、execution attribution 可以关联批准与取消目标服务可信,或不存在代理之外的网络路径
shell env policyspawn 环境可被显式构造和过滤默认配置必然删除全部敏感变量
MCP approval外部工具调用可进入统一审批Codex 能限制 MCP server 内部副作用

最容易犯的错误,是把“决策存在”写成“强制已经发生”。审批、exec policy、Guardian 和 permission profile 都属于安全决策或配置;只有本地 OS sandbox、exec-server sandbox、文件系统实现和 managed proxy 才靠近最终资源边界。对于 External profile 与第三方 MCP,最终强制甚至位于 Codex 进程之外。

12. 测试反推边界 ​

以下测试分别覆盖 denied-read、executor sandbox context、环境变量与网络归属,避免只验证一条 happy path:

text
cd codex-rs
cargo test -p codex-core --lib tools::sandboxing::tests:: -- --nocapture --test-threads=1
cargo test -p codex-protocol --lib permissions::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib tools::network_approval::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib exec_env::tests:: -- --nocapture --test-threads=1
cargo test -p codex-exec-server --lib process_sandbox::tests:: -- --nocapture --test-threads=1

五组测试分别通过 11、43、26、12、6 项,共 98 项。它们覆盖策略转换、权限匹配、网络调用归属、环境构造与 executor sandbox request;没有跨平台启动所有真实 OS sandbox backend。

deny_read_blocks_explicit_escalation_and_policy_bypass 构造带 unreadable root 的 restricted policy,再分别输入显式 escalation 与 exec-policy bypass。关键断言是两条路径都保留 sandbox;它验证 Core 不会通过授权升级悄悄删除 Deny,但不验证具体 OS backend 的 syscall 拒绝。

源码位置:codex-rs/core/src/tools/sandboxing_tests.rs :: deny_read_blocks_explicit_escalation_and_policy_bypass

rust
assert_eq!(
    sandbox_override_for_first_attempt(
        SandboxPermissions::RequireEscalated,
        &ExecApprovalRequirement::Skip {
            bypass_sandbox: false,
            proposed_execpolicy_amendment: None,
        },
        &file_system_policy,
    ),
    SandboxOverride::NoOverride,
    "explicit escalation would drop deny-read filesystem policy, so keep the first attempt sandboxed",
);
assert!(!unsandboxed_execution_allowed(&file_system_policy));
assert_eq!(
    sandbox_permissions_preserving_denied_reads(
        SandboxPermissions::RequireEscalated,
        &file_system_policy,
    ),
    SandboxPermissions::UseDefault,
);

exec_server_env_keeps_command_native_and_carries_sandbox_context 输入 executor-managed attempt,断言 host wrapper 为 None、原始 command 保留,同时 exec_server_sandbox 携带 permissions、cwd 与 workspace roots。这证明强制责任被正确传递到 executor,不证明远端 executor 一定忠实执行。

源码位置:codex-rs/core/src/tools/sandboxing_tests.rs :: exec_server_env_keeps_command_native_and_carries_sandbox_context

rust
assert_eq!(
    request.command,
    vec![
        "/bin/bash".to_string(),
        "-lc".to_string(),
        "pwd".to_string()
    ]
);
assert_eq!(request.arg0, None);
assert_eq!(request.sandbox, SandboxType::None);
assert_eq!(
    request.exec_server_sandbox,
    Some(codex_exec_server::FileSystemSandboxContext {
        permissions: exec_server_permissions.clone().into(),
        cwd: Some(cwd_uri.clone()),
        workspace_roots: vec![cwd_uri.clone()],
        windows_sandbox_level: if cfg!(windows) {
            codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
        } else {
            codex_protocol::config_types::WindowsSandboxLevel::Disabled
        },
        windows_sandbox_private_desktop: false,
        windows_sandbox_proxy_settings_mode: None,
        use_legacy_landlock: false,
    })
);

multiple_active_calls_are_ambiguous_even_in_the_same_environment 注册两个 active network call,断言无 execution attribution 的请求不能被归给任意一个 caller;配套 attributed 测试则只取消匹配 execution。它验证并发网络请求不会共享猜测出的授权归属,不验证实际代理数据面。

环境测试还揭示一个重要边界:默认 Core inherit 与开启 default excludes 的行为不同。读者应把测试输入中的 policy 当作结论前提,不能把某个 filter 测试外推成所有子进程默认无凭据。

排查安全问题时,先问“谁拥有最终资源”:本地进程看 SandboxManager 与 OS wrapper;远程进程看 exec_server_sandbox;直接文件工具看 FileSystemSandboxContext;网络看 proxy attribution;MCP 看 server policy 与审批。只有定位到最终 owner,才能判断某条检查是授权、配置、传输还是强制。

下一篇权限审批沙箱三层模型将继续沿同一条调用链,逐行拆开用户批准、Codex 策略与平台 sandbox 之间的转换。