# Approvals and server requests

> Inline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/message_processor.rs`
- `src/attestation.rs`
- `src/request_processors/turn_processor.rs`
- `src/request_processors/mcp_processor.rs`
- `src/bespoke_event_handling.rs`

---

---
title: "Approvals and server requests"
description: "Inline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling."
---

During an active turn, `codex app-server` issues JSON-RPC **requests** from server to client (not notifications). The client must answer with a JSON-RPC `result` (or `error`) so the agent can continue. The server maps core agent events into v2 wire methods, tracks pending request ids per connection, and resumes the turn when `process_response` delivers the matching result. After each resolution—or when a turn boundary clears outstanding prompts—the server emits `serverRequest/resolved` to thread subscribers so UIs can drop inline prompts safely.

## Server request model

| Wire method | Purpose | Typical trigger |
| --- | --- | --- |
| `item/commandExecution/requestApproval` | Approve or deny a sandboxed shell command | `ExecApprovalRequest` during a turn |
| `item/fileChange/requestApproval` | Approve or deny a patch / file write | `ApplyPatchApprovalRequest` during a turn |
| `item/tool/requestUserInput` | Collect 1–3 structured answers for `request_user_input` (experimental) | `RequestUserInput` during a turn |
| `mcpServer/elicitation/request` | MCP `elicitation/create` (form or URL) | `ElicitationRequest` during or outside a turn |
| `item/permissions/requestApproval` | Grant a subset of requested permission profile | `request_permissions` tool |
| `attestation/generate` | Produce opaque attestation token for upstream `x-oai-attestation` | Just-in-time before ChatGPT Codex HTTP calls |
| `item/tool/call` | Run a registered dynamic tool on the client (experimental) | `DynamicToolCallRequest` |
| `account/chatgptAuthTokens/refresh` | Refresh ChatGPT auth tokens | Account layer (not turn-inline) |

v1-only `ApplyPatchApproval` and `ExecCommandApproval` remain in the schema for legacy turn APIs; new integrations should use the v2 `item/*` methods above.

<Note>
Server requests are distinct from high-volume **notifications** (`item/started`, deltas, `turn/completed`). Clients must read the transport continuously and handle interleaved server requests while a turn is in progress.
</Note>

### Routing and correlation

- Turn-inline approvals use `ThreadScopedOutgoingMessageSender`, which sends only to connections subscribed to that `threadId`.
- Attestation uses `send_request_to_connections` with the first initialized connection on that thread where `initialize.params.capabilities.requestAttestation` is `true` (lowest `connection_id` wins).
- Each server request carries a monotonic integer JSON-RPC `id` (server-assigned). Clients echo that `id` in the response.
- Params always include `threadId` and usually `turnId` + `itemId` for turn-bound prompts.

## `serverRequest/resolved` lifecycle

After the client responds—or when the server aborts a still-pending prompt—the app-server enqueues `ResolveServerRequest` on the thread listener so resolution notifications stay ordered with other thread events. Subscribers then receive:

```json
{
  "method": "serverRequest/resolved",
  "params": {
    "threadId": "thr_123",
    "requestId": 42
  }
}
```

```mermaid
sequenceDiagram
    participant Core as codex-core events
    participant Bespoke as bespoke_event_handling
    participant Out as OutgoingMessageSender
    participant Client as JSON-RPC client
    participant Listener as thread listener

    Core->>Bespoke: ExecApprovalRequest / ElicitationRequest / ...
    Bespoke->>Out: send_request(ServerRequestPayload)
    Out->>Client: item/.../requestApproval or mcpServer/elicitation/request
    alt Client answers in time
        Client->>Out: JSON-RPC result (matching id)
        Out->>Bespoke: oneshot callback
        Bespoke->>Listener: resolve_server_request_on_thread_listener
        Listener->>Client: serverRequest/resolved
        Bespoke->>Core: Op::PatchApproval / ExecApproval / ResolveElicitation / ...
    else Turn start / complete / interrupt / abort
        Bespoke->>Out: abort_pending_server_requests
        Out-->>Client: JSON-RPC error (reason turnTransition)
        Bespoke->>Listener: serverRequest/resolved (cleanup)
    end
```

### When resolution fires without a user answer

`ThreadScopedOutgoingMessageSender::abort_pending_server_requests` runs at:

- `TurnStarted` (defensive cleanup before a new turn)
- `TurnComplete`
- `TurnAborted` (including `turn/interrupt`)

Canceled callbacks receive an internal JSON-RPC error with `data.reason = "turnTransition"`. Handlers treat that as a no-op for core submission (no duplicate `decline`).

<Warning>
Integration tests assert `serverRequest/resolved` arrives **before** `turn/completed` when the client answered normally, and still arrives when `turn/interrupt` cancels an outstanding command approval.
</Warning>

Pending server requests for a thread can be replayed to a connection after resume via `replay_requests_to_connection_for_thread`.

## Command execution approvals

Approval policy comes from config and optional `thread/start` / `turn/start` overrides (`approvalPolicy`, `approvalsReviewer`). When a command needs review, the server emits the standard item lifecycle plus a server request.

<Steps>
<Step title="Render the pending item">
Receive `item/started` with `item.type = "commandExecution"` and `status = "inProgress"`. For top-level shell approvals (no `approvalId`), the server may emit this item immediately before the approval request.
</Step>
<Step title="Show the approval prompt">
Handle `item/commandExecution/requestApproval`. Key params: `threadId`, `turnId`, `itemId`, `startedAtMs`, optional `approvalId` (subcommand callbacks), `reason`, and either command fields (`command`, `cwd`, `commandActions`) or `networkApprovalContext` for network-only prompts.
</Step>
<Step title="Respond with a decision">
Send a JSON-RPC response whose `result` is `{ "decision": ... }`.
</Step>
<Step title="Observe completion">
Wait for `serverRequest/resolved`, then `item/completed` with final `status` (`completed`, `failed`, or `declined`).
</Step>
</Steps>

### Decision values

| `decision` | Effect on agent |
| --- | --- |
| `"accept"` | Run the command for this prompt |
| `"acceptForSession"` | Approve and cache for the session |
| `{ "acceptWithExecpolicyAmendment": { "execpolicyAmendment": ... } }` | Approve and persist execpolicy hint |
| `{ "applyNetworkPolicyAmendment": { "networkPolicyAmendment": { "host", "action" } } }` | Approve with network rule |
| `"decline"` | Skip command; turn continues |
| `"cancel"` | Deny and interrupt the turn |

When `initialize.params.capabilities.experimentalApi` is `true`, the request may include `additionalPermissions`, `proposedExecpolicyAmendment`, `proposedNetworkPolicyAmendments`, and `availableDecisions`. Clients without experimental API receive stripped `additionalPermissions` on outbound requests.

<RequestExample>
```json
{
  "method": "item/commandExecution/requestApproval",
  "id": 7,
  "params": {
    "threadId": "thr_abc",
    "turnId": "turn_abc",
    "itemId": "call_shell_1",
    "startedAtMs": 1710000000000,
    "command": "npm test",
    "cwd": "/Users/me/project",
    "commandActions": [],
    "reason": "Command requires approval"
  }
}
```
</RequestExample>

<ResponseExample>
```json
{
  "id": 7,
  "result": {
    "decision": "accept"
  }
}
```
</ResponseExample>

## File change approvals

Patch approvals follow the same resolved-notification pattern with a smaller decision set.

<Steps>
<Step title="Show proposed edits">
`item/started` with `item.type = "fileChange"`, `changes[]`, `status = "inProgress"`.
</Step>
<Step title="Prompt">
`item/fileChange/requestApproval` with `threadId`, `turnId`, `itemId`, `startedAtMs`, optional `reason`, optional unstable `grantRoot` for session-scoped write access.
</Step>
<Step title="Respond">
`{ "decision": "accept" | "acceptForSession" | "decline" | "cancel" }`.
</Step>
<Step title="Finalize">
`serverRequest/resolved`, then `item/completed` with updated `status`.
</Step>
</Steps>

| `decision` | Maps to core review |
| --- | --- |
| `accept` | Approved |
| `acceptForSession` | Approved for session |
| `decline` | Denied (turn continues) |
| `cancel` | Abort (turn interrupted) |

Streaming patch previews may arrive as `item/fileChange/patchUpdated` when `features.apply_patch_streaming_events` is enabled.

## `item/tool/requestUserInput`

The built-in `request_user_input` tool blocks the turn until the client answers. The wire method is `item/tool/requestUserInput` (experimental). Params include `questions[]` with `id`, `header`, `question`, optional `options`, and flags `isOther` / `isSecret`.

<ResponseField name="answers" type="object">
Map from question `id` to `{ "answers": ["selected label", ...] }`. Omitted or malformed responses are treated as empty answers; turn-transition aborts skip core submission.
</ResponseField>

<RequestExample>
```json
{
  "method": "item/tool/requestUserInput",
  "id": 9,
  "params": {
    "threadId": "thr_abc",
    "turnId": "turn_abc",
    "itemId": "call1",
    "questions": [
      {
        "id": "confirm_path",
        "header": "Confirm",
        "question": "Proceed?",
        "options": [{ "label": "yes", "description": "Continue" }]
      }
    ]
  }
}
```
</RequestExample>

<ResponseExample>
```json
{
  "id": 9,
  "result": {
    "answers": {
      "confirm_path": { "answers": ["yes"] }
    }
  }
}
```
</ResponseExample>

## MCP server elicitations

MCP servers call `elicitation/create`; app-server forwards them as `mcpServer/elicitation/request`. The payload is tagged by `mode`:

- **form** — `message`, `requestedSchema` (JSON Schema object), optional `_meta`
- **url** — `message`, `url`, `elicitationId`, optional `_meta`

`turnId` is nullable: present when correlated with an active turn, otherwise `null` for out-of-band elicitations. Clients can call `thread/incrementElicitation` / `thread/decrementElicitation` to pause turn progress while showing external UI.

<ResponseExample>
```json
{
  "id": 12,
  "result": {
    "action": "accept",
    "content": { "confirm": true },
    "_meta": null
  }
}
```
</ResponseExample>

| `action` | Server behavior |
| --- | --- |
| `accept` | Forward content to `Op::ResolveElicitation` |
| `decline` | Deny; turn may continue |
| `cancel` | Abort elicitation |

<Info>
**Compatibility:** connections identifying as Xcode `26.4` auto-deny MCP elicitations at turn start because that client predates visible elicitation requests.
</Info>

For MCP tool approval elicitations, form `_meta` may include `codex_approval_kind: "mcp_tool_call"` and `persist` hints (`session`, `always`, or both) so clients can offer durable approval choices.

## Attestation (`attestation/generate`)

Desktop hosts that can produce upstream attestation should set `capabilities.requestAttestation: true` during `initialize`. Before ChatGPT Codex requests that need `x-oai-attestation`, `AppServerAttestationProvider` issues `attestation/generate` to the first attestation-capable subscriber for that thread (100 ms timeout).

<ParamField body="token" type="string" required>
Opaque client-owned value, typically `v1.<payload>`.
</ParamField>

App-server wraps the client token into header JSON `{ "v": 1, "s": 0, "t": "<token>" }`. Failure statuses omit `t`:

| `s` | Meaning |
| --- | --- |
| `0` | Success |
| `1` | Timeout (request canceled server-side) |
| `2` | Request failed (client JSON-RPC error) |
| `3` | Request canceled |
| `4` | Malformed client response |

If no connection opted in, upstream requests omit `x-oai-attestation` entirely.

## Permission requests (related)

The `request_permissions` tool uses `item/permissions/requestApproval` with a `permissions` profile (network + filesystem paths). Clients return the **granted subset** in `result.permissions`, optionally `scope: "session"` for sticky grants within the session. Omitted permissions are denied; extra permissions in the response are ignored. Granular approval policy with `request_permissions: false` auto-denies standalone permission tool calls without a prompt.

## Client implementation checklist

- Read JSON-RPC messages concurrently: notifications, client RPC responses, and **server requests** share one transport.
- Match UI state with `threadId`, `turnId`, and `itemId`; stash `requestId` until `serverRequest/resolved`.
- Respond to every server request promptly; the agent thread blocks on the oneshot channel.
- Treat `turnTransition` errors as cleanup, not user decline.
- For attestation, implement `attestation/generate` only when the host can supply real tokens.
- Regenerate TypeScript/JSON Schema from the running binary after protocol changes (`codex app-server generate-ts` / `generate-json-schema`).

## Related pages

<CardGroup>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Handle server requests mid-turn, request ids, and `initialized` ordering.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Item lifecycle, deltas, and `turn/completed` after approvals finish.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize capabilities, thread subscribe, and server vs client RPC roles.
</Card>
<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
MCP OAuth, tool calls, and elicitation-related server configuration.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog including experimental markers.
</Card>
</CardGroup>
