Skip to content

RequestPermissions工具

追踪 request_permissions 的环境解析、权限交集、审批 holder、Turn/Session 授权和后续执行消费。

基于rust-v0.150.0
CodexRustToolsPermissions

RequestPermissions工具 ​

request_permissions 不是“把权限字段写入当前配置”的快捷入口。它先选择一个 turn environment,把相对路径解析到该 environment 的 cwd,归一化请求 profile,再由 Session 根据审批策略决定自动拒绝、Guardian 审查或发送客户端事件。客户端 返回的权限还要与原始请求做交集,最后按 Turn 或 Session scope 写入不同的授权 owner;后续 Unified Exec、Apply Patch 或 extension execution 才会把这些授权合并进 additional permissions。

本文面向已经读过工具审批架构、SandboxPolicy选择、工具运行时抽象和RequestUserInput工具的读者。本文只研究动态权限请求的请求/响应生命周期,不重复一般 command approval,也不把客户端批准等同于立即修改 PermissionProfile。读完后,读者应能解释 environment_id、cwd、请求权限、实际授予权限、scope 和 strict auto review 如何在不同 owner 之间流动。

1. 工具边界 ​

1.1 注册条件 ​

request_permissions 是 core utility tool,由 add_core_utility_tools 在当前 turn 有可用 environment 且 Feature::RequestPermissionsTool 开启时注册。工具是否真正生效还要经过当前 turn 的 approval policy 和 granular 配置。它的 ToolSpec 允许 reason、可选 environment_id 和 permissions。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: add_core_utility_tools

rust
if environment_mode.has_environment()
    && features.enabled(Feature::RequestPermissionsTool)
{
    registry.add(RequestPermissionsHandler);
}

源码位置:codex-rs/core/src/tools/handlers/shell_spec.rs :: create_request_permissions_tool

rust
pub fn create_request_permissions_tool(description: String) -> ToolSpec {
    let properties = BTreeMap::from([
        (
            "reason".to_string(),
            JsonSchema::string(Some(
                "Optional short explanation for why additional permissions are needed."
                    .to_string(),
            )),
        ),
        (
            "environment_id".to_string(),
            JsonSchema::string(Some(
                "Environment id from <environment_context>. Omit to use the primary environment."
                    .to_string(),
            )),
        ),
        ("permissions".to_string(), permission_profile_schema()),
    ]);

    ToolSpec::Function(ResponsesApiTool {
        name: "request_permissions".to_string(),
        description,
        strict: false,
        defer_loading: None,
        parameters: JsonSchema::object(
            properties,
            Some(vec!["permissions".to_string()]),
            Some(false.into()),
        ),
        output_schema: None,
    })
}

1.2 策略门槛 ​

Session 在真正建立 pending holder 前检查 approval policy:AskForApproval::Never 直接返回空权限;Granular policy 如果 request_permissions 为 false,也直接返回空权限。两条路径都不发送客户端事件,模型只收到一个序列化的空授予结果。

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

rust
match turn_context.as_ref().approval_policy() {
    AskForApproval::Never => {
        return Some(RequestPermissionsResponse {
            permissions: RequestPermissionProfile::default(),
            scope: PermissionGrantScope::Turn,
            strict_auto_review: false,
        });
    }
    AskForApproval::Granular(granular_config)
        if !granular_config.allows_request_permissions() =>
    {
        return Some(RequestPermissionsResponse {
            permissions: RequestPermissionProfile::default(),
            scope: PermissionGrantScope::Turn,
            strict_auto_review: false,
        });
    }
    AskForApproval::OnRequest
    | AskForApproval::UnlessTrusted
    | AskForApproval::Granular(_) => {}
}

空权限不是错误,它表示请求被策略静默拒绝或没有授予任何内容;handler 仍会把该 response 序列化并以成功工具输出返回。

2. 请求协议 ​

2.1 Profile结构 ​

