Skip to content

权限与Sandbox指令

追踪PermissionProfile如何投影为模型权限指令、按WorldState增量更新,并与真正的平台沙箱强制保持边界。

基于rust-v0.150.0
CodexRustContextSandbox

权限与Sandbox指令 ​

Codex 同时维护两套与权限有关的事实:一套告诉模型“哪些操作允许、何时请求批准”,另一套在命令执行前把权限转换为 macOS Seatbelt、Linux sandbox 或 Windows restricted token。前者影响模型决策,后者才承担实际强制;即使模型完整遵守 <permissions instructions>,也不能替代操作系统边界。

本文回答三个问题:运行时 PermissionProfile 如何降维成模型能理解的 sandbox、网络和审批文案;WorldState 为何把权限正文与已批准命令前缀分开追踪;工具调用抵达执行层后,谁合并临时权限并选择平台沙箱。读者应先了解 Context变更语义 的 section snapshot,并可结合 环境上下文生成与差分 区分环境说明和权限说明。

1. 三层边界 ​

权限链路中的三个对象名字相近,但所有者和职责不同。

层次核心对象所有者消费者能否强制安全边界
运行时权限PermissionProfileTurn / environment / commandprompt 与 sandboxing只是规范化输入
模型说明PermissionsInstructions、PermissionsStateWorldState模型上下文不能
执行强制SandboxManager、平台 backendexec runtime子进程启动器能

这里最容易产生的误解是把提示文本当成 sandbox 配置。实际上,PermissionsInstructions 只生成 developer-role fragment;执行器仍会独立读取 PermissionProfile。提示缺失会降低模型对边界的理解,但不会自动关闭平台沙箱;反过来,提示写着只读也不能证明某个子进程已经被正确隔离。

2. Profile规范化 ​

当前协议层没有把权限压成单一 sandbox_mode。PermissionProfile 先区分文件系统隔离由谁负责,再携带文件系统和网络策略。

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

rust
#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum PermissionProfile {
    /// Codex owns sandbox construction for this profile.
    #[serde(rename_all = "snake_case")]
    #[ts(rename_all = "snake_case")]
    Managed {
        file_system: ManagedFileSystemPermissions,
        network: NetworkSandboxPolicy,
    },
    /// Do not apply an outer sandbox.
    Disabled,
    /// Filesystem isolation is enforced by an external caller.
    #[serde(rename_all = "snake_case")]
    #[ts(rename_all = "snake_case")]
    External { network: NetworkSandboxPolicy },
}

Managed 表示 Codex 自己构造外层 sandbox;Disabled 表示不加外层文件系统 sandbox;External 表示文件系统隔离由外部调用者承担。External 不是 Disabled 的别名:二者都可能不由当前进程构造文件系统 wrapper,但前者明确声明已有外部隔离边界。

内置 workspace-write profile 也不是立刻可执行的绝对路径集合。它先保留 :workspace_roots 符号项,等 Step 选定 environment 后再物化。

源码位置:codex-rs/protocol/src/models.rs :: workspace_write、materialize_project_roots_with_workspace_roots。

rust
pub fn workspace_write() -> Self {
    Self::workspace_write_with(
        &[],
        NetworkSandboxPolicy::Restricted,
        /*exclude_tmpdir_env_var*/ false,
        /*exclude_slash_tmp*/ false,
    )
}

pub fn materialize_project_roots_with_workspace_roots(
    self,
    workspace_roots: &[AbsolutePathBuf],
) -> Self {
    match self {
        Self::Managed {
            file_system,
            network,
        } => {
            let file_system = file_system
                .to_sandbox_policy()
                .materialize_project_roots_with_workspace_roots(workspace_roots);
            Self::Managed {
                file_system: ManagedFileSystemPermissions::from_sandbox_policy(&file_system),
                network,
            }
        }
        Self::Disabled => Self::Disabled,
        Self::External { network } => Self::External { network },
    }
}

这一步解释了为什么同一个配置 profile 在不同 environment 中可以得到不同的 writable roots。模型文案和执行器必须消费物化后的 profile,否则提示中的路径和实际执行边界可能不一致。

3. 模型投影 ​

模型不直接读取 PermissionProfile 的完整枚举和 entry 列表。from_permission_profile 先恢复文件系统策略,再投影为简化的 SandboxMode、网络开关、可写根、拒绝读取项和审批文案。

源码位置:codex-rs/prompts/src/permissions_instructions.rs :: from_permission_profile。

