Skip to content

Permissions协议

沿着 PermissionProfile、额外权限请求、环境归一化和运行时执行链,理解权限如何从协议值变成实际约束。

基于rust-v0.150.0
CodexRustProtocolPermissions

Permissions协议 ​

本文承接Approvals协议。审批回答“这一次是否允许”,权限协议回答“允许哪些能力、作用于哪个环境、持续多久”。在 rust-v0.150.0 中,这两层通过 RequestPermissionProfile、AdditionalPermissionProfile、PermissionProfile 和 Session/Turn 状态连接起来。

不要把权限 profile、审批决定和操作系统 sandbox 当成同一对象:profile 是能力集合,审批是一次决策,sandbox 是执行 owner 根据已解析 profile 构造的约束。它们之间还有 environment、cwd、turn 和 session 作用域。

1. 请求与授权类型 ​

源码位置:codex-rs/protocol/src/request_permissions.rs :: PermissionGrantScope、RequestPermissionProfile、RequestPermissionsArgs、RequestPermissionsResponse、RequestPermissionsEvent

rust
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum PermissionGrantScope {
    #[default]
    Turn,
    Session,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(deny_unknown_fields)]
pub struct RequestPermissionProfile {
    pub network: Option<NetworkPermissions>,
    pub file_system: Option<FileSystemPermissions>,
}

