Permission and elicitation request ingress
This stage is the front desk for requests that need a human or policy decision. It sits in the main work loop, when the assistant or an outside MCP integration needs permission, confirmation, or extra information before continuing. MCP means “Model Context Protocol,” a way for other tools to connect to Codex.
The permission request handler lets the model ask for broader access, such as using a file or action that is currently blocked. It checks the request, connects it to the correct workspace, and sends it into the session’s approval path. The user input handler does a similar job when the assistant must pause and ask the human a question, then returns the answer as tool output.
For MCP traffic, elicitation decides whether an outside request is safe to accept, must be rejected by policy, or should be forwarded for review. The exec approval code asks an MCP client before running a shell command. The patch approval code asks before applying code edits. Together, these pieces normalize many kinds of “may I?” and “please answer” moments into the same review machinery.
Tool request handlers
These handlers accept tool-originated permission and user-input requests, normalize their arguments, and forward them into the session approval flow.
core/src/tools/handlers/request_permissions.rssource ↗
This file is the bridge between a model tool call named request_permissions and the system that can approve or deny extra access. Without it, the model could not formally ask to expand its sandbox permissions, so any task needing new file or environment access would get stuck or fail in a less clear way.
The handler first identifies itself as the request_permissions tool and provides the tool’s public shape, which tells the model how to call it. When a call arrives, it accepts only a function-style payload with JSON arguments. It then looks for an optional environment ID, finds the matching tool environment for the current turn, and makes sure that environment has a host-native current directory. This matters because permission paths need to be interpreted relative to a real path on the machine running Codex.
Next, it parses the requested permissions using that current directory as the base. It normalizes them, meaning it turns different but equivalent permission requests into a consistent internal form. It rejects empty requests, because asking for “no permissions” is not useful. Finally, it asks the session to request those permissions for the selected environment, waits for a response unless the operation is cancelled, serializes the response as JSON text, and returns it to the model.
RequestPermissionsHandler::tool_name29–31 ↗
fn tool_name(&self) -> ToolName
Purpose: This function gives the handler its public tool name: request_permissions. The registry uses this name to match an incoming model tool call to this handler.
Data flow: It takes the handler itself as input, reads no outside state, and creates a plain tool name value containing request_permissions. The output is that tool name, ready for the tool system to compare and register.
Call relations: When the tool registry asks this handler what it is called, this function answers with the exact name the model will use. It relies on the shared tool-name helper to build the name in the standard format.
Call graph: calls 1 internal fn (plain).
RequestPermissionsHandler::spec33–35 ↗
fn spec(&self) -> ToolSpec
Purpose: This function describes how the request_permissions tool should look to the model. It supplies the tool definition and human-readable description used when exposing the tool.
Data flow: It takes the handler, asks for the standard request-permissions description, and passes that into the tool-spec builder. The output is a complete tool specification that says what arguments the tool accepts and what it is for.
Call relations: During tool registration or tool listing, the system calls this function so the model can be told how to use request_permissions. It delegates the actual wording and schema construction to the shell-spec helpers, keeping this file focused on the handler flow.
Call graph: calls 2 internal fn (create_request_permissions_tool, request_permissions_tool_description).
RequestPermissionsHandler::handle37–39 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
Purpose: This function is the entry point used when the model actually calls the tool. It starts the real asynchronous work and wraps it in the future type expected by the tool framework.
Data flow: It receives a full tool invocation, including the session, turn, call ID, payload, and cancellation token. It passes that invocation into handle_call, pins the resulting asynchronous task so it can be safely awaited, and returns that task to the caller.
Call relations: The tool framework calls this when it has matched an incoming tool call to RequestPermissionsHandler. This function does not inspect the request itself; it hands everything to handle_call, which performs the validation, permission request, and response creation.
Call graph: calls 1 internal fn (handle_call); 1 external calls (pin).
RequestPermissionsHandler::handle_call43–117 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
Purpose: This function performs the actual permission request flow. It validates the model’s arguments, finds the correct environment, normalizes the requested permissions, asks the session for approval, and turns the answer into tool output.
Data flow: It receives a tool invocation. First it extracts the JSON arguments from the payload and rejects unsupported payload types with a message for the model. It parses any environment ID, resolves the matching environment for the current turn, converts that environment’s current directory into a host-native absolute path, then parses the permission request relative to that path. It normalizes the permissions, rejects an empty permission list, and sends the request to the session along with the turn, call ID, environment selection, and cancellation token. If a response arrives, it serializes that response to JSON text and wraps it as successful tool output. If parsing fails, the environment is missing, the request is empty, or the operation is cancelled, it returns an error message for the model; if response serialization fails, it returns a fatal internal error.
Call relations: This is called by RequestPermissionsHandler::handle after the tool framework receives a request_permissions call. It uses shared parsing helpers to understand the JSON, shared environment resolution to choose where the request applies, and shared permission normalization so later code receives a clean permission profile. It then hands the actual approval request to the session, which is the part of the system that can communicate the request and return the decision.
Call graph: calls 6 internal fn (from_text, boxed_tool_output, parse_arguments, parse_arguments_with_base_path, resolve_tool_environment, normalize_additional_permissions); called by 1 (handle); 2 external calls (to_string, RespondToModel).
core/src/tools/handlers/request_user_input.rssource ↗
This file is the bridge between a model tool call named request_user_input and the real user-facing session. Its job is to make sure the assistant can ask the person a question only when that is safe and supported. Without this file, the model might call the tool but nothing would connect that request to the session, or worse, non-root helper agents could interrupt the user directly.
The central type is RequestUserInputHandler. It knows which collaboration modes allow user input requests. When the tool system asks what tool this handler represents, it gives back the tool name and a tool specification, which is the description and shape of the arguments the model is allowed to send.
When a call arrives, the handler first confirms the payload is really a function-style tool call. It then blocks calls from non-root agents, because only the main conversation thread is allowed to ask the human directly. Next it checks the current collaboration mode and returns a clear message if this tool is unavailable. If the request is allowed, it parses and normalizes the arguments, asks the session to request input from the user, waits for the answer, serializes that answer as JSON text, and wraps it as normal tool output for the model.
RequestUserInputHandler::tool_name24–26 ↗
fn tool_name(&self) -> ToolName
Purpose: This tells the tool registry the exact name of the tool this handler serves. The name is how a model tool call is matched to this handler.
Data flow: It reads no outside state from the handler. It takes the fixed request_user_input tool name constant, turns it into a plain ToolName, and returns that value to the tool system.
Call relations: When the core tool runtime registers or looks up tools, it calls this method to identify the handler. This method hands the name off by using plain, so the rest of the system can compare tool calls against the standard tool name format.
Call graph: calls 1 internal fn (plain).
RequestUserInputHandler::spec28–30 ↗
fn spec(&self) -> ToolSpec
Purpose: This builds the public description of the request_user_input tool that the model sees. It includes wording based on which collaboration modes are available, so the model gets an accurate idea of when the tool can be used.
Data flow: It reads available_modes from the handler. It uses those modes to create a human-readable tool description, then wraps that description into a full tool specification and returns it.
Call relations: The tool registry calls this when it needs to advertise available tools to the model. This method delegates the wording to request_user_input_tool_description and the final tool shape to create_request_user_input_tool.
Call graph: calls 2 internal fn (create_request_user_input_tool, request_user_input_tool_description).
RequestUserInputHandler::handle32–34 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
Purpose: This is the entry point used by the tool system when the model actually calls request_user_input. It turns the real work into an asynchronous task, meaning the system can wait for the user without blocking everything else.
Data flow: It receives a ToolInvocation, which contains the session, conversation turn, call id, and tool payload. It passes that invocation to handle_call, boxes the resulting future, and returns it to the tool runtime.
Call relations: The tool runtime calls this after matching a model tool call to this handler. This method does not process the request itself; it hands the invocation to handle_call, which performs all validation, user prompting, and output creation.
Call graph: calls 1 internal fn (handle_call); 1 external calls (pin).
RequestUserInputHandler::handle_call38–92 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
Purpose: This performs the full request_user_input workflow: validate the call, check permissions, ask the session to contact the user, and package the user's reply for the model. It is the safety gate and delivery path for this tool.
Data flow: It receives a ToolInvocation and pulls out the session, turn, call id, and payload. If the payload is not a function call, it returns an error message for the model. If the call comes from a non-root agent, it returns an error because only the main thread may ask the user. It then checks the session's current collaboration mode against the handler's available modes. If the mode does not allow user input, it returns the appropriate unavailable message. Otherwise it parses the JSON-like argument text into RequestUserInputArgs, normalizes those arguments, sends the request to the session, waits for the user's response, converts that response to JSON text, and returns it as boxed tool output. If the request is cancelled before a response arrives, it reports that back to the model; if response serialization fails, it returns a fatal error.
Call relations: This method is called by RequestUserInputHandler::handle whenever the model invokes the tool. It relies on parse_arguments and normalize_request_user_input_args to turn the model's raw arguments into safe structured data, uses request_user_input_unavailable_message to enforce mode rules, calls the session's user-input request path to get the actual human response, and finally uses FunctionToolOutput::from_text and boxed_tool_output to hand the result back in the standard tool-output form.
Call graph: calls 5 internal fn (from_text, boxed_tool_output, parse_arguments, normalize_request_user_input_args, request_user_input_unavailable_message); called by 1 (handle); 3 external calls (format!, to_string, RespondToModel).
MCP elicitation bridge
This bridge tracks MCP elicitation requests, applies approval policy, and routes requests and later responses between MCP callbacks and Codex protocol events.
codex-mcp/src/elicitation.rssource ↗
An MCP server can sometimes ask Codex to “elicit” something, meaning it wants Codex to ask the user for input, confirmation, or permission. This file is the gatekeeper for those requests. Without it, Codex would have no reliable way to apply approval rules, show the request to the user, and later connect the user’s answer back to the waiting MCP server.
The main piece is ElicitationRequestManager. It keeps a table of open requests, keyed by server name and request id, with a one-time reply channel for each request. Think of it like a coat-check desk: when a request is sent out for review, the manager keeps the ticket so the eventual answer can be returned to the right waiting server.
When a new elicitation arrives, the manager first checks a global auto-deny switch. If that is on, it declines immediately. Next it checks Codex’s approval policy and permission profile. Some very simple form confirmations can be safely auto-accepted, but URL-based requests and forms asking for specific fields are not auto-accepted. If policy forbids elicitation, the request is declined. If a custom reviewer exists, it gets a chance to answer. Otherwise the request is converted into a Codex protocol event and sent outward, then the manager waits for resolve to deliver the final response.
ElicitationRequestManager::new61–73 ↗
fn new(
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
reviewer: Option<ElicitationReviewerHandle>,
) -> Self
Purpose: Creates a new manager for MCP elicitation requests. It stores the current approval policy, permission profile, optional reviewer, and an empty list of requests waiting for answers.
Data flow: It receives an approval policy, a permission profile, and optionally a reviewer object. It wraps shared state in safe shared containers so different async tasks can read or update it, starts with no pending requests, and sets auto-deny to false. The result is a ready-to-use ElicitationRequestManager.
Call relations: This is used during setup paths that create Codex MCP support, including normal construction, uninitialized construction with a permission profile, and tests around auto-accept behavior. It prepares the shared state that later functions such as make_sender, resolve, auto_deny, and set_auto_deny rely on.
Call graph: called by 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 external calls (new, new, new, new).
ElicitationRequestManager::auto_deny75–80 ↗
fn auto_deny(&self) -> bool
Purpose: Reports whether the manager is currently set to automatically reject all elicitation requests. This is useful for checking the current safety mode.
Data flow: It reads the stored auto-deny flag through a lock, which is a guard that prevents two pieces of code from changing the same value at once. If the flag can be read, it returns that value; if the lock is unavailable because of an error, it safely returns false.
Call relations: This is called by elicitations_auto_deny when the wider system needs to know the current auto-deny setting. The value it returns also matches the flag that make_sender checks before deciding whether to decline new requests immediately.
Call graph: called by 1 (elicitations_auto_deny).
ElicitationRequestManager::set_auto_deny82–86 ↗
fn set_auto_deny(&self, auto_deny: bool)
Purpose: Turns automatic rejection of elicitation requests on or off. This lets the rest of Codex quickly stop MCP servers from asking the user for anything.
Data flow: It receives a boolean value: true means reject all future elicitation requests, false means allow normal policy checks again. It locks the stored flag and replaces the old value with the new one. It does not return a value.
Call relations: This is called by set_elicitations_auto_deny when the broader system changes the elicitation safety setting. Later, make_sender reads this flag for each incoming request, and auto_deny can report the current setting.
Call graph: called by 1 (set_elicitations_auto_deny).
ElicitationRequestManager::resolve88–101 ↗
async fn resolve(
&self,
server_name: String,
id: RequestId,
response: ElicitationResponse,
) -> Result<()>
Purpose: Completes a pending elicitation request by sending back the user’s or reviewer’s answer. This is the bridge from an external decision back to the MCP server that is waiting.
Data flow: It receives the server name, request id, and the chosen ElicitationResponse. It looks up the matching waiting request in the pending-request table, removes it so it cannot be answered twice, and sends the response through the saved one-time channel. It returns success if the response was delivered, or an error if no matching request exists or the receiver is gone.
Call relations: This is called by resolve_elicitation after Codex has received an answer for a previously surfaced request. It pairs with make_sender: make_sender stores the response channel when it emits a request event, and resolve later finds that channel to finish the conversation.
Call graph: called by 1 (resolve_elicitation).
ElicitationRequestManager::make_sender103–231 ↗
fn make_sender(
&self,
server_name: String,
tx_event: Sender<Event>,
) -> SendElicitation
Purpose: Builds the callback that the MCP client uses whenever a server asks for elicitation. This callback applies safety policy, optionally asks a reviewer, emits a Codex event if human input is needed, and waits for the final answer.
Data flow: It receives a server name and an event sender. It returns a boxed async function. When that returned function is called with a request id and elicitation details, it checks the auto-deny flag, approval policy, and permission profile. It may immediately return a decline, immediately return a safe empty accept, or ask an optional reviewer. If no answer is available yet, it converts the MCP request into Codex’s event format, stores a one-time response channel in the pending-request map, sends an ElicitationRequest event outward, and waits until resolve provides the response.
Call relations: This is the main request-time path for the file. It uses the shared state created by ElicitationRequestManager::new, depends on the policy helper elicitation_is_rejected_by_policy and the safety helper can_auto_accept_elicitation, and creates the waiting entry that ElicitationRequestManager::resolve later completes.
Call graph: 1 external calls (new).
elicitation_is_rejected_by_policy234–242 ↗
fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool
Purpose: Answers a simple policy question: does the current approval setting forbid MCP elicitation requests? It keeps the policy decision in one small, readable place.
Data flow: It receives an AskForApproval policy value. For the Never policy it returns true, meaning elicitation should be declined. For most approval modes it returns false, meaning elicitation may continue. For granular approval settings, it asks whether MCP elicitations are allowed and returns true only when they are not.
Call relations: This helper is used during the decision flow built by ElicitationRequestManager::make_sender. After auto-accept has been considered, this check decides whether the request must be declined before it can reach a reviewer or user-facing event.
can_auto_accept_elicitation246–256 ↗
fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) -> bool
Purpose: Decides whether an elicitation request is simple and safe enough to accept automatically. It is intentionally conservative.
Data flow: It receives the MCP elicitation details. If the request is a form and the form schema asks for no fields, it returns true, treating it like a plain confirmation. If the form asks for any fields, or if the request is URL-based, it returns false. It does not change anything.
Call relations: This helper supports ElicitationRequestManager::make_sender during the auto-approval path. Even when the broader permission policy allows automatic approval, this function prevents Codex from auto-accepting requests that ask for actual user data or involve opening or approving a URL.
Approval-specific MCP ingress
These adapters construct concrete MCP elicitation requests for execution and patch approvals, then translate client decisions back into Codex approval operations.
mcp-server/src/exec_approval.rssource ↗
Codex sometimes wants to run a command on the user’s machine, such as a build command or a file search. That can be risky, so this file creates a checkpoint: it asks the connected MCP client whether the command should be allowed. MCP means Model Context Protocol, a standard way for tools and clients to exchange structured messages.
The main flow is simple. When Codex needs command approval, this file builds a readable message like “Allow Codex to run ... in ...?” It also packages extra details, such as the command, working directory, thread id, and Codex event ids, so the client can show useful context and match the answer back to the right tool call. It sends that package as an elicitation/create request, which is MCP’s way of asking the client to get input from the user.
The reply is handled asynchronously, meaning the main Codex loop does not have to sit and wait. Think of it like leaving a note with a receptionist and continuing work until the answer comes back. When the answer arrives, the file reads the approval decision and submits it back into Codex as an ExecApproval operation. If the response is malformed, it chooses the safer option and denies the command.
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
Purpose: This starts the approval process for a command Codex wants to run. It prepares a user-facing permission request, sends it to the MCP client, and sets up a background listener for the answer.
Data flow: It receives the command, the working folder, Codex identifiers, an outgoing message sender, and the Codex thread to report back to. It turns the command into a readable shell-like string, builds a JSON request containing both the display message and Codex tracking details, and sends that request to the client. If the request cannot be converted to JSON, it sends an error back instead. Otherwise, it returns immediately after spawning a background task that will wait for the client’s response.
Call relations: This function is called by run_codex_tool_session_inner when a Codex tool session reaches a command that needs approval. It sends the MCP elicitation/create request through the outgoing message sender, then hands the waiting part to on_exec_approval_response so the main agent loop can keep running.
Call graph: calls 1 internal fn (on_exec_approval_response); called by 1 (run_codex_tool_session_inner); 8 external calls (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>,
)
Purpose: This waits for the client’s answer to the command approval prompt and tells Codex whether the command was approved or denied. It is the return path from the user-facing prompt back into Codex’s internal workflow.
Data flow: It receives an approval id, an event id, a one-time response channel, and the Codex thread. It waits for a JSON value from the client, tries to read it as an ExecApprovalResponse, and extracts the approval decision. If waiting fails, it logs the problem and stops. If the response cannot be understood, it logs the problem and treats the decision as denied for safety. It then submits an ExecApproval operation to Codex with the original ids and the final decision.
Call relations: This function is launched in the background by handle_exec_approval_request after the approval prompt has been sent. Its job is to complete that earlier request: once the MCP client replies, it passes the decision back into CodexThread so Codex can either run the command or avoid it.
Call graph: called by 1 (handle_exec_approval_request); 1 external calls (error!).
mcp-server/src/patch_approval.rssource ↗
Codex may prepare changes to files, but it should not always apply them without a human or client saying yes. This file is the bridge for that moment. It packages the proposed file changes, the reason for the change, and several IDs that let the system connect the answer back to the original tool call. Then it sends an MCP request named elicitation/create, which means “ask the client to provide a response.” Think of it like handing a permission slip to the user interface: “Codex wants to edit these files; should it proceed?”
The file defines the shape of that permission slip with PatchApprovalElicitRequestParams, and the expected answer with PatchApprovalResponse. The answer contains a ReviewDecision, such as approval or denial.
A key detail is that the file does not wait in place for the user’s answer. After sending the request, it starts a separate asynchronous task. An asynchronous task is work that can continue in the background while the main loop keeps running. When the answer arrives, the task converts it into a Codex operation, Op::PatchApproval, and submits it back to the running Codex thread. If anything goes wrong, such as a failed request or unreadable response, the code chooses the safe default: deny the patch.
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
Purpose: This function starts the approval process when Codex wants to apply code changes. It builds a clear request for the client, sends it out, and sets up background work to process the eventual answer.
Data flow: It receives the patch approval details: the approval ID, optional reason, optional project root to grant access to, the proposed file changes, the current Codex thread, the outgoing message sender, and IDs that identify the request and tool call. It turns these into PatchApprovalElicitRequestParams, including a user-facing message asking whether Codex may apply the proposed changes. It serializes those parameters into JSON, because MCP messages are sent as structured JSON data. If serialization fails, it sends an error response back through the outgoing message sender and stops. If serialization succeeds, it sends an elicitation/create request and receives a one-time channel that will later carry the client's answer. It then starts a background task that will wait for that answer.
Call relations: This function is called by run_codex_tool_session_inner when a Codex tool session reaches a point where patch approval is needed. It does the outward-facing part of the flow: asking the client. It then hands the waiting-and-submitting part to on_patch_approval_response so the main Codex agent loop is not blocked while waiting for the user or client to respond.
Call graph: calls 1 internal fn (on_patch_approval_response); called by 1 (run_codex_tool_session_inner); 8 external calls (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>,
)
Purpose: This function finishes the approval process after the client answers the permission request. It converts the client response into a Codex patch-approval operation, using denial as the safe fallback if the response fails or cannot be read.
Data flow: It receives the approval ID, a one-time receiver for the JSON response, and the Codex thread to report back to. First it waits for the response. If the request failed before a response arrived, it logs the problem and submits a denied PatchApproval operation to Codex. If a response arrives, it tries to read it as a PatchApprovalResponse. If that JSON cannot be understood, it logs the problem and treats the decision as denied. Finally, it submits Op::PatchApproval to the Codex thread with the approval ID and the chosen decision.
Call relations: This function is launched by handle_patch_approval_request in a background asynchronous task. It represents the return trip of the approval flow: after the client has been asked, this function waits for the answer and hands the final decision back to Codex. That lets Codex continue its work knowing whether the patch was approved or denied.
Call graph: called by 1 (handle_patch_approval_request); 1 external calls (error!).