rust
pub fn from_permission_profile(
    permission_profile: &PermissionProfile,
    approval_policy: AskForApproval,
    approval_context: ApprovalPromptContext<'_>,
    exec_policy: &Policy,
    cwd: &Path,
    exec_permission_approvals_enabled: bool,
    request_permissions_tool_enabled: bool,
) -> Self {
    let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
    let (sandbox_mode, writable_roots) =
        sandbox_prompt_from_policy(&file_system_sandbox_policy, cwd);

    Self::from_permissions_with_network_and_denied_reads(
        sandbox_mode,
        network_access_from_policy(permission_profile.network_sandbox_policy()),
        PermissionsPromptConfig {
            approval_policy,
            approvals_reviewer: approval_context.reviewer,
            approval_messages: approval_context.messages,
            permission_messages: approval_context.permission_messages,
            exec_policy,
            exec_permission_approvals_enabled,
            request_permissions_tool_enabled,
        },
        writable_roots,
        denied_reads_text(&file_system_sandbox_policy, cwd),
    )
}

投影规则只看“是否拥有全盘写权限”和“可写根是否为空”:全盘可写映射为 DangerFullAccess,没有可写根映射为 ReadOnly,其余映射为 WorkspaceWrite。

源码位置:codex-rs/prompts/src/permissions_instructions.rs :: sandbox_prompt_from_policy、network_access_from_policy。

rust
fn sandbox_prompt_from_policy(
    file_system_policy: &FileSystemSandboxPolicy,
    cwd: &Path,
) -> (SandboxMode, Option<Vec<WritableRoot>>) {
    if file_system_policy.has_full_disk_write_access() {
        return (SandboxMode::DangerFullAccess, None);
    }

    let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd);
    if writable_roots.is_empty() {
        (SandboxMode::ReadOnly, None)
    } else {
        (SandboxMode::WorkspaceWrite, Some(writable_roots))
    }
}

fn network_access_from_policy(network_policy: NetworkSandboxPolicy) -> NetworkAccess {
    if network_policy.is_enabled() {
        NetworkAccess::Enabled
    } else {
        NetworkAccess::Restricted
    }
}

这是有意的降维:模型需要的是操作规则,不需要重建所有内部 entry。代价是多个内部 profile 可能生成相同的 SandboxMode;可写根和 denied reads 因此作为附加 section 保留必要边界。

4. 文案拼装 ​

PermissionsInstructions 固定使用 developer 角色和 <permissions instructions> marker。正文按 sandbox、approval、writable roots、denied reads 的顺序拼装。

源码位置:codex-rs/prompts/src/permissions_instructions.rs :: from_permissions_with_network_and_denied_reads、ContextualUserFragment 实现。

rust
fn from_permissions_with_network_and_denied_reads(
    sandbox_mode: SandboxMode,
    network_access: NetworkAccess,
    config: PermissionsPromptConfig<'_>,
    writable_roots: Option<Vec<WritableRoot>>,
    denied_reads: Option<String>,
) -> Self {
    let mut text = String::new();
    let sandbox = sandbox_text(sandbox_mode, network_access, config.permission_messages);
    if !sandbox.is_empty() {
        append_section(&mut text, &sandbox);
    }
    append_section(
        &mut text,
        &approval_text(
            config.approval_policy,
            config.approvals_reviewer,
            config.approval_messages,
            config.exec_policy,
            config.exec_permission_approvals_enabled,
            config.request_permissions_tool_enabled,
        ),
    );
    if let Some(writable_roots) = writable_roots_text(writable_roots) {
        append_section(&mut text, &writable_roots);
    }
    if let Some(denied_reads) = denied_reads {
        append_section(&mut text, &denied_reads);
    }
    if !text.ends_with('\n') {
        text.push('\n');
    }
    Self { text }
}

impl ContextualUserFragment for PermissionsInstructions {
    fn role(&self) -> &'static str {
        "developer"
    }

    fn markers(&self) -> (&'static str, &'static str) {
        Self::type_markers()
    }

    fn type_markers() -> (&'static str, &'static str) {
        ("<permissions instructions>", "</permissions instructions>")
    }

    fn body(&self) -> String {
        PermissionsInstructions::body(self)
    }
}

模型 catalog 可以为三种 sandbox mode 和审批策略提供定制文案;对应项不存在时回退内置模板。sandbox catalog 项如果存在但内容为空,只省略 sandbox 段,不会连带删除 approval、writable roots 和 denied reads。这使模型定制与权限事实解耦,避免一个空模板抹掉其余约束。