请求 profile 只有两个维度:network 和 file_system。RequestPermissionProfile 可以为空,但 handler 会在归一化后拒绝空请求,因此成功进入 Session 的请求至少包含一个网络或文件系统权限。

源码位置:codex-rs/protocol/src/request_permissions.rs :: RequestPermissionProfile

rust
#[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()
    }
}

RequestPermissionProfile 与 AdditionalPermissionProfile 有双向转换,但转换只改变类型包装,不代表已经经过请求范围交集或 grant scope 处理。

2.2 参数字段 ​

源码位置:codex-rs/protocol/src/request_permissions.rs :: RequestPermissionsArgs

rust
pub struct RequestPermissionsArgs {
    #[serde(
        default,
        rename = "environment_id",
        alias = "environmentId",
        skip_serializing_if = "Option::is_none"
    )]
    pub environment_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    pub permissions: RequestPermissionProfile,
}

environment_id 缺省时使用 primary environment;提供未知 id 时 handler 返回“requires a primary environment”这一类模型可见错误。reason 只用于客户端/Guardian 展示,不参与权限交集计算。

2.3 响应字段 ​

源码位置:codex-rs/protocol/src/request_permissions.rs :: RequestPermissionsResponse

rust
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,
}

permissions 是客户端实际想授予的子集,scope 选择 Turn 或 Session owner,strict_auto_review 只对 Turn scope 有效。handler 不直接构造 response;Session 或 App Server 负责回传并归一化。

3. Handler路径 ​

3.1 环境选择 ​

handler 先解析一个只含 environment_id 的轻量结构,再通过 resolve_tool_environment 找到完整 TurnEnvironment。这样可以在解析权限 profile 前确定相对路径的 base path。

源码位置:codex-rs/core/src/tools/handlers/request_permissions.rs :: RequestPermissionsHandler::handle_call

rust
let environment_args: RequestPermissionsEnvironmentArgs =
    parse_arguments(&arguments)?;
let Some(turn_environment) = resolve_tool_environment(
    &step_context.environments,
    environment_args.environment_id.as_deref(),
)? else {
    return Err(FunctionCallError::RespondToModel(
        "request_permissions requires a primary environment".to_string(),
    ));
};

3.2 路径归一化 ​

当前实现要求所选 environment cwd 能转成宿主 native path。随后用 parse_arguments_with_base_path 解析完整权限 profile,再调用 normalize_additional_permissions。foreign environment path 在这里被拒绝并返回模型可见错误。

源码位置:codex-rs/core/src/tools/handlers/request_permissions.rs :: RequestPermissionsHandler::handle_call

rust
let native_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
    FunctionCallError::RespondToModel(format!(
        "request_permissions cwd `{}` is not native to the Codex host: {err}",
        turn_environment.cwd()
    ))
})?;
let mut args: RequestPermissionsArgs =
    parse_arguments_with_base_path(&arguments, &native_cwd)?;
args.permissions = normalize_additional_permissions(args.permissions.into())
    .map(codex_protocol::request_permissions::RequestPermissionProfile::from)
    .map_err(FunctionCallError::RespondToModel)?;
if args.permissions.is_empty() {
    return Err(FunctionCallError::RespondToModel(
        "request_permissions requires at least one permission".to_string(),
    ));
}

3.3 Session调用 ​

handler 将完整 StepContext、selection、call id、请求参数和 invocation cancellation token 交给 Session。Session 先把 StepContext 转成 GuardianReviewContext,使 reviewer 同时看到当前 Turn 和 Step 级上下文;权限审批和最终记录仍由 Session 拥有。

源码位置:codex-rs/core/src/tools/handlers/request_permissions.rs :: RequestPermissionsHandler::handle_call

rust
let response = session
    .request_permissions_for_environment(
        &step_context,
        call_id,
        args,
        turn_environment.selection(),
        cancellation_token,
    )
    .await
    .ok_or_else(|| {
        FunctionCallError::RespondToModel(
            "request_permissions was cancelled before receiving a response".to_string(),
        )
    })?;

