From d2ec419a02e7904aa59e7ada793dd3694914c79c Mon Sep 17 00:00:00 2001 From: Robin Bially <7304732+RobinBially@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:12:03 +0200 Subject: [PATCH 1/5] feat(sidecar): make Off selectable in the web-search card and switch Codex's web_search off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The web-search sidecar could only be turned off by editing `config.json`. That is not enough when an MCP search server should be the only search path: Codex keeps declaring its native hosted `web_search` tool until its own root `web_search` mode says otherwise, and the tool a client advertises is the tool the model reaches for — so the model kept calling the native tool instead of the MCP one. Off is now the first row of the Dashboard's web-search model picker (i18n `dash.webSearchOff`, all ten locales) and `ocx agent sidecar web --enabled off` accepts the same switch. Both go through the existing `PUT /api/sidecar-settings`, which persists `webSearchSidecar.enabled` and — only when the switch actually MOVES — re-runs the Codex config injection, so the sidecar state and Codex's client-side key follow each other immediately instead of at the next `ocx sync`. The response carries the Codex-side write as `codexWebSearch` (`applied`/`reason`/`retryable`), the same report the Desktop switches use. Ownership follows the routing keys: while the sidecar is off the injection owns root `web_search` and writes `web_search = "disabled"` — the only mode that removes the native tool. A user-owned root line is replaced in that state because two root keys of the same name are invalid TOML; the journal snapshot returns it on `ocx restore`. Switching the sidecar back on removes only the marker-owned pair, so a re-enabled sidecar cannot be left with nothing to intercept. --- docs-site/src/content/docs/guides/sidecars.md | 18 +++ .../src/content/docs/reference/cli/agents.md | 8 + .../docs/reference/configuration/server.md | 2 +- gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/vi.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/dashboard-overview-sections.tsx | 19 ++- gui/src/pages/dashboard-shared.ts | 11 +- scripts/test-layout/layout.json | 2 + src/cli/agent.ts | 18 ++- src/cli/runtime-api.ts | 17 +++ src/cli/system-command.ts | 10 +- src/codex/desktop-switches.ts | 10 +- src/codex/inject/config-toml.ts | 67 +++++++++ src/codex/inject/plan.ts | 5 + src/codex/inject/remove.ts | 2 + src/server/management/config-routes.ts | 29 +++- structure/config.md | 8 + tests/cli/cli-headless-parity.test.ts | 55 +++++++ .../codex-web-search-switch.test.ts | 138 ++++++++++++++++++ tests/fixtures/test-layout-expected.json | 2 + .../sidecar-settings-web-search-off.test.ts | 138 ++++++++++++++++++ 29 files changed, 548 insertions(+), 21 deletions(-) create mode 100644 tests/codex-integration/codex-web-search-switch.test.ts create mode 100644 tests/vision/sidecar-settings-web-search-off.test.ts diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md index 1f80f4fbc95..e1a83bb650d 100644 --- a/docs-site/src/content/docs/guides/sidecars.md +++ b/docs-site/src/content/docs/guides/sidecars.md @@ -207,6 +207,24 @@ timeout, and limit. omitted keys unchanged. `timeoutMs` uses the runtime integer bounds (1–2147483647 ms). +The web-search sidecar card carries the same control shape: the model picker's first row is +**Off**. Off does two things, and the second one is the reason the row exists. OpenCodex stops +intercepting `web_search`, and the Codex integration writes Codex's own +`web_search = "disabled"` mode into `~/.codex/config.toml` — because Codex keeps declaring its +native hosted `web_search` tool until its own mode says otherwise, and the tool a client +advertises is the one the model reaches for. An operator who wants an MCP search server to be +the only search path needs both halves; otherwise the model keeps calling the native tool. + +`web_search` is Codex's key with its own value space (`disabled`, `cached`, `indexed`, `live`). +OpenCodex only ever writes `disabled` while the sidecar is off, and removes its marker-owned line +again once the sidecar is back on — a re-enabled sidecar whose client still had the native tool +switched off would have nothing to intercept. The write needs a managed `~/.codex/config.toml` +(`ocx sync`); the management response reports it as `codexWebSearch`, and +`ocx agent sidecar web --enabled off` prints whether it happened. A root `web_search` line the +operator set by hand is replaced while the sidecar is off, since two root keys of the same name +are not valid TOML; `ocx restore` replays the journal snapshot and brings that value back, like +every other line the injection rewrites. + You can still set `enabled: false` in `config.json` if you prefer to edit the file directly. Anthropic-OAuth search and image description reuse the existing Claude Code OAuth fingerprint precedent, but should be soak-tested with the diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md index 0f68be9f7ac..781424cea41 100644 --- a/docs-site/src/content/docs/reference/cli/agents.md +++ b/docs-site/src/content/docs/reference/cli/agents.md @@ -31,8 +31,16 @@ stay writable). ```bash ocx agent sidecar web --list ocx agent sidecar web --model gpt-5.6-luna +ocx agent sidecar web --enabled off ``` +`--enabled off` is the same switch as the Dashboard's Off row: OpenCodex stops running the +sidecar and the Codex integration writes `web_search = "disabled"` into `~/.codex/config.toml`, +which is what lets an MCP search server be the only search path. `--enabled on` removes that +marker-owned line again. The command reports the Codex-side write (`codexWebSearch` in `--json`, +a trailing `Codex config:` line otherwise) and points at `ocx sync` when it could not happen. +The flag works for `vision` too. + ### `ocx effort [status|set|clear]` Inspect or change main and subagent reasoning-effort caps through the live proxy, or the local diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 0394a17d5f3..d3aee62c2c1 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -657,7 +657,7 @@ Images API paths and response shape expected by Codex. | Field | Type | Default | Meaning | | --- | --- | --- | --- | -| `enabled?` | `boolean` | on when usable | Master switch. | +| `enabled?` | `boolean` | on when usable | Master switch. When false, OpenCodex stops intercepting `web_search` AND the Codex integration writes `web_search = "disabled"` into `~/.codex/config.toml`. | | `backend?` | `"openai" \| "anthropic" \| "xai" \| "gemini" \| "exa"` | `openai` | Explicit wins; unset always resolves to `openai`. `anthropic` and `xai` run only when explicitly configured; `gemini` and `exa` remain reserved until their executors ship. | | `model?` | `string` | backend-dependent | `gpt-5.6-luna` for OpenAI, `claude-sonnet-5` for Anthropic, or `grok-4.6` for xAI. Legacy explicit `gpt-5.4-mini` migrates on start. | | `exaApiKey?` | `string` | none | Operator key for the `exa` backend. Write-only: management reads never return the stored value. | diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 85a992dabef..8437742522d 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -356,6 +356,7 @@ export const de: Record = { "dash.visionModelHint": "Modell zur Beschreibung von Bildern für nur-Text-Routen. Erfordert ChatGPT-Login.", "dash.webSearchSidecar": "Websuche-Sidecar", "dash.webSearchSidecarHint": "Backend und Modell für die Websuche gerouteter Modelle auswählen.", + "dash.webSearchOff": "Aus", "dash.webSearchStream": "Antworten live streamen", "dash.webSearchStreamHint": "Führenden Text und Reasoning live streamen, bis das Modell über einen Tool-Aufruf entscheidet; der Rest bleibt für das Abfangen der Suche gepuffert. Text vor einer Suche kann sich teilweise wiederholen.", "dash.visionSidecar": "Vision-Sidecar", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index d48d0ed10bb..6a5b79ad92d 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -368,6 +368,7 @@ export const en = { "dash.visionModelHint": "Model used to describe images for text-only routed models. Requires ChatGPT login.", "dash.webSearchSidecar": "Web search sidecar", "dash.webSearchSidecarHint": "Choose the backend and model used for web search on routed models.", + "dash.webSearchOff": "Off", "dash.webSearchStream": "Stream answers live", "dash.webSearchStreamHint": "Stream the model’s leading text and reasoning live until it decides on a tool call; the rest of the turn stays buffered for search interception. Text written before a search may partially repeat.", "dash.visionSidecar": "Vision sidecar", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 659ea434de2..98563f1b6b7 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -358,6 +358,7 @@ export const fr: Record = { "dash.visionModelHint": "Modèle utilisé pour décrire les images aux modèles routés en mode texte uniquement. Nécessite une connexion à ChatGPT.", "dash.webSearchSidecar": "Service auxiliaire de recherche Web", "dash.webSearchSidecarHint": "Choisissez le moteur et le modèle utilisés pour la recherche Web sur les modèles routés.", + "dash.webSearchOff": "Désactivé", "dash.webSearchStream": "Diffuser les réponses en direct", "dash.webSearchStreamHint": "Diffuse en direct le texte initial et le raisonnement du modèle jusqu’à ce qu’il décide d’appeler un outil ; le reste du tour demeure en mémoire tampon pour intercepter la recherche. Le texte produit avant une recherche peut être partiellement répété.", "dash.visionSidecar": "Service auxiliaire de vision", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 2136f3322e1..d2d876a73d7 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -365,6 +365,7 @@ export const ja: Record = { "dash.visionModelHint": "テキスト専用ルーティングモデルで画像を説明するために使うモデル。ChatGPT ログインが必要です。", "dash.webSearchSidecar": "ウェブ検索サイドカー", "dash.webSearchSidecarHint": "ルーティングモデルでウェブ検索に使うバックエンドとモデルを選択します。", + "dash.webSearchOff": "オフ", "dash.webSearchStream": "回答をライブ配信", "dash.webSearchStreamHint": "モデルがツール呼び出しを決定するまで、先頭のテキストと推論をライブ配信します。以降は検索インターセプトのためバッファされます。検索前のテキストは一部繰り返される場合があります。", "dash.visionSidecar": "ビジョンサイドカー", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b7a459d52de..59df62b5b73 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -360,6 +360,7 @@ export const ko: Record = { "dash.visionModelHint": "텍스트 전용 라우팅 모델에 이미지를 설명하는 데 사용되는 모델입니다. ChatGPT 로그인 필요.", "dash.webSearchSidecar": "웹 검색 사이드카", "dash.webSearchSidecarHint": "라우팅 모델의 웹 검색에 쓸 백엔드와 모델을 고릅니다.", + "dash.webSearchOff": "끔", "dash.webSearchStream": "응답 실시간 스트리밍", "dash.webSearchStreamHint": "모델이 도구 호출을 결정할 때까지 앞부분 텍스트와 추론을 실시간 스트리밍합니다. 이후는 검색 가로채기를 위해 버퍼링됩니다. 검색 전 텍스트가 일부 반복될 수 있습니다.", "dash.visionSidecar": "비전 사이드카", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 93965606aa7..d4d29d30c56 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -365,6 +365,7 @@ export const ru: Record = { "dash.visionModelHint": "Модель, которая описывает изображения для маршрутизируемых моделей, работающих только с текстом. Требуется вход в аккаунт ChatGPT.", "dash.webSearchSidecar": "Сайдкар веб-поиска", "dash.webSearchSidecarHint": "Выберите бэкенд и модель, используемые для веб-поиска на маршрутизируемых моделях.", + "dash.webSearchOff": "Выкл", "dash.webSearchStream": "Стримить ответы вживую", "dash.webSearchStreamHint": "Транслировать начальный текст и рассуждения вживую, пока модель не решит вызвать инструмент; остальное буферизуется для перехвата поиска. Текст до поиска может частично повторяться.", "dash.visionSidecar": "Сайдкар для изображений", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 1856735a5bc..5d8f0084159 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -366,6 +366,7 @@ export const tr: Record = { "dash.visionModelHint": "Salt metin yönlendirilen modeller için görselleri tanımlamakta kullanılan model. ChatGPT girişi gerektirir.", "dash.webSearchSidecar": "Web arama yan aracı (sidecar)", "dash.webSearchSidecarHint": "Yönlendirilen modellerde web araması için kullanılan arka ucu ve modeli seçin.", + "dash.webSearchOff": "Kapalı", "dash.webSearchStream": "Yanıtları canlı akıt", "dash.webSearchStreamHint": "Model bir araç çağrısına karar verene kadar baştaki metni ve akıl yürütmeyi canlı akıtır; kalanı arama yakalama için arabelleğe alınır. Aramadan önce yazılan metin kısmen tekrarlanabilir.", "dash.visionSidecar": "Görsel yan aracı (sidecar)", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index a1b8502f152..6a2c746ea24 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -358,6 +358,7 @@ export const vi: Record = { "dash.visionModelHint": "Model được sử dụng để mô tả hình ảnh cho các model định tuyến chỉ hỗ trợ văn bản. Yêu cầu đăng nhập ChatGPT.", "dash.webSearchSidecar": "Web search sidecar", "dash.webSearchSidecarHint": "Chọn backend và model được sử dụng cho tìm kiếm web trên các models định tuyến.", + "dash.webSearchOff": "Tắt", "dash.webSearchStream": "Phát trực tuyến (Stream) các câu trả lời trực tiếp", "dash.webSearchStreamHint": "Phát trực tuyến các văn bản dẫn dắt và quá trình lý luận của model cho đến khi nó quyết định gọi một công cụ; phần còn lại của lượt chạy sẽ được lưu đệm (buffered) để can thiệp tìm kiếm. Văn bản được viết trước một tìm kiếm có thể lặp lại một phần.", "dash.visionSidecar": "Vision sidecar", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index eba5ce1b2ea..4123cadbd04 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -255,6 +255,7 @@ export const zhTW: Record = { "dash.visionModelHint": "為純文字路由模型描述圖像的模型。需要 ChatGPT 登入。", "dash.webSearchSidecar": "網頁搜尋附屬服務", "dash.webSearchSidecarHint": "選擇路由模型進行網頁搜尋時使用的後端和模型。", + "dash.webSearchOff": "關閉", "dash.webSearchStream": "即時串流輸出回答", "dash.webSearchStreamHint": "即時串流輸出開頭的文字和推理,直到模型決定呼叫工具;其餘部分為攔截搜尋而保持緩衝。搜尋前的文字可能會部分重複。", "dash.visionSidecar": "視覺附屬服務", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index fe9424b8151..b4c2e51d3a1 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -360,6 +360,7 @@ export const zh: Record = { "dash.visionModelHint": "为纯文本路由模型描述图像的模型。需要 ChatGPT 登录。", "dash.webSearchSidecar": "网页搜索附属服务", "dash.webSearchSidecarHint": "选择路由模型进行网页搜索时使用的后端和模型。", + "dash.webSearchOff": "关闭", "dash.webSearchStream": "实时流式输出回答", "dash.webSearchStreamHint": "实时流式输出开头的文本和推理,直到模型决定调用工具;其余部分为拦截搜索而保持缓冲。搜索前的文本可能会部分重复。", "dash.visionSidecar": "视觉附属服务", diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 2aa28acb107..f514ea61ffb 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -17,6 +17,7 @@ import { shadowCallModelOptions, webSearchSidecarSelectionForModel, updateJobLabel, + webSearchEnabledPatch, visionEnabledPatch, visionMaxDescriptionsPatch, visionReasoningLadder, @@ -445,6 +446,9 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) { } = d; const visionEnabled = sidecar?.vision?.enabled !== false; const visionModel = visionEnabled ? (sidecar?.vision?.model ?? "gpt-5.6-luna") : ""; + const webSearchEnabled = sidecar?.webSearch?.enabled !== false; + // Same shape as the Vision card: Off is a row in the picker, and choosing a model is the way back. + const webSearchModel = webSearchEnabled ? (sidecar?.webSearch?.model ?? "gpt-5.6-luna") : ""; const persistedVisionReasoning = sidecar?.vision?.reasoning ?? "low"; const visionLadder = visionReasoningLadder(models, visionModel); const visionReasoning = clampVisionReasoningToLadder(visionLadder, persistedVisionReasoning); @@ -561,10 +565,17 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {