Codex 系统手册

权限和 elicitation 请求入口

stage-14.1.45 个文件

这一阶段像系统的“前台加保安”,发生在 Codex 正在干活儿时。模型想看更多文件、用更多环境权限,会走 request_permissions 统一登记和送审;想问真人问题,会走 request_user_input,把问题传到会话层再拿回答案。MCP 这边如果需要用户补充信息,elicitation 会判断能不能直接处理,或转给前端。执行命令、修改代码前,也分别由 exec_approval 和 patch_approval 去问用户准不准,避免机器偷偷做高风险动作。

本阶段的文件5

工具请求处理器

这些处理器接收来自工具的权限和用户输入请求,规范化其参数,并将其转发到会话批准流程。

core/src/tools/handlers/request_permissions.rs源码 ↗
orchestrationrequest handling

可以把这个文件理解成“权限申请窗口”。模型发来一份申请,说自己想要哪些额外权限;这里先确认这真的是函数工具调用,再读出参数,找到这次对话对应的运行环境,也就是代码实际执行的地方。接着它把路径按当前环境的工作目录补全,避免相对路径说不清;再把权限列表规范化,像把手写申请表整理成统一格式。如果申请里没有任何权限,它会直接拒绝,因为空申请没有意义。最后,它把整理好的申请交给会话对象去处理,等待用户或系统给出结果,再把结果转成 JSON 文本返回给模型。这个文件的重点不是“批准权限”,而是把申请安全、清楚、可追踪地送到真正能决定权限的地方。

函数细节4
RequestPermissionsHandler::tool_name29–31 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具系统:这个处理器对应的工具名字叫 request_permissions。别人要调用这个权限申请工具时,就是靠这个名字找到它。

数据流:进去的是这个处理器本身,不需要额外参数;它创建一个普通工具名 request_permissions;出来的是一个 ToolName,让工具注册表能识别这个工具。

调用关系:它是工具注册和查找时用的小标签。内部把名字交给 plain 来包装成标准格式,之后系统就能用这个名字把请求分派到这个处理器。

调用图:调用 1 个内部函数(plain)。

RequestPermissionsHandler::spec33–35 ↗
fn spec(&self) -> ToolSpec

作用:提供这个工具的说明书,告诉模型这个工具怎么用、能填哪些参数、用途是什么。没有它,模型就很难按正确格式发起权限申请。

数据流:进去的是处理器本身;它先拿到 request_permissions 的文字说明,再用这些说明创建完整工具规格;出来的是 ToolSpec,也就是工具系统给模型看的“使用说明和参数格式”。

调用关系:它在工具暴露给模型时被使用。它把 request_permissions_tool_description 生成的说明交给 create_request_permissions_tool,组合成最终规格。

调用图:调用 2 个内部函数(create_request_permissions_tool, request_permissions_tool_description)。

RequestPermissionsHandler::handle37–39 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具真正被调用时的入口。它把一次权限申请调用包装成异步任务,让系统可以一边等待结果,一边不堵住其他工作。

数据流:进去的是一次 ToolInvocation,里面有会话、当前轮次、调用编号、参数等信息;它把这些信息交给 handle_call,并用 pin 包起来变成可等待的异步结果;出来的是一个 future,也就是“稍后会完成的任务”。

调用关系:工具运行时调用它来开始处理 request_permissions。它自己不做细活,而是把全部实际流程交给 RequestPermissionsHandler::handle_call。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

RequestPermissionsHandler::handle_call43–117 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:这是权限申请的核心流程:检查调用内容,解析申请参数,找到对应环境,整理权限,提交申请,并把答复返回给模型。它像窗口工作人员,先审表,再转交,再把回执交回来。