审批文案还取决于 AskForApproval、ApprovalsReviewer 和两个 feature。OnRequest 在启用 exec permission approvals 时优先引导 with_additional_permissions;RequestPermissionsTool 启用后追加专用工具说明;AutoReview 会增加自动评审后的拒绝处理规则;Granular 则逐类列出可提示和自动拒绝的审批类别。

说明: 已批准命令前缀属于 exec policy,而不是文件系统 profile。它会出现在完整审批文案中,但 WorldState 会把它从稳定权限 hash 中拆出,原因见下一节。

5. 主环境权限 ​

一次 Step 可以包含多个 environment,但完整权限指令只选择 primary environment。WorldState 先取其 profile 与 workspace roots,物化符号根,再使用同一 environment 的 cwd 生成相对路径相关文案。

源码位置:codex-rs/core/src/session/world_state.rs :: build_world_state_for_step。

rust
if turn_context.config.include_permissions_instructions {
    let environment = step_context.environments.primary();
    let permission_profile = environment
        .map(|environment| {
            let workspace_roots = environment
                .workspace_roots()
                .iter()
                .filter_map(|workspace_root| workspace_root.to_abs_path().ok())
                .collect::<Vec<_>>();
            environment
                .permission_profile()
                .clone()
                .materialize_project_roots_with_workspace_roots(&workspace_roots)
        })
        .unwrap_or_else(|| turn_context.permission_profile());
    #[allow(deprecated)]
    let cwd = environment
        .and_then(|environment| environment.cwd().to_abs_path().ok())
        .unwrap_or_else(|| turn_context.cwd.clone());
    let model_messages = turn_context.model_info.model_messages.as_ref();
    let exec_policy = self.services.exec_policy.current();
    world_state.add_section(PermissionsState::new(
        &permission_profile,
        turn_context.approval_policy(),
        ApprovalPromptContext::new(
            turn_context.config.approvals_reviewer,
            model_messages.and_then(|messages| messages.approvals.as_ref()),
            model_messages.and_then(|messages| messages.permissions.as_ref()),
        ),
        exec_policy.as_ref(),
        &cwd,
        turn_context
            .config
            .features
            .enabled(Feature::ExecPermissionApprovals),
        turn_context
            .config
            .features
            .enabled(Feature::RequestPermissionsTool),
    ));
} else {
    let exec_policy = self.services.exec_policy.current();
    world_state.add_section(CompactPermissionsState::new(exec_policy.as_ref()));
}

primary environment 不存在时才回退 Turn 级 profile 和旧 cwd。这个 fallback 保证无 environment snapshot 的路径仍能生成权限说明,但不能据此认为多环境权限已被合并;非 primary environment 的 profile 不进入这一个 section。

同一份 permission snapshot 还会被执行器再次读取:模型可见的 PermissionsInstructions 只描述有效 profile, 而 sandbox transform 会根据命令携带的 additional_permissions 重新计算 effective profile。临时授权因此不会 修改 TurnContext 中保存的基础 profile,也不会把 External enforcement 变成 Managed。

6. Snapshot拆分 ​

完整权限正文可能包含已批准前缀。如果直接对整段文本取 hash,每批准一个新前缀都会迫使系统重发庞大的 permissions block。当前 snapshot 因而把稳定指令 hash 与 prefix 集合拆开。

源码位置:codex-rs/core/src/context/world_state/permissions.rs :: PermissionsSnapshot、PermissionsState::new。

rust
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum PermissionsSnapshot {
    Current {
        instructions: WorldStateHash,
        approved_command_prefixes: BTreeSet<Vec<String>>,
    },
    Legacy(WorldStateHash),
}

impl PermissionsState {
    pub(crate) fn new(
        permission_profile: &PermissionProfile,
        approval_policy: AskForApproval,
        approval_context: ApprovalPromptContext<'_>,
        exec_policy: &Policy,
        cwd: &Path,
        exec_permission_approvals_enabled: bool,
        request_permissions_tool_enabled: bool,
    ) -> Self {
        let build_instructions = |exec_policy| {
            PermissionsInstructions::from_permission_profile(
                permission_profile,
                approval_policy,
                approval_context,
                exec_policy,
                cwd,
                exec_permission_approvals_enabled,
                request_permissions_tool_enabled,
            )
        };
        let instructions = build_instructions(exec_policy);
        let instructions_without_approved_prefixes = build_instructions(&Policy::empty());
        let snapshot = PermissionsSnapshot::Current {
            instructions: WorldStateHash::from_fragment(&instructions_without_approved_prefixes),
            approved_command_prefixes: exec_policy.get_allowed_prefixes().into_iter().collect(),
        };
        Self {
            snapshot,
            instructions,
        }
    }
}