impl RequestPermissionProfile {
    pub fn is_empty(&self) -> bool {
        self.network.is_none() && self.file_system.is_none()
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestPermissionsResponse {
    pub permissions: RequestPermissionProfile,
    #[serde(default)]
    pub scope: PermissionGrantScope,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub strict_auto_review: bool,
}

请求 profile 是部分集合:网络和文件系统可以独立为空。Turn 与 Session 只描述授权保存在哪里,不能绕过当前 profile、exec policy 或环境隔离。strict_auto_review 还会影响后续命令是否继续经过严格 reviewer。

2. 额外权限Profile ​

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

rust
/// Partial permission overlay used for per-command requests and approved
/// session/turn grants.
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
pub struct AdditionalPermissionProfile {
    pub network: Option<NetworkPermissions>,
    pub file_system: Option<FileSystemPermissions>,
}

#[derive(Debug, Clone, Eq, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PermissionProfile {
    Managed {
        file_system: ManagedFileSystemPermissions,
        network: NetworkSandboxPolicy,
    },
    Disabled,
    External { network: NetworkSandboxPolicy },
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
pub struct ActivePermissionProfile {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub extends: Option<String>,
}

AdditionalPermissionProfile 是 overlay,不是完整的执行策略;PermissionProfile 才是当前 turn/environment 可以交给 sandbox owner 的 canonical 结果。ActivePermissionProfile 只是显示选中的 profile 身份,运行时必须信任前者,不能从名字反推出权限内容。

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

rust
pub enum ManagedFileSystemPermissions {
    Restricted {
        entries: Vec<FileSystemSandboxEntry>,
        glob_scan_max_depth: Option<NonZeroUsize>,
    },
    Unrestricted,
}

impl ManagedFileSystemPermissions {
    pub fn to_sandbox_policy(&self) -> FileSystemSandboxPolicy {
        match self {
            Self::Restricted {
                entries,
                glob_scan_max_depth,
            } => FileSystemSandboxPolicy {
                kind: FileSystemSandboxKind::Restricted,
                glob_scan_max_depth: glob_scan_max_depth.map(usize::from),
                entries: entries.clone(),
            },
            Self::Unrestricted => FileSystemSandboxPolicy::unrestricted(),
        }
    }
}

managed profile 的 filesystem 可以是有界 entries,也可以是 unrestricted;这只是 profile 表达能力,是否被选中和如何 enforcement 由外层配置及平台 owner 决定。

3. Profile执行权 ​

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

rust
#[derive(Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum SandboxEnforcement {
    #[default]
    Managed,
    Disabled,
    External,
}

impl SandboxEnforcement {
    pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self {
        match sandbox_policy {
            SandboxPolicy::DangerFullAccess => Self::Disabled,
            SandboxPolicy::ExternalSandbox { .. } => Self::External,
            SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed,
        }
    }
}

Managed 表示 Codex 构造 sandbox,Disabled 表示不加 outer filesystem sandbox,External 表示隔离由调用方负责。将 External 当成 Disabled 会错误地把“外部 owner 尚未执行”解释为“完全开放”。

4. Session保存快照 ​

源码位置:codex-rs/core/src/config/resolved_permission_profile.rs :: PermissionProfileState

rust
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PermissionProfileState {
    permission_profile: Constrained<PermissionProfileSnapshot>,
}

pub(crate) fn from_constrained_active_profile(
    constrained_permission_profile: Constrained<PermissionProfile>,
    active_permission_profile: Option<ActivePermissionProfile>,
    profile_workspace_roots: Vec<AbsolutePathBuf>,
) -> ConstraintResult<Self> {
    let permission_profile = constrained_permission_profile.get().clone();
    let snapshot = match active_permission_profile {
        Some(active_permission_profile) => {
            PermissionProfileSnapshot::active_with_profile_workspace_roots(
                permission_profile,
                active_permission_profile,
                profile_workspace_roots,
            )
        }
        None => PermissionProfileSnapshot::legacy(permission_profile),
    };
    Self::from_constrained_snapshot(constrained_permission_profile, snapshot)
}

快照把 concrete permissions、active profile 身份和 profile 声明的 workspace roots 放在一起原子安装。这样 profile 名称和解析结果不会在 session 中分别更新,避免显示值与执行值脱节。

5. 请求进入Session ​

源码位置:codex-rs/core/src/session/handlers.rs :: request_permissions_response

rust
pub async fn request_permissions_response(
    sess: &Arc<Session>,
    id: String,
    response: RequestPermissionsResponse,
) {
    sess.notify_request_permissions_response(&id, response).await;
}

handler 不直接修改 sandbox。它把 callback id 和 response 交给 Session;Session 找到挂起请求后,按 originating environment 和 turn 处理 scope、交集、strict review 和 waiter 生命周期。

源码位置:codex-rs/core/src/session/mod.rs :: notify_request_permissions_response

rust
pub async fn notify_request_permissions_response(
    &self,
    call_id: &str,
    response: RequestPermissionsResponse,
) {
    let entry = {
        let mut active = self.active_turn.lock().await;
        match active.as_mut() {
            Some(at) => {
                let mut ts = at.turn_state.lock().await;
                ts.remove_pending_request_permissions(call_id)
            }
            None => None,
        }
    };
    // matching request is normalized, recorded and replied below
}

未知 call_id 没有匹配 entry 时不会授予权限;迟到响应不能重新创建已经释放的 pending request。

6. 归一化与请求交集 ​

源码位置:codex-rs/core/src/session/mod.rs :: normalize_request_permissions_response

rust
fn normalize_request_permissions_response(
    requested_permissions: RequestPermissionProfile,
    response: RequestPermissionsResponse,
    cwd: &Path,
) -> RequestPermissionsResponse {
    if response.strict_auto_review && matches!(response.scope, PermissionGrantScope::Session) {
        return RequestPermissionsResponse {
            permissions: RequestPermissionProfile::default(),
            scope: PermissionGrantScope::Turn,
            strict_auto_review: false,
        };
    }

    if response.permissions.is_empty() {
        return response;
    }

    RequestPermissionsResponse {
        permissions: intersect_permission_profiles(
            requested_permissions.into(),
            response.permissions.into(),
            cwd,
        )
        .into(),
        scope: response.scope,
        strict_auto_review: response.strict_auto_review,
    }
}

授权不是客户端想给多少就给多少:Session 把响应与原始 requested profile 求交集,并以请求环境 cwd 作为路径解释基准。strict_auto_review + Session 会被降级为空权限的 Turn 级响应,防止严格审查标记跨越整个 session。

7. 授权作用域 ​

源码位置:codex-rs/core/src/session/mod.rs :: record_granted_request_permissions_for_turn

rust
async fn record_granted_request_permissions_for_turn(
    &self,
    response: &RequestPermissionsResponse,
    environment_id: &str,
    originating_turn_state: Option<&Arc<Mutex<crate::state::TurnState>>>,
) {
    if response.permissions.is_empty() {
        return;
    }
    match response.scope {
        PermissionGrantScope::Turn => {
            if let Some(turn_state) = originating_turn_state {
                let mut ts = turn_state.lock().await;
                let permissions: AdditionalPermissionProfile =
                    response.permissions.clone().into();
                ts.record_granted_permissions(environment_id, permissions);
                if response.strict_auto_review {
                    ts.enable_strict_auto_review();
                }
            }
        }
        PermissionGrantScope::Session => {
            let mut state = self.state.lock().await;
            state.record_granted_permissions(
                environment_id,
                response.permissions.clone().into(),
            );
        }
    }
}

Turn grant写入 originating turn state,Session grant写入 session state;两者都带 environment_id。因此“同一 session”不等于“所有 environment 都自动共享”,跨环境请求必须重新计算。

8. 读取已授予权限 ​

源码位置:codex-rs/core/src/session/mod.rs :: granted_turn_permissions、granted_session_permissions

rust
pub(crate) async fn granted_turn_permissions(
    &self,
    environment_id: &str,
) -> Option<AdditionalPermissionProfile> {
    let active = self.active_turn.lock().await;
    let active = active.as_ref()?;
    let ts = active.turn_state.lock().await;
    ts.granted_permissions(environment_id)
}

pub(crate) async fn granted_session_permissions(
    &self,
    environment_id: &str,
) -> Option<AdditionalPermissionProfile> {
    let state = self.state.lock().await;
    state.granted_permissions(environment_id)
}

工具重试时通常要同时检查 turn grant、session grant 和本次 action 的 additional permissions。只读一次 Session 配置无法得到完整的运行时权限。

9. App Server投影 ​

源码位置:codex-rs/app-server-protocol/src/protocol/v2/permissions.rs :: AdditionalPermissionProfile、GrantedPermissionProfile、PermissionsRequestApprovalParams、PermissionsRequestApprovalResponse

rust
pub struct AdditionalPermissionProfile {
    pub network: Option<AdditionalNetworkPermissions>,
    pub file_system: Option<AdditionalFileSystemPermissions>,
}

pub struct GrantedPermissionProfile {
    pub network: Option<AdditionalNetworkPermissions>,
    pub file_system: Option<AdditionalFileSystemPermissions>,
}

pub struct PermissionsRequestApprovalParams {
    pub thread_id: String,
    pub turn_id: String,
    pub item_id: String,
    pub environment_id: Option<String>,
    pub started_at_ms: i64,
    pub cwd: AbsolutePathBuf,
    pub reason: Option<String>,
    pub permissions: RequestPermissionProfile,
}

pub struct PermissionsRequestApprovalResponse {
    pub permissions: GrantedPermissionProfile,
    pub scope: PermissionGrantScope,
    pub strict_auto_review: Option<bool>,
}

App Server 使用 camelCase wire 字段,并把 Core profile 转为自己的 Additional*Permissions 结构。TryFrom 可能因路径 URI 或权限项无法表示而失败,不能把 schema 能序列化误解成 Core 一定接受。

10. 测试与边界 ​

当前测试覆盖未知 call id、originating turn 归属、Turn/Session scope、strict auto review 降级、cwd 归一化和 App Server 权限转换。

源码位置:codex-rs/core/src/session/tests.rs :: notify_request_permissions_response_ignores_unmatched_call_id、record_granted_request_permissions_for_turn_uses_originating_turn、request_permissions_response_materializes_session_cwd_grants_before_recording

源码位置:codex-rs/app-server-protocol/src/protocol/v2/tests.rs :: permissions_request_approval_uses_request_permission_profile、permissions_request_approval_rejects_macos_permissions、additional_file_system_permissions_preserves_canonical_entries

text
cd codex-rs
cargo test -p codex-core notify_request_permissions_response_ignores_unmatched_call_id -- --nocapture --test-threads=1
cargo test -p codex-core record_granted_request_permissions_for_turn_uses_originating_turn -- --nocapture --test-threads=1
cargo test -p codex-core request_permissions_response_materializes_session_cwd_grants_before_recording -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol permissions_request_approval_uses_request_permission_profile -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol permissions_request_approval_rejects_macos_permissions -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol additional_file_system_permissions_preserves_canonical_entries -- --nocapture --test-threads=1

这些测试说明授权关联、作用域和协议转换边界;不能证明操作系统 sandbox 最终允许某个路径、远程 environment 已经执行策略,或所有平台都支持相同权限表达。

11. 源码定位练习 ​

遇到“已经授权但命令仍被拒绝”,先查请求的 call_id、environment_id 和 cwd,再查 requested/response 交集,随后分别读取 originating Turn grant、Session grant 和当前 PermissionProfile。最后才进入 Unified Exec、Bubblewrap、Seatbelt 或 Windows enforcement owner。

遇到“Session 授权泄露到另一环境”,检查 grant map 的 environment key;遇到“客户端显示允许但 Core 没有能力”,检查 App Server GrantedPermissionProfile 到 Core 的 TryFrom 是否拒绝了不可表示的路径或权限项。

权限协议的学习顺序应是:请求 profile → Session 归一化 → Turn/Session 记录 → additional overlay → canonical PermissionProfile → 平台执行。跳过任何一层,都容易把审批文案当成真正的运行时权限。