Start with a normal action: the model wants to run tests. In the UI this looks like a shell tool call. In the source, several questions appear at once. Why was shell visible in this turn? How is the model's call parsed? Who decides whether it can run in parallel? Where do approval and sandbox checks happen? If the tool fails, what does the model see next?

That is the reading route for Codex tools. Do not begin with a tool inventory. Follow the contract: a tool starts as a model-visible spec, returns as a model call, becomes a runtime invocation, and leaves output plus events behind.

Keep four objects in view: ToolSpec is the model-visible menu; ToolRouter is the turn-scoped router; ToolInvocation is the runtime execution request; ResponseInputItem is the tool result shape returned to the next model call.

Evidence boundary. This article describes only the tool construction, routing, execution, and recording logic visible in the public openai/codex source. Terms such as tool menu, router, execution request, and evidence are reading aids for source objects including ToolSpec, ToolRouter, ToolInvocation, EventMsg, and ResponseInputItem. It does not infer private model service behavior or private deployment-specific tools.

This part follows six questions:

  1. Where does the tool menu for each model request come from?
  2. Why is the model-visible tool list not the same as the runtime registry?
  3. How does a model function call become a ToolInvocation?
  4. Where do parallelism, cancellation, hooks, and lifecycle notifications attach?
  5. Why do approval and sandboxing belong to the runtime path, not to the model output?
  6. How does a tool result flow back to the model, clients, and history?

1. Split "Tool" Into Layers

The word tool is overloaded. It can mean a schema in the model request, a shell executor, apply_patch, MCP, plugins, dynamic tools, multi-agent collaboration tools, or tools discoverable later through tool search. The source becomes readable only after separating model visibility from runtime dispatch.

Layer Source Object Responsibility Common Misread
Model menu ToolSpec Describe how the model may request tools in this turn. Treating schema visibility as execution authority.
Runtime index ToolRegistry Map tool names to handlers and capability metadata. Assuming hidden-from-model means absent from runtime.
Turn router ToolRouter Parse model outputs into tool calls and dispatch them. Missing direct, deferred, and dispatch-only exposure.
Execution request ToolInvocation Carry session, turn, call id, cancellation, diff tracking, and payload. Thinking handlers receive only model arguments.
Returned evidence ToolOutput / ResponseInputItem Normalize results for the next model call and history. Looking only at terminal output.

These layers make Codex tools a runtime contract. The model proposes an action. The runtime decides whether the call is known, whether it can run in parallel, which hooks apply, whether approval or sandboxing is required, and what output shape returns to the model.

2. The Tool Menu Is Built Per Turn

The menu is constructed before each sampling request. run_sampling_request calls built_tools(sess, turn_context, cancellation_token) to obtain the turn's ToolRouter, then creates ToolCallRuntime. build_prompt sends router.model_visible_specs() as Prompt.tools and passes the model's parallel-tool-call capability into the prompt.

That menu depends on the current TurnContext. Shell availability, model tool shape, MCP servers, enabled plugins and apps, discoverable connectors, extension executors, and dynamic tools can all change the router for this specific turn.

built_tools makes those sources visible: it loads MCP tools, plugins, connectors, discoverable tools, MCP exposure, extension executors, and dynamic tools, then passes them to ToolRouter::from_turn_context.

The key property is turn-scoped. In the same Codex process, different turns can have different tool surfaces.

3. Model Visibility and Runtime Registration Are Separate

ToolRouter stores two things: registry and model_visible_specs. The first is for dispatch. The second is for the model request. They are related, but not identical.

Both are built in build_tool_specs_and_registry. Codex creates PlannedTools, adds shell, MCP resource tools, core utility tools, collaboration tools, MCP runtime tools, extension tools, dynamic tools, and hosted model specs, then appends tool search and prepends code-mode executors.

The split happens in build_model_visible_specs_and_registry. Only direct exposure that is not hidden by code-mode-only becomes a model-visible spec. The registry is built from all runtimes. A tool can therefore be hidden from the model while still remaining dispatchable.

Shell tools show why this matters. add_shell_tools can make unified exec model-visible while keeping the legacy shell handler registered as dispatch-only. The prompt can move forward without breaking older runtime calls.

4. A Model Call Becomes a Runtime Invocation