instructions 保存当前可渲染全文;snapshot 中的 hash 却来自空 exec policy。于是 profile、cwd、审批策略、feature 或模板变化会改变 hash,而单纯新增 prefix 只改变集合。Legacy(WorldStateHash) 继续兼容旧持久化格式,不要求已有会话立即迁移。

7. Prefix增量 ​

diff 算法先比较稳定 hash。hash 相同且 prefix 集合相等时不发送内容;当前集合是旧集合超集时只发送新增项;prefix 被删除或其他权限事实变化时重发完整 block。

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

rust
fn render_diff(
    &self,
    previous: PreviousSectionState<'_, Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>> {
    match (previous, &self.snapshot) {
        (
            PreviousSectionState::Known(PermissionsSnapshot::Current {
                instructions: previous_instructions,
                approved_command_prefixes: previous_prefixes,
            }),
            PermissionsSnapshot::Current {
                instructions,
                approved_command_prefixes,
            },
        ) if previous_instructions == instructions => {
            if previous_prefixes == approved_command_prefixes {
                return None;
            }
            if previous_prefixes.is_subset(approved_command_prefixes) {
                let added_prefixes = approved_command_prefixes
                    .difference(previous_prefixes)
                    .cloned()
                    .collect();
                if let Some(prefixes) = format_allow_prefixes(added_prefixes) {
                    return Some(Box::new(ApprovedCommandPrefixSaved::new(prefixes)));
                }
            }
        }
        (
            PreviousSectionState::Known(PermissionsSnapshot::Legacy(previous)),
            PermissionsSnapshot::Current { .. },
        ) if previous == &WorldStateHash::from_fragment(&self.instructions) => return None,
        _ => {}
    }

    Some(Box::new(self.instructions.clone()))
}

删除 prefix 必须重发全文,因为只追加“新增规则”的消息无法撤销模型历史里已有的批准信息。完整刷新把当前 prefix 集合重新陈述出来,恢复模型可见状态与 exec policy 的一致性。

8. Compact模式 ​

include_permissions_instructions = false 并不等于 WorldState 完全忽略权限变化。此时使用 CompactPermissionsState,只跟踪已批准前缀的新增项。

源码位置:codex-rs/core/src/context/world_state/compact_permissions.rs :: CompactPermissionsState。

rust
#[derive(Clone, Debug)]
pub(crate) struct CompactPermissionsState {
    prefixes: BTreeSet<Vec<String>>,
}

impl WorldStateSection for CompactPermissionsState {
    const ID: &'static str = "approved_command_prefixes";
    type Snapshot = BTreeSet<Vec<String>>;

    fn snapshot(&self) -> Self::Snapshot {
        self.prefixes.clone()
    }

    fn render_diff(
        &self,
        previous: PreviousSectionState<'_, Self::Snapshot>,
    ) -> Option<Box<dyn ContextualUserFragment>> {
        let added_prefixes = match previous {
            PreviousSectionState::Known(previous) => self
                .prefixes
                .difference(previous)
                .cloned()
                .collect::<Vec<_>>(),
            PreviousSectionState::Absent | PreviousSectionState::Unknown => return None,
        };
        format_allow_prefixes(added_prefixes)
            .filter(|prefixes| !prefixes.is_empty())
            .map(|prefixes| Box::new(ApprovedCommandPrefixSaved::new(prefixes)) as _)
    }
}

只有 previous snapshot 为 Known 时才能判定“新增”。Absent 或 Unknown 时不把现有 prefix 全量注入,prefix 删除也没有输出。这是一种保守语义:compact section 负责通知增量,不负责建立完整权限基线,也不声称模型已经看到 sandbox 和 approval 规则。

9. 执行强制 ​

工具请求进入执行层后,SandboxManager::transform 重新从权限对象出发。它先把 command 的 additional_permissions 与 base profile 合并,再校验 cwd URI,并按 SandboxType 构造平台命令。

源码位置:codex-rs/sandboxing/src/policy_transforms.rs :: effective_permission_profile。