数据流:进去的是一次完整工具调用,包含会话、当前对话轮次、取消信号、调用编号和参数;它先确认参数来自函数调用,然后解析 environment_id,找到要申请权限的运行环境;接着把当前工作目录转成主机能理解的绝对路径,用它解析权限申请里的路径;之后把权限列表规范成统一格式,并拒绝空权限列表;再把申请交给 session.request_permissions_for_environment 等待答复;最后把答复转成 JSON 文本,包装成工具输出返回。过程中如果参数不对、环境缺失、路径不合适、申请被取消或结果无法序列化,就会返回相应错误。

调用关系:它由 RequestPermissionsHandler::handle 调起,是整个文件最主要的工作函数。它会调用 parse_arguments 先读环境信息,调用 resolve_tool_environment 找到运行环境,调用 parse_arguments_with_base_path 解析带路径的申请,调用 normalize_additional_permissions 清理权限格式,最后用 from_text 和 boxed_tool_output 把结果包装成模型能读的输出。

调用图:调用 6 个内部函数(from_text, boxed_tool_output, parse_arguments, parse_arguments_with_base_path, resolve_tool_environment, normalize_additional_permissions);被 1 处调用(handle);外部调用 2 个(to_string, RespondToModel)。

core/src/tools/handlers/request_user_input.rs源码 ↗
domain_logicrequest handling

这个文件像一个前台接待员,专门处理 request_user_input 这个工具调用。它先告诉系统:这个工具叫什么、长什么样、在当前协作模式下能不能用。真正收到调用时,它不会马上去问用户,而是先检查几件事:传进来的内容是不是函数参数、是不是根线程在调用、当前模式是否允许提问、参数能不能解析并整理成标准格式。只有这些都通过后,它才把问题交给 session 去真正等待用户输入。用户答完后,它把回答转成 JSON 字符串,再包装成工具输出返回给模型。一个重要点是:非根代理不能随便打断流程问用户,这避免了后台子任务乱弹问题;如果工具不可用或被取消,它会把原因明确回给模型。

函数细节4
RequestUserInputHandler::tool_name24–26 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具系统:这个处理器对应的工具名字就是 request_user_input。这样系统收到同名工具调用时,才能把活儿派给它。

数据流:进去的是这个处理器本身 → 它读取固定的工具名常量,并用 ToolName::plain 包成系统认识的工具名格式 → 出来的是一个 ToolName,不改动任何状态。

调用关系:这是工具注册和匹配时会用到的小入口。它把固定名字交给 plain 包装,后续系统就靠这个名字把模型的 request_user_input 调用路由到这个处理器。

调用图:调用 1 个内部函数(plain)。

RequestUserInputHandler::spec28–30 ↗
fn spec(&self) -> ToolSpec

作用:生成这个工具的说明书,也就是告诉模型这个工具能做什么、该怎么填参数。说明书会根据当前允许的协作模式调整描述。

数据流:进去的是处理器里的 available_modes,也就是哪些模式允许使用这个工具 → 它先用 request_user_input_tool_description 生成说明文字,再交给 create_request_user_input_tool 做成完整工具规格 → 出来的是 ToolSpec,供系统展示给模型使用。

调用关系:这是工具暴露给模型之前会用到的步骤。它把“当前环境允许什么”变成模型能读懂的工具描述,并把具体创建工作交给 request_user_input_spec 里的两个函数。

调用图:调用 2 个内部函数(create_request_user_input_tool, request_user_input_tool_description)。

RequestUserInputHandler::handle32–34 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具调用真正进来时的统一入口。它把一次调用包装成异步任务,让系统可以一边等用户回答,一边不堵住整个程序。

数据流:进去的是一次 ToolInvocation,也就是模型发来的工具调用和上下文 → 它调用 handle_call 做实际处理,并用 Box::pin 把这个异步过程固定成工具系统要求的返回形式 → 出来的是一个将来会完成的工具执行结果。

调用关系:工具运行时会先调用 handle。handle 自己不做复杂判断,只是把工作转交给 handle_call;handle_call 才负责检查权限、解析参数、请求用户输入和返回结果。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

