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
#[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
/// 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
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
#[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
#[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
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
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
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
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
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
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
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 → 平台执行。跳过任何一层,都容易把审批文案当成真正的运行时权限。