When the model stream contains a tool call, Codex does not execute raw JSON. ToolRouter::build_tool_call parses response items into ToolCall: function calls become named tool payloads, client-side tool search calls become tool-search payloads, and custom tool calls keep their custom input.

Then dispatch_tool_call_with_code_mode_result_inner wraps the call in ToolInvocation. The invocation carries session, turn, cancellation token, diff tracker, call id, tool name, source, and payload. That complete request is passed to the registry.

Shape sketch:
  model function_call
      ↓
  ToolRouter::build_tool_call(...)
      ↓
  ToolCall { tool_name, call_id, payload }
      ↓
  ToolInvocation { session, turn, cancellation_token, tracker, source, payload }
      ↓
  ToolRegistry dispatch

5. Registry Owns Hooks, Lifecycle, and Normalized Output

ToolRegistry is the map from tool name to runtime. from_tools rejects duplicate names. During dispatch, dispatch_any_with_terminal_outcome increments the active turn's tool count and looks up the handler. Unknown tools become model-visible failures instead of silent drops.

Registry also wraps the handler with cross-cutting runtime behavior. It notifies lifecycle start, runs pre-tool-use hooks that can block or rewrite input, records telemetry, runs the handler, runs post-tool-use hooks, records additional context, optionally replaces model-visible output, and finally notifies lifecycle finish. That flow is visible in the second half of dispatch.

6. Parallelism and Cancellation Are Runtime Decisions

A prompt can say the model supports parallel tool calls, but Codex still gates actual concurrency per tool. ToolCallRuntime holds a parallel_execution read/write lock and asks the router whether each tool supports parallel execution.

In handle_tool_call_with_source, parallel-safe tools take the read lock; non-parallel tools take the write lock. Cancellation is handled in the same runtime layer: Codex either waits for runtime teardown or aborts the task, then creates an aborted response and notifies lifecycle contributors.

Parallel tool calls mean the model may express parallel intent. Actual concurrency is narrowed by each tool runtime's capability and ToolCallRuntime's lock policy.

7. Side Effects Enter Approval and Sandbox Gates

Tools that affect the external environment enter a narrower runtime gate. The header of orchestrator.rs describes it as the central place for approvals, sandbox selection, and retry semantics. ToolOrchestrator::run checks approval requirements, selects the first sandbox attempt, and runs the tool.

If the sandboxed attempt is denied, the retry path uses denial details, network policy, approval policy, and tool escalation rules to decide whether to request approval again and which retry sandbox to use. The model's request and the real side effect are therefore separated by runtime authority checks.

8. Results Return to the Model, Events, and History

Tool results first need a model-visible shape. FunctionToolOutput, ApplyPatchToolOutput, and ExecCommandToolOutput implement to_response_item, producing ResponseInputItem. Shell output includes wall time, exit code or session id, token counts, and truncated output text.

Client progress comes through the event layer. Tool events include emit_exec_command_begin and ToolEmitter variants for shell, apply patch, and unified exec. Extension lifecycle contributors receive start and finish through notify_tool_start / notify_tool_finish.

History is closed by the turn loop. drain_in_flight waits for in-flight tool futures, converts ResponseInputItem into ResponseItem, and records conversation items. A tool output is therefore not just user-visible output. It becomes model input and recoverable history.

9. Rules To Carry Forward

Observation Source Question Handle
The model can call a tool. Is it direct, deferred, or dispatch-only? Separate model_visible_specs from ToolRegistry.
The model emits a function call. Which ToolPayload does it parse into? ToolRouter::build_tool_call.
A tool starts running. Which turn-level data does the handler receive? ToolInvocation.
Tools appear to run in parallel. Does each runtime support parallel calls? ToolCallRuntime read/write locking.
A command is approved, sandboxed, denied, or retried. Did it enter the orchestrator path? ToolOrchestrator::run.
A tool returns output. What shape is seen by the user, the model, and history? EventMsg, ToolOutput, ResponseInputItem.

The tool path is now closed: turn context builds the tool surface; the prompt exposes model-visible specs; the model returns a call; the router creates an invocation; registry dispatches it with hooks and lifecycle; side-effecting handlers pass through approval and sandbox gates; outputs return to the model, event stream, and history.

The next part can now focus on authority: where approval policy, permission hooks, sandbox policy, and exec policy each intercept a tool that wants to touch processes, files, or the network.

Sources