RequestUserInputHandler::handle_call38–92 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:这是整个文件的核心:检查这次“问用户”的请求是否合法,真的去等待用户回答,然后把回答整理成模型能读的结果。它也负责把各种失败情况变成清楚的错误消息。

数据流:进去的是 ToolInvocation,里面有会话 session、当前轮次 turn、调用编号 call_id 和模型给的参数 payload → 它先确认 payload 是函数参数;再拒绝非根线程调用;接着查看当前协作模式,判断工具是否可用;然后用 parse_arguments 把原始参数解析成 RequestUserInputArgs,并用 normalize_request_user_input_args 做规范化;之后调用 session.request_user_input 等用户回复;拿到回复后用 serde_json::to_string 转成 JSON 文本,再用 FunctionToolOutput::from_text 和 boxed_tool_output 包成工具输出 → 出来的是成功的工具输出,或者一个会回给模型/中止流程的错误;它不会直接保存回答,只把回答作为本次工具调用结果返回。

调用关系:它只被 RequestUserInputHandler::handle 调用,是一次工具执行的主流程。它把参数解析交给 parse_arguments,把参数清洗交给 normalize_request_user_input_args,把可用性判断交给 request_user_input_unavailable_message,把真正向用户提问交给 session.request_user_input,最后把结果包装成标准工具输出交回工具系统。

调用图:调用 5 个内部函数(from_text, boxed_tool_output, parse_arguments, normalize_request_user_input_args, request_user_input_unavailable_message);被 1 处调用(handle);外部调用 3 个(format!, to_string, RespondToModel)。

MCP 引出桥接

此桥接会跟踪 MCP 引出请求,应用批准策略,并在 MCP 回调与 Codex 协议事件之间路由请求及后续响应。

codex-mcp/src/elicitation.rs源码 ↗
orchestrationrequest handling

可以把这个文件想成一个“询问前台”。MCP 服务器有时会问 Codex:能不能让用户填个表、确认一下,或者打开某个链接?这里会先看当前权限政策:如果系统处在自动拒绝模式,就直接回绝;如果政策允许并且请求很安全,比如只是一个不需要填任何字段的确认表,就自动接受;如果政策明确不允许,就直接拒绝。剩下的情况,先交给可选的审核器看一眼;审核器不处理时,就把请求包装成 Codex 协议事件发出去,并把一个一次性回复通道存起来。之后外部调用 resolve 时,再用保存的通道把用户选择送回原来的 MCP 请求。这里的互斥锁(一把锁,防止多个任务同时改同一份数据)用来保护共享状态,比如待回复请求表和权限设置。

函数细节7
ElicitationRequestManager::new61–73 ↗
fn new(
        approval_policy: AskForApproval,
        permission_profile: PermissionProfile,
        reviewer: Option<ElicitationReviewerHandle>,
    ) -> Self

作用:创建一个新的询问请求管理器。调用者给它初始审批策略、权限档案,以及可选的审核器,它就准备好记录后续所有 MCP 询问请求。

数据流:进去的是审批策略、权限配置和一个可能存在的审核器 → 它把待回复请求表建成空的,把这些配置放进可共享、可加锁的容器里,并把自动拒绝开关设为关闭 → 出来的是一个可以被复制使用的 ElicitationRequestManager。

调用关系:这是整个模块的起点之一。上层初始化流程会调用它来准备管理器;测试里也会用它搭出不同权限场景,检查空表单能不能自动接受、有字段表单会不会被拦住。

调用图:被 4 处调用(new, new_uninitialized_with_permission_profile, disabled_permissions_auto_accept_elicitation_with_empty_form_schema, disabled_permissions_do_not_auto_accept_elicitation_with_requested_fields);外部调用 4 个(new, new, new, new)。

ElicitationRequestManager::auto_deny75–80 ↗
fn auto_deny(&self) -> bool

作用:读取当前是否开启了“所有询问都自动拒绝”的开关。这个开关像总闸,一开就不再弹出问题,直接拒绝。

