权限与Sandbox指令
Codex 同时维护两套与权限有关的事实:一套告诉模型“哪些操作允许、何时请求批准”,另一套在命令执行前把权限转换为 macOS Seatbelt、Linux sandbox 或 Windows restricted token。前者影响模型决策,后者才承担实际强制;即使模型完整遵守 <permissions instructions>,也不能替代操作系统边界。
本文回答三个问题:运行时 PermissionProfile 如何降维成模型能理解的 sandbox、网络和审批文案;WorldState 为何把权限正文与已批准命令前缀分开追踪;工具调用抵达执行层后,谁合并临时权限并选择平台沙箱。读者应先了解 Context变更语义 的 section snapshot,并可结合 环境上下文生成与差分 区分环境说明和权限说明。
1. 三层边界
权限链路中的三个对象名字相近,但所有者和职责不同。
| 层次 | 核心对象 | 所有者 | 消费者 | 能否强制安全边界 |
|---|---|---|---|---|
| 运行时权限 | PermissionProfile | Turn / environment / command | prompt 与 sandboxing | 只是规范化输入 |
| 模型说明 | PermissionsInstructions、PermissionsState | WorldState | 模型上下文 | 不能 |
| 执行强制 | SandboxManager、平台 backend | exec runtime | 子进程启动器 | 能 |
这里最容易产生的误解是把提示文本当成 sandbox 配置。实际上,PermissionsInstructions 只生成 developer-role fragment;执行器仍会独立读取 PermissionProfile。提示缺失会降低模型对边界的理解,但不会自动关闭平台沙箱;反过来,提示写着只读也不能证明某个子进程已经被正确隔离。
2. Profile规范化
当前协议层没有把权限压成单一 sandbox_mode。PermissionProfile 先区分文件系统隔离由谁负责,再携带文件系统和网络策略。
源码位置:codex-rs/protocol/src/models.rs :: PermissionProfile。
#[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。
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。
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。
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 实现。
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。
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。
#[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。
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。
#[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。
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。
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。
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。
#[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。
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。
#[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 root | permissions_instructions.rs、world_state.rs | primary environment、root 物化、cwd |
| 审批说明不匹配配置 | approval_text | policy、reviewer、feature、catalog fallback |
| 批准后重复整块注入 | permissions.rs | 空 policy hash、prefix 集合、legacy snapshot |
| 关闭完整指令后无初始 prefix | compact_permissions.rs | 这是 Absent/Unknown 的既定语义 |
| 命令仍被系统拒绝 | sandboxing/src/manager.rs | effective profile、SandboxType、平台 backend |
| Linux 启动前失败 | SandboxTransformError | executable、cwd URI、WSL1/bubblewrap |
读完后可以从本版本源码完成三项只读验证:从 build_world_state_for_step 追到 <permissions instructions> 的 developer fragment;给 render_diff 构造“新增”和“删除”两种 prefix 集合并预测输出;从 SandboxManager::transform 指出 macOS、Linux 和 Windows 分支各自在哪里获得 effective profile。能清楚解释这三条路径,就不会再把模型提示、审批状态和操作系统强制混为一谈。
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