rust
pub fn effective_permission_profile(
    permission_profile: &PermissionProfile,
    additional_permissions: Option<&AdditionalPermissionProfile>,
) -> PermissionProfile {
    let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();
    let effective_file_system_policy =
        effective_file_system_sandbox_policy(&file_system_policy, additional_permissions);
    let effective_network_policy =
        effective_network_sandbox_policy(network_policy, additional_permissions);
    PermissionProfile::from_runtime_permissions_with_enforcement(
        permission_profile.enforcement(),
        &effective_file_system_policy,
        effective_network_policy,
    )
}

合并时保留原 profile 的 enforcement,因此临时增加路径或网络权限不会把 External 悄悄改成 Managed。这也是“追加权限”和“完全绕过 sandbox”必须分开的实现依据。

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

rust
let additional_permissions = command.additional_permissions.take();
let managed_mitm_ca_trust_bundle_path =
    network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
let base_effective_permission_profile =
    effective_permission_profile(permissions, additional_permissions.as_ref());
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
    &command.cwd,
    sandbox_policy_cwd,
    base_effective_permission_profile.clone(),
    managed_mitm_ca_trust_bundle_path.as_ref(),
);

let (argv, arg0_override, pending_sandboxed_request) = match sandbox {
    SandboxType::None => (os_argv_to_strings(argv), None, None),
    #[cfg(target_os = "macos")]
    SandboxType::MacosSeatbelt => {
        let pending = pending_sandboxed_request?;
        let (file_system_sandbox_policy, network_sandbox_policy) = pending
            .effective_permission_profile
            .to_runtime_permissions();
        let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
            command: os_argv_to_strings(argv),
            file_system_sandbox_policy: &file_system_sandbox_policy,
            network_sandbox_policy,
            sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
            enforce_managed_network,
            managed_network,
            environment_id,
            network,
            extra_allow_unix_sockets: &[],
        })
        .map_err(SandboxTransformError::EnvironmentNetworkProxy)?;
        let mut full_command = Vec::with_capacity(1 + args.len());
        full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
        full_command.append(&mut args);
        (full_command, None, Some(pending))
    }
    SandboxType::LinuxSeccomp => {
        let pending = pending_sandboxed_request?;
        let exe = codex_linux_sandbox_exe
            .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
        let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
        let mut args = create_linux_sandbox_command_args_for_permission_profile(
            os_argv_to_strings(argv),
            pending.native_command_cwd.as_path(),
            &pending.effective_permission_profile,
            pending.native_sandbox_policy_cwd.as_path(),
            use_legacy_landlock,
            allow_proxy_network,
        );
        let mut full_command = Vec::with_capacity(1 + args.len());
        full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
        full_command.append(&mut args);
        (full_command, Some(linux_sandbox_arg0_override(exe)), Some(pending))
    }
    SandboxType::WindowsRestrictedToken => (
        os_argv_to_strings(argv),
        None,
        Some(pending_sandboxed_request?),
    ),
};

SandboxType::None 不构造 wrapper,但结果仍保留 effective profile,供后续执行链观察。平台准备失败则在生成 SandboxExecRequest 前返回错误,例如 command cwd 无法转换、本机缺少 Linux sandbox executable,或 WSL1 路径需要 bubblewrap:

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

rust
fn ensure_linux_bubblewrap_is_supported(
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    use_legacy_landlock: bool,
    allow_network_for_proxy: bool,
    is_wsl1: bool,
) -> Result<(), SandboxTransformError> {
    let requires_bubblewrap = allow_network_for_proxy
        || (!use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access());
    if is_wsl1 && requires_bubblewrap {
        return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);
    }

    Ok(())
}

这是执行失败,不是“模型没有理解权限”。排查时应先判断错误产生在 prompt 构建、WorldState diff、审批决策还是平台 transform,不能把所有 sandbox 现象归因于同一层。

10. 权限测试 ​

提示层测试以不同 mode、network 和 catalog message 为输入,断言最终正文包含对应模板、网络状态及 fallback。它证明字符串选择和 section 拼装,不证明操作系统真的拒绝读写。

源码位置:codex-rs/prompts/src/permissions_instructions_tests.rs :: renders_sandbox_mode_text。