数据流:进去的是管理器本身 → 它尝试锁住保存开关的地方并读取布尔值;如果锁出问题,就保守地当作没有开启 → 出来的是 true 或 false,不改动任何状态。

调用关系:它会被查询自动拒绝状态的上层接口使用,比如 elicitations_auto_deny。它只负责看当前开关,不参与发送事件或回复请求。

调用图:被 1 处调用(elicitations_auto_deny)。

ElicitationRequestManager::set_auto_deny82–86 ↗
fn set_auto_deny(&self, auto_deny: bool)

作用:设置“所有询问都自动拒绝”的开关。外部需要临时禁止 MCP 弹出询问时,会用它切换状态。

数据流:进去的是一个 true 或 false → 它锁住保存开关的地方,把旧值替换成新值;如果拿锁失败,就不做修改 → 出来没有返回值,但管理器里的自动拒绝状态可能已经改变。

调用关系:它被 set_elicitations_auto_deny 这样的上层接口调用。设置之后,后面由 make_sender 处理的新询问会先看到这个开关,并可能直接拒绝。

调用图:被 1 处调用(set_elicitations_auto_deny)。

ElicitationRequestManager::resolve88–101 ↗
async fn resolve(
        &self,
        server_name: String,
        id: RequestId,
        response: ElicitationResponse,
    ) -> Result<()>

作用:把某个等待中的询问请求补上最终答案。比如用户在界面上点了同意或拒绝后,上层就用它把结果送回原来的 MCP 服务器。

数据流:进去的是服务器名、请求编号和要返回的回答 → 它在待回复请求表里找到对应的一次性发送通道,移除这条记录,然后把回答发过去 → 成功时返回空结果;如果找不到请求或发送失败,就返回错误。

调用关系:它被 resolve_elicitation 调用,处在流程的收尾位置。make_sender 之前会把等待回复的通道存进表里;resolve 后来取出这个通道,把用户或系统的决定交还给正在等待的 MCP 请求。

调用图:被 1 处调用(resolve_elicitation)。

ElicitationRequestManager::make_sender103–231 ↗
fn make_sender(
        &self,
        server_name: String,
        tx_event: Sender<Event>,
    ) -> SendElicitation

作用:生成一个真正处理 MCP 询问请求的发送器。这个发送器每次收到服务器的询问时,会按政策自动接受、拒绝、交给审核器,或者发事件等待外部回复。

数据流:进去的是服务器名和一个事件发送通道 → 它返回一个异步函数;这个函数之后会接收请求编号和询问内容,先检查自动拒绝,再检查审批政策和权限档案,必要时调用审核器;如果还不能决定,就把询问转换成 Codex 协议事件发出去,同时存下一次性回复通道并等待结果 → 出来的是给 MCP 服务器的 ElicitationResponse,或者错误。

调用关系:这是本文件最核心的流水线。MCP 客户端拿到它后,在服务器发起询问时调用。它会使用自动拒绝开关、审批策略、权限档案、可选审核器、待回复请求表和事件通道;如果请求被转给外部处理,后续需要 ElicitationRequestManager::resolve 来把答案送回来。

调用图:外部调用 1 个(new)。

elicitation_is_rejected_by_policy234–242 ↗
fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool

作用:判断当前审批政策是否要求直接拒绝 MCP 询问。它把一堆权限模式翻译成一个简单答案:这次能不能继续问用户。

数据流:进去的是 AskForApproval 审批策略 → 它逐项判断:Never 表示永远不问所以拒绝;普通的按需询问策略不拒绝;细粒度策略则查看是否允许 MCP 询问 → 出来的是 true 或 false。

调用关系:它服务于询问处理流程,特别是在 make_sender 里决定是否要提前拒绝请求。它不发送消息,也不保存状态,只提供一个清楚的政策判断。

can_auto_accept_elicitation246–256 ↗
fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) -> bool

作用:判断某个询问是否安全到可以自动接受。这里非常保守,只允许没有任何填写字段的表单确认被自动接受,链接类请求不会自动接受。

