PermissionProfile解析
Permission profile 的名称不是权限本身。:workspace、:read-only 或用户定义的 ci-agent 只是选择键;Codex 必须先解析 extends、合并 requirements 提供的 managed profile、选择有效 ID、编译文件与网络规则、应用约束,再把“实际权限”和“选中的名称”原子地装入运行时。
rust-v0.150.0 用两个对象保持这一区分:PermissionProfile 是执行器必须遵守的 canonical 权限;ActivePermissionProfile 是用于 UI、状态恢复和重新选择的身份 sidecar。两者由 PermissionProfileSnapshot 绑定,避免 profile ID 与权限内容在更新期间错配。
本文承接SandboxPolicy完整参考和ApprovalPolicy完整参考。前文解释 legacy 投影与审批;本文从配置输入一路走到 Turn environment,回答哪个 profile 最终生效、何时生效、为什么某个名称可能存在但不可选。
每个箭头都有失败分支:同名 managed/config profile 会冲突,extends 可以形成环,requirements 可以禁止选择,路径编译可以产生警告或错误,最终约束还可能把选中 profile 回退成另一组权限。
1. TOML结构
[permissions] 下每个 key 是一个 profile。profile 可以携带描述、父 profile、额外 workspace roots、文件规则与网络规则。
源码位置:codex-rs/config/src/permissions_toml.rs :: PermissionsToml, PermissionProfileToml
pub struct PermissionsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, PermissionProfileToml>,
}
pub struct PermissionProfileToml {
pub description: Option<String>,
pub extends: Option<String>,
pub workspace_roots: Option<WorkspaceRootsToml>,
pub filesystem: Option<FilesystemPermissionsToml>,
pub network: Option<NetworkToml>,
}文件表允许直接 path、glob 或 special path key,并可配置 glob_scan_max_depth。网络表不仅有 enabled,还能包含 proxy、domain allow/deny、Unix socket、local binding 和 MITM hook 配置;但 network.enabled 才决定 canonical network sandbox bit,proxy 配置是否真正启动还受 feature 与 requirements 约束。
源码位置:codex-rs/config/src/permissions_toml.rs :: FilesystemPermissionsToml, NetworkToml
pub struct FilesystemPermissionsToml {
#[schemars(range(min = 1))]
pub glob_scan_max_depth: Option<usize>,
#[serde(flatten)]
pub entries: BTreeMap<String, FilesystemPermissionToml>,
}
pub struct NetworkToml {
pub enabled: Option<bool>,
pub proxy_url: Option<String>,
pub enable_socks5: Option<bool>,
pub socks_url: Option<String>,
pub enable_socks5_udp: Option<bool>,
pub allow_upstream_proxy: Option<bool>,
pub dangerously_allow_non_loopback_proxy: Option<bool>,
pub dangerously_allow_all_unix_sockets: Option<bool>,
pub mode: Option<NetworkMode>,
pub domains: Option<NetworkDomainPermissionsToml>,
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
pub allow_local_binding: Option<bool>,
pub mitm: Option<NetworkMitmToml>,
}2. Extends解析
extends 采用单继承链。解析器逐级收集 profile,先检测环和缺失父项,再从最老父项向子项合并,所以 child 的同名 key 覆盖 parent。
源码位置:codex-rs/config/src/permissions_toml.rs :: PermissionsToml::resolve_profile
loop {
if let Some(cycle_start) = profile_names
.iter()
.position(|name| name == &next_profile_name)
{
let cycle = profile_names[cycle_start..]
.iter()
.cloned()
.chain(std::iter::once(next_profile_name))
.collect::<Vec<_>>();
return Err(PermissionProfileResolutionError::Cycle { cycle });
}
let profile = self
.entries
.get(&next_profile_name)
.cloned()
.or_else(|| parent_profile(&next_profile_name))
.ok_or_else(|| {
referenced_by.as_deref().map_or_else(
|| PermissionProfileResolutionError::UndefinedProfile {
profile_name: next_profile_name.clone(),
},
|referenced_by| {
if next_profile_name.starts_with(':') {
PermissionProfileResolutionError::UnsupportedBuiltInParent {
profile_name: referenced_by.to_string(),
parent_profile_name: next_profile_name.clone(),
}
} else {
PermissionProfileResolutionError::UndefinedParent {
profile_name: referenced_by.to_string(),
parent_profile_name: next_profile_name.clone(),
}
}
},
)
})?;
let parent_profile_name = profile.extends.clone();
profile_names.push(next_profile_name.clone());
if let Some(parent_profile_name) = parent_profile_name {
profiles.push(profile);
referenced_by = Some(next_profile_name);
next_profile_name = parent_profile_name;
continue;
}
let profile = profiles
.into_iter()
.rev()
.try_fold(profile, merge_permission_profiles)?;
return Ok(profile);
}真实源码对以 : 开头但不支持继承的 built-in parent 返回 UnsupportedBuiltInParent,而不是普通 UndefinedParent。当前只有 :read-only 与 :workspace 能作为用户 profile 的父模板;:danger-full-access 不允许被 extends,因为它不提供可安全增量合并的 managed filesystem 基础。
合并时,父 profile 的 description 与 extends 不继承,最终 sidecar 保留选中 child 自己的声明元数据。若 parent 和 child 都配置 network domains,域名先规范化再合并,防止大小写或等价 host 形成重复规则。
源码位置:codex-rs/config/src/permissions_toml.rs :: merge_permission_profiles
parent.description = None;
parent.extends = None;
if merges_network_domains {
normalize_profile_network_domains(&mut parent);
normalize_profile_network_domains(&mut child);
}
let mut merged = TomlValue::try_from(parent)
.map_err(|source| PermissionProfileResolutionError::SerializeProfileToml { source })?;
let child = TomlValue::try_from(child)
.map_err(|source| PermissionProfileResolutionError::SerializeProfileToml { source })?;
merge_toml_values(&mut merged, &child);
merged
.try_into()
.map_err(|source| PermissionProfileResolutionError::DeserializeProfileToml { source })3. 内置Profile
Codex 保留三个内置 ID,并禁止用户定义以 : 开头的名称,避免伪装或覆盖 built-in profile。
源码位置:codex-rs/core/src/config/permissions.rs :: built-in profile constants, builtin_permission_profile
pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
pub(crate) const BUILT_IN_DANGER_FULL_ACCESS_PROFILE: &str =
BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
pub(crate) fn builtin_permission_profile(
profile_name: &str,
workspace_write: Option<&SandboxWorkspaceWrite>,
) -> Option<PermissionProfile> {
match profile_name {
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
Some(SandboxWorkspaceWrite {
writable_roots: _,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
}) => PermissionProfile::workspace_write_with(
&[],
if *network_access {
NetworkSandboxPolicy::Enabled
} else {
NetworkSandboxPolicy::Restricted
},
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
),
None => PermissionProfile::workspace_write(),
}),
BUILT_IN_DANGER_FULL_ACCESS_PROFILE => Some(PermissionProfile::Disabled),
_ => None,
}
}默认 built-in 选择还依赖 project trust 与 Windows sandbox 能力。trusted 和 untrusted 项目通常都选 :workspace;Windows sandbox disabled 时回退 :read-only,避免只有策略形状却没有平台强制。
源码位置:codex-rs/core/src/config/permissions.rs :: default_builtin_permission_profile_name
if (active_project.is_trusted() || active_project.is_untrusted())
&& !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled)
{
BUILT_IN_WORKSPACE_PROFILE
} else {
BUILT_IN_READ_ONLY_PROFILE
}4. Managed合并
requirements 可以定义 managed profile。Core 在选择前把 configured 与 managed catalog 合并;同名 profile 直接报错,不允许 requirements 静默覆盖用户配置或反过来被用户覆盖。
源码位置:codex-rs/core/src/config/mod.rs :: merge_managed_permission_profiles
let managed_profiles = requirements_toml
.permissions
.as_ref()
.map(|permissions| &permissions.profiles)
.filter(|profiles| !profiles.is_empty());
let Some(managed_profiles) = managed_profiles else {
return Ok(configured_permissions.cloned());
};
let mut merged_permissions = configured_permissions.cloned().unwrap_or_default();
for (profile_id, managed_profile) in managed_profiles {
if merged_permissions.entries.contains_key(profile_id) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"requirements.toml permissions profile `{profile_id}` conflicts with a config-defined profile of the same name"
),
));
}
merged_permissions
.entries
.insert(profile_id.clone(), managed_profile.clone());
}
Ok(Some(merged_permissions))对远程 executor,还有一个只做“选择、不编译路径”的入口。它合并 catalog 与 allowlist,返回 profile ID 和未编译 TOML;Windows 路径留给目标 executor 解释,不在本地 Unix Core 上转成错误路径。
源码位置:codex-rs/core/src/config/permission_profile_selection.rs :: resolve_permission_profile_selection
let selection = super::resolve_effective_permission_selection(
configured_profiles,
/*default_permissions_override*/ None,
/*persisted_profile_id*/ None,
configured_default_profile_id,
requirements,
&mut Vec::new(),
)?;
Ok(ResolvedPermissionProfileSelection {
profile_id: selection.selected_profile_id,
profiles: selection.profiles,
})5. 选择优先级
配置加载时,显式 concrete permission_profile、legacy sandbox_mode 或 default_permissions override 会使持久化 profile ID 失效;否则合法的 persisted ID 可以覆盖配置文件中的 default。
源码位置:codex-rs/core/src/config/mod.rs :: ConfigBuilder permission selection
let persisted_permission_profile_id = if sandbox_mode.is_some()
|| permission_profile.is_some()
|| default_permissions_override.is_some()
{
None
} else {
persisted_permission_profile_id.as_deref()
};
let effective_permission_selection = resolve_effective_permission_selection(
cfg.permissions.as_ref(),
default_permissions_override.as_deref(),
persisted_permission_profile_id,
cfg.default_permissions.as_deref(),
requirements_toml,
&mut startup_warnings,
)?;resolve_effective_permission_selection 会先验证 persisted ID 是否仍能编译。失效或已删除的 persisted ID 被忽略,然后按 override/persisted → configured default → requirements fallback 的顺序选取。
源码位置:codex-rs/core/src/config/mod.rs :: resolve_effective_permission_selection, resolve_default_permissions
let valid_persisted_profile_id = persisted_profile_id.filter(|profile_id| {
is_builtin_permission_profile_name(profile_id)
|| profiles.as_ref().is_some_and(|profiles| {
compile_permission_profile_selection(
Some(profiles),
profile_id,
/*workspace_write*/ None,
&mut Vec::new(),
)
.is_ok()
})
});
let selected_profile_id = resolve_default_permissions(
default_permissions_override.or(valid_persisted_profile_id),
configured_default_profile_id,
requirements_toml,
startup_warnings,
)?;requirements 有 allowlist 时,未允许的选择不会失败退出,而是回退 requirements default 并产生 warning;但 allowlist 引用不存在的 profile、default 不在 allowlist 中,或无法推出默认值时,会在加载阶段报错。
源码位置:codex-rs/core/src/config/mod.rs :: resolve_default_permissions
match selected_permissions {
None => Ok(Some(fallback_permissions)),
Some(selected_permissions)
if is_permission_allowed(allowed_permission_profiles, selected_permissions) =>
{
Ok(Some(selected_permissions))
}
Some(selected_permissions) => {
startup_warnings.push(format!(
"Configured value for `permission_profile` is disallowed by requirements; falling back from `{selected_permissions}` to required value `{fallback_permissions}`."
));
Ok(Some(fallback_permissions))
}
}6. Profile编译
选中 built-in profile 时直接返回 runtime permissions;命名 profile 则解析 extends,逐条编译 filesystem entry,检查 glob 平台支持与 scan depth,再编译 network enabled bit。
源码位置:codex-rs/core/src/config/permissions.rs :: compile_permission_profile_selection
if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) {
return Ok(permission_profile.to_runtime_permissions());
}
reject_unknown_builtin_permission_profile(profile_name)?;
let permissions = permissions.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"default_permissions requires a `[permissions]` table",
)
})?;
compile_permission_profile(permissions, profile_name, startup_warnings)源码位置:codex-rs/core/src/config/permissions.rs :: compile_permission_profile
let profile = resolve_permission_profile(permissions, profile_name)?;
let mut file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(Vec::new());
let base_network_sandbox_policy = NetworkSandboxPolicy::Restricted;
if let Some(filesystem) = profile.filesystem.as_ref() {
if filesystem.is_empty() && file_system_sandbox_policy.entries.is_empty() {
push_warning(
startup_warnings,
missing_filesystem_entries_warning(profile_name),
);
} else {
if cfg!(not(target_os = "macos")) {
for pattern in unsupported_read_write_glob_paths(filesystem) {
push_warning(
startup_warnings,
format!(
"Filesystem glob `{pattern}` uses `read` or `write` access, which is not fully supported by this platform's sandboxing. Use an exact path or trailing `/**` subtree rule instead. `deny` globs are supported."
),
);
}
for pattern in unbounded_unreadable_globstar_paths(filesystem) {
push_warning(
startup_warnings,
format!(
"Filesystem deny-read glob `{pattern}` uses `**`. Non-macOS sandboxing does not support unbounded `**` natively; set `glob_scan_max_depth` in this filesystem profile to cap Linux glob expansion and silence this warning, or enumerate explicit depths such as `*.env`, `*/*.env`, and `*/*/*.env`."
),
);
}
}
for (path, permission) in &filesystem.entries {
file_system_sandbox_policy
.entries
.extend(compile_filesystem_permission(
path,
permission,
startup_warnings,
)?);
}
}
} else if file_system_sandbox_policy.entries.is_empty() {
push_warning(
startup_warnings,
missing_filesystem_entries_warning(profile_name),
);
}
let glob_scan_max_depth = validate_glob_scan_max_depth(
profile
.filesystem
.as_ref()
.and_then(|filesystem| filesystem.glob_scan_max_depth),
)?;
if let Some(glob_scan_max_depth) = glob_scan_max_depth {
file_system_sandbox_policy.glob_scan_max_depth = Some(glob_scan_max_depth);
}
let network_sandbox_policy =
compile_network_sandbox_policy(profile.network.as_ref(), base_network_sandbox_policy);
Ok((file_system_sandbox_policy, network_sandbox_policy))完整实现还会为缺失/空 filesystem、非 macOS read/write glob 与无限 deny globstar 生成 startup warning。warning 不等于忽略全部 profile:可编译部分仍会进入 policy,真正无效的 path 或 depth 才返回错误。
profile 自己声明的 workspace roots 与 Turn 当前 workspace roots 是两组来源。前者先按 policy cwd 解析并保存在 snapshot;后续 environment 可以把它们与运行时 roots 合并,再物化 :project_roots。
7. Catalog可用性
catalog 总是包含三个 built-in,再追加命名 profile。allowed 不是“是否能编译”的同义词:一个 profile 可以语法正确,但被 requirements ID allowlist、sandbox-mode constraint 或 managed deny-read 判定为不可选。
源码位置:codex-rs/core/src/config/permission_profile_catalog.rs :: permission_profile_catalog_from_permissions
let mut catalog = [
(BUILT_IN_READ_ONLY_PROFILE, PermissionProfile::read_only()),
(
BUILT_IN_WORKSPACE_PROFILE,
PermissionProfile::workspace_write(),
),
(
BUILT_IN_DANGER_FULL_ACCESS_PROFILE,
PermissionProfile::Disabled,
),
]
.into_iter()
.map(|(id, permission_profile)| PermissionProfileCatalogEntry {
id: id.to_string(),
description: None,
allowed: permission_profile_is_allowed(config_layer_stack, id, &permission_profile),
})
.collect::<Vec<_>>();源码位置:codex-rs/core/src/config/permission_profile_catalog.rs :: permission_profile_is_allowed
let allowed_by_id = config_layer_stack
.requirements_toml()
.allowed_permission_profiles
.as_ref()
.is_none_or(|allowed| is_permission_allowed(allowed, profile_id));
let allowed_by_sandbox_mode = config_layer_stack
.requirements()
.permission_profile
.can_set(permission_profile)
.is_ok();
let allowed_by_filesystem = config_layer_stack
.requirements()
.filesystem
.as_ref()
.is_none_or(|Sourced { value, source }| {
value.deny_read.is_empty()
|| validate_permission_profile_for_deny_read(permission_profile, source).is_ok()
});
allowed_by_id && allowed_by_sandbox_mode && allowed_by_filesystemmanaged deny-read 存在时,DangerFullAccess 与 ExternalSandbox 不可选,因为它们无法保留 managed read deny。ReadOnly 与 WorkspaceWrite 可以继续作为候选,并在最终 effective policy 中合并 Deny。
8. Snapshot身份
PermissionProfileSnapshot 将 concrete profile、active identity 和 profile-defined roots 绑定在一起。legacy override 没有 identity;命名或 built-in selection 使用 active snapshot。
源码位置:codex-rs/protocol/src/permission_profile_snapshot.rs :: PermissionProfileSnapshot
pub struct PermissionProfileSnapshot {
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
profile_workspace_roots: Vec<AbsolutePathBuf>,
}
pub fn active_with_profile_workspace_roots(
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> Self {
Self {
permission_profile,
active_permission_profile: Some(active_permission_profile),
profile_workspace_roots,
}
}ActivePermissionProfile 只保存 ID 与直接 extends 元数据,运行时不能从名称反推权限;真正执行仍读取 snapshot 中的 PermissionProfile。
源码位置:codex-rs/protocol/src/models.rs :: ActivePermissionProfile
pub struct ActivePermissionProfile {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
}PermissionProfileState 再用 Constrained<PermissionProfileSnapshot> 包装 snapshot。每次 set 会调用现有 canonical permission constraint,避免只替换 ID sidecar 而绕过 requirements。
源码位置:codex-rs/core/src/config/resolved_permission_profile.rs :: PermissionProfileState::from_constrained_snapshot
let permission_profile = Constrained::new(permission_profile, move |candidate| {
constrained_permission_profile.can_set(candidate.permission_profile())
})?;
Ok(Self { permission_profile })9. Requirements回退
requirements 约束在最终配置装配时仍会重新检查 canonical profile。若 selected profile 被强制回退,Core 清除 active identity 与 profile roots,因为原名称不再描述实际权限。
源码位置:codex-rs/core/src/config/mod.rs :: ConfigBuilder requirements application
let permission_profile_was_constrained = apply_requirement_constrained_value(
"permission_profile",
permission_profile,
&mut constrained_permission_profile,
&mut startup_warnings,
)?;
if permission_profile_was_constrained {
active_permission_profile = None;
profile_workspace_roots.clear();
}随后会把原 profile、requirements forced profile 和 managed deny-read 重新组合,保留 Deny,再添加 Codex runtime helper 所需的只读根。最终 PermissionProfileState 保存的是这份 effective profile,不是最初未经约束的编译结果。
10. 生效时机
Session 设置更新会比较前后 concrete profile。变化时先更新 environment selections/config,再刷新 managed network proxy;已经构造的旧 StepContext 仍持有自己的 snapshot,后续 Turn/Step 才读取更新后的 environment 权限。
源码位置:codex-rs/core/src/session/mod.rs :: Session::update_settings
let previous_permission_profile = state.session_configuration.permission_profile();
let updated_permission_profile = updated.permission_profile();
let permission_profile_changed =
previous_permission_profile != updated_permission_profile;
let mcp_inputs_changed =
self.mcp_inputs_differ(&state.session_configuration, &updated, &updates);
if mcp_inputs_changed {
self.mark_mcp_runtime_dirty();
}
let environment_config = updated.inferred_environment_config();
if let Some(environments) = &updates.environments {
self.services
.turn_environments
.update_selections(&environments.environments, &environment_config);
} else if state.session_configuration.inferred_environment_config() != environment_config {
self.services
.turn_environments
.update_thread_config(&environment_config);
}
state.session_configuration = updated;源码位置:codex-rs/core/src/session/mod.rs :: Session::update_settings
if permission_profile_changed {
self.refresh_managed_network_proxy_for_current_permission_profile()
.await;
}profile 变化不是只改 UI 标签。它会影响下一次 environment snapshot、工具 sandbox、直接文件工具和 managed network;但不会追溯重写已经启动的进程权限。
11. 失败定位
| 现象 | 失败阶段 | 典型原因 |
|---|---|---|
| profile ID 不存在 | selection/extends | 配置删除、父 profile 缺失 |
| inheritance cycle | extends | A extends B,B extends A |
| managed/config 同名 | catalog merge | requirements 与用户定义冲突 |
catalog 中 allowed=false | requirements | allowlist、sandbox mode 或 deny-read 冲突 |
| persisted ID 被忽略 | selection | profile 已失效或显式 override 优先 |
| profile 回退且名称消失 | final constraint | requirements 强制了另一组 concrete permissions |
| 远程 Windows path 本地未编译 | executor selection | 有意保留 URI-native path,不是丢失配置 |
| profile 更新后旧进程未变化 | lifecycle | 权限对后续 Turn/进程生效,不 retroactive |
12. 测试反推边界
以下测试覆盖 extends、managed selection、默认/持久化优先级、catalog 约束和 Turn 生效:
cd codex-rs
cargo test -p codex-core --lib config::permissions::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib config::permission_profile_selection::tests:: -- --nocapture --test-threads=1
cargo test -p codex-core --lib config::config_loader_tests::resolve_permission_profile -- --nocapture --test-threads=1
cargo test -p codex-core --lib config::tests::default_permissions -- --nocapture --test-threads=1
cargo test -p codex-core --lib persisted_permission_profile -- --nocapture --test-threads=1
cargo test -p codex-core --lib session::tests::permission_profile_updates_apply_to_next_turn_environment -- --nocapture --test-threads=1六组测试分别通过 19、2、3、7、3、1 项,共 35 项。它们覆盖继承、编译、managed catalog、选择优先级和 Turn 传播;不执行平台 sandbox。
permissions_profiles_resolve_extends_parent_first_with_child_overrides 构造 parent/child 文件和网络字段,断言 parent 先合并、child 同名字段覆盖;cycle、undefined parent 与 unsupported built-in parent 分别有独立失败测试。它们验证继承图,不验证平台路径执行。
resolves_managed_profile_without_compiling_executor_paths 同时提供本地 Windows 风格 configured profile 与 requirements managed profile,断言选择 managed ID 并保留两份未编译 TOML。它证明远程选择不会错误解释 foreign path。
persisted_permission_profile_id_wins_over_configured_default、missing 与 invalid 两个配套测试分别断言合法 persisted ID 优先,失效 ID 回退 configured default。它们验证选择优先级,不表示持久化 ID 自身携带权限。
permission_profile_updates_apply_to_next_turn_environment 更新 Session profile,断言下一 Turn environment 看到新 profile。它限定了生效时间,不证明已启动进程权限可以动态收紧。
测试覆盖解析与状态传播,不能替代平台 sandbox,也不能从 active profile ID 推断 concrete 权限。调试时应同时打印 ID、snapshot 中的 PermissionProfile、workspace roots 和 requirements 来源。
下一篇ExecPolicy语言与规则将继续追踪命令规则如何从 trusted 配置层加载、解析 shell segment,并生成 Allow、Prompt、Forbidden 与 proposed amendment。