4. 审批路径 ​

4.1 Guardian分支 ​

如果当前 Turn 路由到 Guardian,Session 不建立客户端 pending sender,而是构造 ApprovalAction::RequestPermissions 和 ApprovalContext,交给 centralized Guardian approval。Approved 与 ApprovedExecpolicyAmendment 返回 Turn scope; ApprovedForSession 返回 Session scope;Denied、Abort、TimedOut 和 MCP amendment 返回空权限。若 reviewer 返回 network-policy amendment,Allow 映射为 Turn grant,Deny 映射为空权限。

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

rust
let action = ApprovalAction::RequestPermissions {
    id: call_id.clone(),
    turn_id: turn_context.sub_id.clone(),
    reason: args.reason,
    permissions: requested_permissions.clone(),
};
let approval_context = ApprovalContext {
    review_context: review_context.clone(),
    cancellation_token: Some(cancellation_token.clone()),
    call_id,
    tool_name: ToolName::plain("request_permissions"),
    strict_auto_review: false,
    approval_reason: None,
    retry_reason: None,
    network_approval_context: None,
};
let decision = tokio::select! {
    biased;
    _ = cancellation_token.cancelled() => return None,
    decision = self.request_guardian_approval(
        action,
        &approval_context,
    ) => decision,
};

Guardian 分支仍会经过同一个 normalize_request_permissions_response 和授权记录函数,因此 Guardian 批准并不绕过权限交集。

4.2 客户端分支 ​

非 Guardian 路径注册 PendingRequestPermissions,其中同时保存 sender、requested_permissions 和 environment selection。保存请求快照是为了响应回来时重新计算交集和本地化路径,而不是信任客户端回传的任意 profile。

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

rust
let _elicitation = self.services.elicitations.register();
let (tx_response, rx_response) = oneshot::channel();
let prev_entry = {
    let mut active = self.active_turn.lock().await;
    match active.as_mut() {
        Some(at) => {
            let mut ts = at.turn_state.lock().await;
            ts.insert_pending_request_permissions(
                call_id.clone(),
                PendingRequestPermissions {
                    tx_response,
                    requested_permissions: requested_permissions.clone(),
                    environment: environment.clone(),
                },
            )
        }
        None => None,
    }
};

key 是 call_id,与 RequestUserInput 的 sub_id key 不同;这允许同一 turn 中按不同工具调用 id 维护多个权限请求。

4.3 事件字段 ​

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

rust
let event = EventMsg::RequestPermissions(RequestPermissionsEvent {
    call_id: call_id.clone(),
    turn_id: turn_context.sub_id.clone(),
    environment_id: Some(environment.environment_id.clone()),
    started_at_ms: now_unix_timestamp_ms(),
    reason: args.reason,
    permissions: requested_permissions,
    cwd: Some(native_environment_cwd),
});
self.send_event(turn_context, event).await;

客户端看到的是已选择 environment 的 native cwd 和规范化请求;它不需要再次推断相对路径属于哪个 environment。

5. 响应归一化 ​

5.1 权限交集 ​

Session 从 pending entry 取回 requested profile,再调用 normalize_request_permissions_response。客户端可以少授予,但不能授予请求之外的路径或网络能力;非空响应经过 intersect_permission_profiles,空响应原样作为拒绝/无授权。

源码位置: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,
    }
}

5.2 严格复核 ​

strict_auto_review=true 与 Session scope 互斥。Session 发现这个组合时将权限清空、scope 改为 Turn、strict 改为 false;Turn scope 则可以保留 strict 标记,并让后续工具审批再次经过 review。

5.3 路径边界 ​

App Server 在客户端响应进入 core 前还会把 file-system paths 本地化到请求 cwd;无法本地化时产生 TurnError 并提交 Interrupt。core 随后仍会再次做 requested/actual intersection,形成两层边界。