数据流:进去的是 MCP 询问内容 → 它看请求类型:如果是表单,就检查表单 schema(可以理解成“要求用户填哪些字段的说明”)里是否没有字段;如果是网址请求,就直接判定不能自动接受 → 出来的是 true 或 false。

调用关系:它是 make_sender 自动接受分支里的安全检查。即使整体权限允许自动批准,也还要经过它确认请求本身足够简单,避免系统在没有用户参与的情况下自动提交实际内容或打开链接。

批准专用 MCP 入口

这些适配器为执行和补丁批准构建具体的 MCP 引出请求,然后将客户端决策转换回 Codex 批准操作。

mcp-server/src/exec_approval.rs源码 ↗
orchestrationrequest handling

可以把这个文件理解成一个“执行命令前的门卫”。当 Codex 准备运行某条 shell 命令时,这里会把命令、所在目录、相关编号等信息包装成一条 MCP 请求。MCP 是 Model Context Protocol,可以粗略理解成模型和外部客户端之间约定好的通信格式。它会发出一个 elicitation/create 请求,也就是“请客户端向用户要一个决定”。用户或客户端回复后,这个文件再把结果翻译成 Codex 内部能懂的审批结果:允许或拒绝。一个重要细节是,如果回复内容坏了、看不懂,代码会保守地当作拒绝,避免误放行危险操作。它还把等待回复的工作放到单独的异步任务里,避免卡住主流程。