rust
#[test]
fn renders_sandbox_mode_text() {
    assert_eq!(
        sandbox_text(
            SandboxMode::WorkspaceWrite,
            NetworkAccess::Restricted,
            /*permission_messages*/ None,
        ),
        "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."
    );

    assert_eq!(
        sandbox_text(
            SandboxMode::ReadOnly,
            NetworkAccess::Restricted,
            /*permission_messages*/ None,
        ),
        "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is restricted."
    );

    assert_eq!(
        sandbox_text(
            SandboxMode::DangerFullAccess,
            NetworkAccess::Enabled,
            /*permission_messages*/ None,
        ),
        "Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is enabled."
    );
}

WorldState 测试构造前后两个 snapshot。新增 touch allow-prefix.txt 规则后,断言更新只有 Approved command prefix saved,且明确不含 <permissions instructions>。

源码位置:codex-rs/core/src/context/world_state/permissions_tests.rs :: approved_prefix_is_rendered_without_reinjecting_permissions。

rust
let without_approved_prefix = permissions_state_with_default_messages(&Policy::empty());
let mut exec_policy = Policy::empty();
exec_policy
    .add_prefix_rule(
        &["touch".to_string(), "allow-prefix.txt".to_string()],
        Decision::Allow,
    )
    .expect("test prefix should be valid");
let with_approved_prefix = permissions_state_with_default_messages(&exec_policy);
let rendered_update = with_approved_prefix
    .render_diff(Known(&without_approved_prefix.snapshot()))
    .expect("approving a prefix should render a world-state update")
    .render();

assert_eq!(
    rendered_update,
    "Approved command prefix saved:\n- [\"touch\", \"allow-prefix.txt\"]"
);
assert!(!rendered_update.contains("<permissions instructions>"));

另一个测试从含 touch prefix 的 snapshot 变回空 policy,断言结果等于完整权限正文。它证明删除不能用追加消息表达;仍不证明历史模型一定遗忘旧规则,只证明 Codex 会重新陈述当前完整状态。

源码位置:codex-rs/core/src/context/world_state/permissions_tests.rs :: removing_an_approved_prefix_renders_full_permissions。

rust
#[test]
fn removing_an_approved_prefix_renders_full_permissions() {
    use PreviousSectionState::Known;

    let mut exec_policy = Policy::empty();
    exec_policy
        .add_prefix_rule(&["touch".to_string()], Decision::Allow)
        .expect("test prefix should be valid");
    let with_approved_prefix = permissions_state_with_default_messages(&exec_policy);
    let without_approved_prefix = permissions_state_with_default_messages(&Policy::empty());

    let rendered = without_approved_prefix
        .render_diff(Known(&with_approved_prefix.snapshot()))
        .expect("removing a prefix should refresh permissions")
        .render();

    assert_eq!(rendered, without_approved_prefix.instructions.render());
}

compact 测试则固定了无基线行为:previous 为 Unknown 或 Absent 时,即使当前已有 prefix 也不发送。这证明 compact section 只负责已知快照之间的新增通知。

11. 故障定位 ​

遇到权限异常时,可以按结果出现的位置逆向定位:

现象首查源码判断重点
模型说错 writable rootpermissions_instructions.rs、world_state.rsprimary environment、root 物化、cwd
审批说明不匹配配置approval_textpolicy、reviewer、feature、catalog fallback
批准后重复整块注入permissions.rs空 policy hash、prefix 集合、legacy snapshot
关闭完整指令后无初始 prefixcompact_permissions.rs这是 Absent/Unknown 的既定语义
命令仍被系统拒绝sandboxing/src/manager.rseffective profile、SandboxType、平台 backend
Linux 启动前失败SandboxTransformErrorexecutable、cwd URI、WSL1/bubblewrap

读完后可以从本版本源码完成三项只读验证:从 build_world_state_for_step 追到 <permissions instructions> 的 developer fragment;给 render_diff 构造“新增”和“删除”两种 prefix 集合并预测输出;从 SandboxManager::transform 指出 macOS、Linux 和 Windows 分支各自在哪里获得 effective profile。能清楚解释这三条路径,就不会再把模型提示、审批状态和操作系统强制混为一谈。

bash
rg -n "PermissionProfile|PermissionsInstructions|PermissionsState|CompactPermissionsState|SandboxManager::transform" \
  codex-rs/protocol/src/models.rs \
  codex-rs/prompts/src/permissions_instructions.rs \
  codex-rs/core/src/context/world_state \
  codex-rs/sandboxing/src/manager.rs
cargo test -p codex-prompts permissions_instructions --lib
cargo test -p codex-core 'context::world_state::permissions::' --lib
cargo test -p codex-core 'context::world_state::compact_permissions::' --lib