6. 授权存储 ​

6.1 Turn授权 ​

Turn scope 写入产生请求的 originating turn state,而不是当前 active turn 的任意 state。授权以 environment id 为 key,多个响应通过 merge_permission_profiles 累加。

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

rust
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 => { /* session owner */ }
}

测试 record_granted_request_permissions_for_turn_uses_originating_turn 特意制造 originating turn 与 current active turn 不同,断言授权只写入 originating state。

6.2 Session授权 ​

Session scope 写入 SessionState.granted_permissions_by_environment_id,跨 turn 复用;它仍按 environment id 分开保存,local grant 不会自动出现在 remote environment。

源码位置:codex-rs/core/src/state/session.rs :: SessionState::record_granted_permissions

rust
pub(crate) fn record_granted_permissions(
    &mut self,
    environment_id: &str,
    permissions: AdditionalPermissionProfile,
) {
    let granted_permissions = merge_permission_profiles(
        self.granted_permissions_by_environment_id.get(environment_id),
        Some(&permissions),
    );
    if let Some(granted_permissions) = granted_permissions {
        self.granted_permissions_by_environment_id
            .insert(environment_id.to_string(), granted_permissions);
    }
}

6.3 后续读取 ​

Unified Exec、Apply Patch 和部分 extension tool 在构造执行请求前调用 apply_granted_turn_permissions,同时读取 session grants、 turn grants 和当前显式 additional permissions;有效权限存在时把 sandbox permission 提升为 WithAdditionalPermissions,并计算是否已 preapproved。OpenAI file MCP 路径也会读取 session grant,但不经过同一个 handler helper。

源码位置:codex-rs/core/src/tools/handlers/mod.rs :: apply_granted_turn_permissions

rust
let granted_session_permissions =
    session.granted_session_permissions(environment_id).await;
let granted_turn_permissions =
    session.granted_turn_permissions(environment_id).await;
let granted_permissions = merge_permission_profiles(
    granted_session_permissions.as_ref(),
    granted_turn_permissions.as_ref(),
);
let effective_permissions = merge_permission_profiles(
    additional_permissions.as_ref(),
    granted_permissions.as_ref(),
);
let sandbox_permissions = if effective_permissions.is_some()
    && !sandbox_permissions.uses_additional_permissions()
{
    SandboxPermissions::WithAdditionalPermissions
} else {
    sandbox_permissions
};

7. 客户端路径 ​

7.1 App Server ​

App Server 收到 RequestPermissions 事件后,把 core permissions 转成 PermissionsRequestApprovalParams,保存 requested profile、cwd、call id 和 pending request id;客户端响应回来后,本地化路径、保留 scope/strict 字段,再提交 Op::RequestPermissionsResponse。

源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: EventMsg::RequestPermissions

rust
let params = PermissionsRequestApprovalParams {
    thread_id: conversation_id.to_string(),
    turn_id: request.turn_id.clone(),
    item_id: request.call_id.clone(),
    environment_id: request.environment_id.clone(),
    started_at_ms: request.started_at_ms,
    cwd: request_cwd.clone(),
    reason: request.reason,
    permissions: request.permissions.into(),
};
let (pending_request_id, rx) = outgoing
    .send_request(ServerRequestPayload::PermissionsRequestApproval(params))
    .await;

响应转换对 Turn transition 错误返回 Ok(None),不向已经切换的 Turn 提交空授权;普通 client/receiver/deserialize 失败则产生 空 Turn response。无法本地化 granted paths 时,App Server 发出 TurnError 并提交 Interrupt。

7.2 TUI审批 ​

TUI 把事件转成 ApprovalRequest::Permissions,由 approval overlay 展示 filesystem/network 具体 grant;用户可以选择 Turn/Session scope、strict auto review 和部分权限。overlay 最终发送 response,不直接修改 session state。

源码位置:codex-rs/tui/src/chatwidget/tool_requests.rs :: ChatWidget::handle_request_permissions_now