函数细节2
handle_exec_approval_request51–110 ↗
async fn handle_exec_approval_request(
    command: Vec<String>,
    cwd: PathBuf,
    outgoing: Arc<crate::outgoing_message::OutgoingMessageSender>,
    codex: Arc<CodexThread>,
    request_id: Reque

作用:当 Codex 想执行命令时,这个函数负责发起“请用户批准”的请求。它把命令整理成人能看懂的话,再通过 MCP 发给客户端,让客户端去问用户。

数据流:进去的是命令列表、当前目录、各种用于追踪这次调用的编号、发送消息的通道、Codex 线程对象,以及已经解析过的命令信息。它先把命令拼成一行提示文字,比如“允许 Codex 在某目录运行某命令吗?”,再把这些信息打包成 JSON。打包失败时,它会通过 outgoing 发回一个参数错误并停止;打包成功时,它发出 elicitation/create 请求,然后启动一个后台异步任务等待回复。出来的结果不是直接返回值,而是产生了一条发给客户端的审批请求,并安排好了后续接收回复的流程。

调用关系:它由 run_codex_tool_session_inner 在工具会话中触发,通常发生在 Codex 准备执行外部命令、但需要用户确认的时候。它自己负责把请求发出去,然后把等待回复的工作交给 on_exec_approval_response,这样主会话不用停在那里傻等。

调用图:调用 1 个内部函数(on_exec_approval_response);被 1 处调用(run_codex_tool_session_inner);外部调用 8 个(invalid_params, clone, error!, format!, json!, to_value, try_join, spawn)。

on_exec_approval_response112–147 ↗
async fn on_exec_approval_response(
    approval_id: String,
    event_id: String,
    receiver: tokio::sync::oneshot::Receiver<serde_json::Value>,
    codex: Arc<CodexThread>,
)

作用:这个函数负责等客户端的审批回复,并把“允许”或“拒绝”的决定送回 Codex。它相当于门卫收到用户批条后,把结果登记到系统里。

数据流:进去的是审批编号、事件编号、一个只能收一次回复的通道,以及 Codex 线程对象。它先等待客户端回传 JSON 数据;如果请求失败,就记录错误并结束。拿到数据后,它尝试解析成审批结果;如果解析失败,就出于安全考虑默认改成“拒绝”。最后它把这个决定包装成 Codex 内部的 ExecApproval 操作提交给 Codex,让后续流程知道这条命令能不能执行。

调用关系:它不会自己主动开始,而是由 handle_exec_approval_request 在发出审批请求后放到后台任务里运行。它位于整个流程的后半段:前面负责问,后面它负责收答案,并把答案交回 Codex 的主系统。

调用图:被 1 处调用(handle_exec_approval_request);外部调用 1 个(error!)。

mcp-server/src/patch_approval.rs源码 ↗
orchestrationrequest handling

当 Codex 准备修改文件时,不能直接动手,因为这可能会改坏代码或碰到用户不想改的地方。这个文件就像一个“审批窗口”:先把原因、线程编号、工具调用编号、要改哪些文件等信息打包成一条请求,通过 MCP(一种让工具和客户端对话的协议)发出去,请客户端向用户确认。发送时如果打包成 JSON 失败,就立刻回错误,避免系统假装已经询问。请求发出后,它不会卡住主流程,而是开一个后台任务等回复。等回复来了,它把用户的决定转换成 Codex 能懂的 PatchApproval 操作;如果请求失败或回复格式不对,就默认拒绝,宁可不改,也不冒险乱改代码。

函数细节2
handle_patch_approval_request44–101 ↗
async fn handle_patch_approval_request(
    call_id: String,
    reason: Option<String>,
    grant_root: Option<PathBuf>,
    changes: HashMap<PathBuf, FileChange>,
    outgoing: Arc<OutgoingMessageSe

作用:这个函数在 Codex 想应用一批代码改动时被调用,用来向外部客户端发起一次“请用户批准改动”的请求。它的重点是把审批需要的所有上下文整理好,并把等待回复的工作放到后台,不阻塞主流程。

数据流:进去的是一次改动申请的信息:调用编号、申请原因、允许范围、文件改动列表、当前线程、工具调用和事件编号,以及能发消息的通道。它先拼出给用户看的提示文字,再把这些内容装进 PatchApprovalElicitRequestParams,转换成 JSON。如果转换失败,它通过 outgoing 发回“参数无效”的错误并结束;如果成功,它发送 elicitation/create 请求,然后启动一个后台任务等待用户回复。出来的不是直接返回批准结果,而是留下一个异步等待流程,之后会把结果送回 Codex。

调用关系:它由 run_codex_tool_session_inner 在工具会话中触发,说明它处在 Codex 执行工具、准备改文件的中间环节。它自己负责发出审批请求,然后把“等用户回答并提交结果”的后半段交给 on_patch_approval_response,这样主代理循环不用停在那里干等。

调用图:调用 1 个内部函数(on_patch_approval_response);被 1 处调用(run_codex_tool_session_inner);外部调用 8 个(invalid_params, new, clone, error!, format!, json!, to_value, spawn)。

on_patch_approval_response103–142 ↗
async fn on_patch_approval_response(
    approval_id: String,
    receiver: tokio::sync::oneshot::Receiver<serde_json::Value>,
    codex: Arc<CodexThread>,
)

作用:这个函数负责等待用户对改代码请求的回答,并把回答告诉 Codex。它也负责兜底:如果等不到回复,或者回复看不懂,就按“拒绝”处理,保护用户文件不被未经确认地修改。

数据流:进去的是审批编号、一个只能收到一次回复的接收器,以及 Codex 线程对象。它先等待外部客户端返回 JSON;如果等待失败,就记录错误,并向 Codex 提交一个“拒绝”的 PatchApproval。如果收到了内容,它尝试解析成 PatchApprovalResponse,里面包含用户的决定;解析失败时也默认生成“拒绝”。最后它把审批编号和决定一起提交给 Codex,让 Codex 知道这批改动能不能继续。

调用关系:它由 handle_patch_approval_request 启动在后台任务里运行,是审批流程的收尾工人。前一个函数负责把问题问出去,这个函数负责收答案;答案最终通过 Codex 的 submit 入口回到核心流程,决定后续是否真正应用补丁。

调用图:被 1 处调用(handle_patch_approval_request);外部调用 1 个(error!)。