DOCS

node_repl on gateway rows

node_repl (browser, Chrome and computer use) on gateway rows: native evidence, how auto review runs in gateway executors, rules, model switching and the check script.

About 13 min read · 6 sections

On this page

node_repl runs the browser, Chrome and computer-use tools. Gateway chats get the same node_repl capability and the same safety policy as native rows, and never a weaker one. This guide owns the details, because app-server.md was already over 500 lines on main; the Codex 2 rows section of that guide summarizes them. The code is in src/auto-review.ts, src/codex-native-rows.ts, src/desktop-catalog.ts and src/server.ts.

Evidence

The SDK reads the policy from the ModelInfo fields node_repl_disabled, node_repl_auto_review_required, guardian and auto_review_model_override. The runtime source is the client's own ~/.codex/models_cache.json (client 0.162.0, fetched 2026-10-15), which the adapter loads as the native catalog. It was read-only observed, together with the bundled plugins, ~/.codex/config.toml and the Codex source (ModelInfo in codex-rs/protocol, pinned revision 740e5af):

How review runs in gateway executors

An action that needs review goes to a reviewer model. The SDK chooses it as follows (core/src/guardian/reviewer_config.rs, ext/guardian-reviewer/src/model.rs):

  1. The default reviewer is approval_review_preferred_model() of the turn provider: gpt-5.6-luna only when the provider has cached API-key auth, otherwise codex-auto-review.
  2. A gateway executor's provider authenticates with auth.command, so the SDK builds a fresh external-bearer AuthManager for it whose cache starts empty. Nothing reads auth through the turn provider (the model client and the extensions build their own providers), so the cache stays empty and the default is codex-auto-review. gpt-5.6-luna is never selected from an executor.
  3. The row's own auto_review_model_override, when set, replaces the default.
  4. The chosen reviewer is used only if its row is in the executor's model list. That list is filtered for API-key auth, so the row needs supported_in_api: true. Otherwise the SDK falls back to the parent model, with the parent's own messages. That is not the native review, so the catalog must never allow it: a reviewer row that is missing or not supported_in_api: true keeps node_repl off.
  5. The review text is the SDK's own: the instructions and node_repl policy come from the reviewer row's model_messages.auto_review, plus the SDK's output contract. The gateway adds, removes and rewrites no review text.

The reviewer is a normal thread on the same gateway provider. It never executes anything for the client: its tools (exec_command, write_stdin, view_image, exec and wait) run read-only inside the SDK's sandbox, so the reviewer may inspect state with read-only commands. Full Access approves the empty-schema node_repl elicitations without any review, as natively. So does the user approvals reviewer, for ordinary node_repl js calls that are not sensitive and need no user input (core/src/session/mcp.rs, user_cua_execution).

A live check of the pinned gateway SDK, with a fake gateway and no paid inference, saw the reviewer request as POST /v1/responses with model: "codex-auto-review", reasoning.effort: "low", text.format.type: "json_schema", store: false, stream: true, include, prompt_cache_key, client_metadata and tool_choice; input items additional_tools, two developer messages and user messages; and no service_tier. With the reviewer row removed from the catalog, the same action sent the reviewer request under the parent model instead.

The reviewer route

The gateway serves exactly one reviewer slug, codex-auto-review (src/auto-review.ts), through the Codex subscription provider, and never gpt-5.6-luna.

The desktop adapter

The adapter turns node_repl on for a row that needs review only when all of these hold: the last successful live read of the catalog advertised the row's reviewer slug (its auto_review_model_override, or codex-auto-review when none), and the native catalog carries that reviewer row with supported_in_api: true. A restored cache, a start without a read, a source change or an older gateway means no advertisement, so those rows have node_repl off until a read succeeds. A failed refresh keeps the last snapshot. The reviewers are part of the catalog revision, so a change starts a new executor generation.

Scope, data and decision

The route serves every Auto-review approval in a gateway executor, not only node_repl. The SDK resolves the reviewer the same way for node_repl elicitations, shell and exec escalations, apply_patch, network access, MCP tool calls and permission requests (core/src/tools/approvals.rs, orchestrator.rs, network_approval.rs, runtimes/unified_exec.rs, core/src/session/mcp.rs). The gateway cannot tell these apart without reading request bodies, which it never does. Only chats whose approvals reviewer is Auto-review send reviews.