rust
let request = ApprovalRequest::Permissions(PermissionsApprovalRequest {
    thread_id: self.thread_id.unwrap_or_default(),
    thread_label: None,
    call_id: ev.call_id,
    environment_id: ev.environment_id,
    reason: ev.reason,
    permissions: ev.permissions,
});
self.bottom_pane
    .push_approval_request(request, &self.config.features);

7.3 保护边界 ​

App Server 的响应转换会拒绝或中断无法本地化的 granted filesystem paths;core 的响应 intersection 又会过滤超出 requested profile 的路径。两层都通过后才会写入 Turn/Session grant。

8. 取消与失败 ​

8.1 取消等待 ​

客户端路径使用 tokio::select! 同时等待 cancellation token 与 response。取消发生时移除 pending entry 并返回 None;handler 将 None 转成 model-visible cancellation message。Guardian 路径也用同一 token 中止 review。

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

rust
self.send_event(turn_context.as_ref(), event).await;
tokio::select! {
    biased;
    _ = cancellation_token.cancelled() => {
        let mut active = self.active_turn.lock().await;
        if let Some(at) = active.as_mut() {
            let mut ts = at.turn_state.lock().await;
            let _ = ts.remove_pending_request_permissions(&call_id);
        }
        None
    }
    response = rx_response => response.ok(),
}

8.2 无匹配响应 ​

notify_request_permissions_response 以 call id 删除 pending entry。没有匹配 entry 时只 warning;不会记录授权,也不会把权限写入 session state。

8.3 外部环境 ​

当前 request_permissions 仍要求 environment cwd 可转换为 host native path;foreign remote environment path 会返回空授权或模型可见错误,不能把这个工具当成跨平台 PathUri 权限授予接口。

8.4 Hook顺序 ​

RequestPermissionsHandler 使用默认 function PreToolUse/PostToolUse 投影。PreToolUse 在 environment 解析和 pending holder 之前运行, 可以阻断或收窄请求 JSON;PostToolUse 只有在 response 已归一化并写入 Turn/Session grant 后才运行。因此 PostToolUse block 可以 拒绝 response 回灌给模型,却不能撤销已经记录的权限。阻止 grant 发生必须使用 PreToolUse、PermissionRequest hook 或 reviewer 拒绝,而不是依赖 PostToolUse。

9. 运行时消费 ​

9.1 隐式授权 ​

后续 Unified Exec、Apply Patch 或 extension execution 如果没有显式 additional permissions 或 require_escalated,会从 session/turn grant 取得已授权 profile,并将其作为 WithAdditionalPermissions 执行。显式 with_additional_permissions 请求不会偷偷走 implicit sticky grant 分支。

源码位置:codex-rs/core/src/tools/handlers/mod.rs :: implicit_granted_permissions

rust
pub(super) fn implicit_granted_permissions(
    sandbox_permissions: SandboxPermissions,
    additional_permissions: Option<&AdditionalPermissionProfile>,
    effective_additional_permissions: &EffectiveAdditionalPermissions,
) -> Option<AdditionalPermissionProfile> {
    if !sandbox_permissions.uses_additional_permissions()
        && !matches!(sandbox_permissions, SandboxPermissions::RequireEscalated)
        && additional_permissions.is_none()
    {
        effective_additional_permissions.additional_permissions.clone()
    } else {
        None
    }
}

9.2 Strict审查 ​

Turn grant 的 strict_auto_review 会写入 TurnState。后续 orchestrator 通过 active_turn_context_and_strict_auto_review 同时取得 active Turn 与 strict flag;即使命令满足普通 Skip 条件,仍会请求 reviewer。 Session scope 不允许保存这个 flag。

9.3 环境隔离 ​

grant map 的 key 是 environment id。请求 remote environment 得到的 filesystem/network 权限只会被后续同一 environment 的 handler 读取;切换回 local 不会继承 remote grant。

