Skip to content

PermissionProfile解析

追踪权限配置从命名profile、extends、requirements和持久化选择,编译为每个environment与Turn使用的canonical PermissionProfile。

基于rust-v0.150.0
CodexRustSecurityPermissions

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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_filesystem

managed 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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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 cycleextendsA extends B,B extends A
managed/config 同名catalog mergerequirements 与用户定义冲突
catalog 中 allowed=falserequirementsallowlist、sandbox mode 或 deny-read 冲突
persisted ID 被忽略selectionprofile 已失效或显式 override 优先
profile 回退且名称消失final constraintrequirements 强制了另一组 concrete permissions
远程 Windows path 本地未编译executor selection有意保留 URI-native path,不是丢失配置
profile 更新后旧进程未变化lifecycle权限对后续 Turn/进程生效,不 retroactive

12. 测试反推边界 ​

以下测试覆盖 extends、managed selection、默认/持久化优先级、catalog 约束和 Turn 生效:

text
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。