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
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
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
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
#[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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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. 客观边界
当前实现有三条不能越过的边界:
- permission path 仍需转换为 Codex host native path,foreign executor 的 URI-native grant 尚未实现;
- Turn 与 Session grant 都是内存 owner,不会自动写入磁盘配置,也不会修改基础
PermissionProfile; - 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 可执行检查
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。