10. 客观边界 ​

当前实现有三条不能越过的边界:

  1. permission path 仍需转换为 Codex host native path,foreign executor 的 URI-native grant 尚未实现;
  2. Turn 与 Session grant 都是内存 owner,不会自动写入磁盘配置,也不会修改基础 PermissionProfile;
  3. PostToolUse 发生在 grant 记录之后,结果阻断不能回滚权限,只有请求前或审批阶段的拒绝能防止 grant 生效。

此外,测试能够证明 profile intersection、scope owner 和后续合并,不能证明各平台 sandbox 一定正确强制所有 granted path。真正的 OS enforcement 还取决于后续 Unified Exec、Apply Patch 或 executor backend。

11. 测试验证 ​

11.1 请求与响应 ​

源码位置:codex-rs/core/src/session/tests.rs :: request_permissions_emits_event_when_granular_policy_allows_requests

测试建立允许 request_permissions 的 granular policy,发起 filesystem/network request,断言收到事件,再提交预期 response,最后断言 handler future 返回。它覆盖正常事件和 oneshot round-trip。

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

测试向不存在 call id 提交网络权限,断言 turn grant 仍为空。它验证迟到/错误响应不会创造授权。

11.2 交集与 scope ​

源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: request_permissions_response_accepts_partial_network_and_file_system_grants

测试请求 network、read path 和 write path,客户端只返回部分 network/filesystem grant,断言转换结果只保留实际授予子集;额外的 ignored path 和 unsupported field 不会扩大权限。

源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: request_permissions_response_rejects_session_scoped_strict_auto_review

测试提交 Session scope + strict auto review,断言归一化结果为空权限、Turn scope、strict false。相邻测试验证 Turn scope 可以保留 strict flag。

11.3 存储与消费 ​

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

测试同时准备 originating/current turn,授予 Turn scope,断言只有 originating turn state 记录权限;它证明响应关联不能随 active turn 改变。

源码位置:codex-rs/core/src/tools/handlers/mod.rs :: implicit_sticky_grants_bypass_inline_permission_validation

测试给出已授权 profile 和默认 sandbox permission,断言 implicit grant 被返回;显式 additional permission 测试则断言不走 该分支。它验证后续执行 handler 的读取边界。

11.4 可执行检查 ​

bash
rg -n "RequestPermissionsHandler|request_permissions_for_environment|record_granted_permissions|apply_granted_turn_permissions" codex-rs/core/src
rg -n "normalize_request_permissions_response|PermissionsRequestApprovalParams|strictAutoReview" codex-rs/core/src codex-rs/app-server/src codex-rs/tui/src
cargo test -p codex-core request_permissions_emits_event_when_granular_policy_allows_requests
cargo test -p codex-core notify_request_permissions_response_ignores_unmatched_call_id
cargo test -p codex-core record_granted_request_permissions_for_turn_uses_originating_turn
cargo test -p codex-core implicit_sticky_grants_bypass_inline_permission_validation
cargo test -p codex-app-server request_permissions_response_accepts_partial_network_and_file_system_grants
cargo test -p codex-app-server request_permissions_response_rejects_session_scoped_strict_auto_review

这些测试覆盖请求事件、响应交集、scope owner、strict flag 和后续 sticky grant;它们不会证明所有平台的 foreign path 都能被 request_permissions 支持,也不会把 Session grant 当成永久磁盘配置。

12. 诊断路径 ​

遇到工具不可用,先查 registry 是否注册、approval policy 是否静默拒绝、以及 granular request_permissions 是否关闭;遇到权限 没有生效,检查 response scope 写入的是 originating Turn 还是 Session state、environment id 是否一致,以及后续执行是否被显式 with_additional_permissions 或 require_escalated 覆盖。若客户端批准了超出请求的路径,查看 App Server localize 和 core intersection;若 strict auto review 消失,检查是否错误地选择了 Session scope。