On 0.3.9 each of these reviews got 404 from the gateway, and the SDK counts a failed review as a denial, so every action that needed Auto-review was declined in gateway chats. Now those actions and their transcript context go to OpenAI's reviewer through the Codex subscription; for Claude, Copilot and Qwen chats they leave that chat's own provider. Edi decided on 2026-10-15 that the route is on: gateway engines send the review step to OpenAI's reviewer through the Codex subscription, and it fails closed only when that route is unavailable. The review text is never logged: telemetry records fixed fields only, and no token, prompt, request body, tool argument or environment value is logged.

Telemetry

Every Auto-review request (any kind, above) carries autoReview: true, with model set to that slug, on request_completed, request_empty and request_failed, including the 404. gateway diagnostics adds an autoReview bucket: completed, empty and failed:<code>. Other model names are never recorded for a request without a route.

Rules

Fails closed

Any one of these turns node_repl off for the rows that need review, and for fallback rows:

The adapter does not know a chat's approvals reviewer, so review-required rows stay off without an advertised reviewer even when that chat would not use the reviewer (Full Access or the user approvals reviewer).

At run time any reviewer failure (401, 404, 5xx, timeout) makes the SDK decline the action. In a mixed deployment an inference host older than this release sends no auto_review, so those rows stay off until the host is updated.

Switching models during a running turn

This is the SDK's behaviour, not the gateway's. check_legacy_model_safety (core/src/session/step_activation.rs) rejects turn/settings/update with a model when the destination differs from the running turn in node_repl_disabled, computer_use_review_required, guardian, auto_review_model_override, the cyber specialty, the guardian_v2 settings (with the feature or a guardian), or the parent-fallback review texts. App-server maps the rejection to JSON-RPC -32600 with the SDK's reason. The router forwards it unchanged and records the new model only for an applied result, so the turn keeps its model.

On the live configuration (the guardianv2 feature off, its default) the rows fall in three groups, and a mid-turn switch works only inside one. With guardianv2 on, the guardian_v2 settings are compared too, so gpt-6-luna, which carries them, no longer switches with the 5.6 rows:

Group Rows
Review required gpt-6.1-sol, gpt-6-astra, gpt-6-sol, every Claude, Copilot and Qwen row
Review not required gpt-6-luna, gpt-reserve, the 5.6 rows
Cyber (model_specialty) gpt-daybreak-blue-latest

Claude to gpt-6.1-sol still switches mid-turn; Claude to gpt-6-luna is rejected, like native gpt-6.1-sol to gpt-6-luna, and a switch to or from gpt-daybreak-blue-latest is rejected like natively (the destination changes the admitted Guardian rejection policy). The groups are the same with or without the reviewer. thread/settings/update changes the next turn and is never checked.

ChatGPT desktop 26.1007 changes the model only through thread/settings/update and sends turn/settings/update only with approvalsReviewer. The picker therefore never hits the rejection: it shows the new model, the running turn finishes on the old one, and the next turn uses the new one. A CLI or script gets the error. With midTurnModelSwitch on, the adapter offers the picker change to the running turn and keeps the next-turn behaviour when this check refuses it (mid-turn model changes).

What the desktop shows

Check script

node scripts/check-node-repl.mjs <copy> reads a copy of a combined catalog: <stateDirectory>/desktop-catalog-<revision>.json, which one executor generation loads as model_catalog_json, or <stateDirectory>/desktop-models.json, which holds the latest one once the adapter has restored or refreshed. It prints both node_repl fields for every row as JSON, and guardian when one is set. It then counts the rows that keep node_repl off and the rows that enable it without review, as the SDK reads them: a missing node_repl_disabled means enabled, and a set guardian decides review. A guardian that is not an object, or a computer_use mode that is not a string, counts as without review. The last line reports the reviewer row codex-auto-review: missing, or its supported_in_api and whether its node_repl_policy is present. The script is advisory and exits 0, also for unreadable or malformed input. Pass it a copy, never the installed app's live state.