diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b6d7e7ad467..838b4b273c9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,7 +77,7 @@ on:
# Nightly safety net. The Windows shards run on a pull request only when the
# changes job judges it Windows-sensitive, so anything that slips past that
# filter must still be caught within a day: a scheduled run requests the full
- # lane=all matrix (nine Windows shards and the macOS control included).
+ # lane=all matrix (eleven Windows shards and the macOS control included).
#
# GitHub runs schedules from the DEFAULT branch, which is main, so this
# trigger activates only once this file reaches main by promotion, and the
@@ -235,7 +235,7 @@ jobs:
# Push, dispatch and schedule run both regardless; see the docker/keyring filters.
docker: ${{ steps.narrow.outputs.docker }}
keyring: ${{ steps.narrow.outputs.keyring }}
- # Whether this pull request needs the nine Windows shards: the windows path
+ # Whether this pull request needs the Windows shards: the windows path
# filter below OR an added src/tests line naming a Windows marker. Computed
# by scripts/ci/windows-sensitive-diff.sh, which validates its input and
# treats the diff as data.
@@ -428,7 +428,7 @@ jobs:
- 'package.json'
- 'bun.lock'
- '.github/workflows/ci.yml'
- # Paths whose change selects the nine Windows suite shards on a pull
+ # Paths whose change selects the Windows suite shards on a pull
# request. These carry the Windows service, update, shim, intercept,
# ACL and process code, and the harness every Windows shard runs.
# scripts/ci/windows-sensitive-diff.sh adds the content markers.
@@ -696,10 +696,7 @@ jobs:
# gui/package.json. Without this the affected shards die on
# `Cannot find module 'react/jsx-dev-runtime'` while the other shards pass.
- name: Install dependencies
- run: |
- bun install --frozen-lockfile
- cd gui
- bun install --frozen-lockfile
+ run: bash scripts/ci/bun-install-frozen.sh . gui
# Nor is the build. Tests that fetch the served dashboard read the session
# bootstrap meta tags out of `gui/dist/index.html`, so without a build the
@@ -734,7 +731,7 @@ jobs:
with:
role: test-runner # See the Bun roles contract above test.
- name: Install dependencies and build the GUI the tests serve
- run: bun install --frozen-lockfile && cd gui && bun install --frozen-lockfile && bun run build
+ run: bash scripts/ci/bun-install-frozen.sh . gui && cd gui && bun run build
- name: Test the files this pull request reaches
run: bash scripts/ci/run-changed-tests.sh
@@ -760,10 +757,7 @@ jobs:
role: test-runner # See the Bun roles contract above test.
- name: Install dependencies
- run: |
- bun install --frozen-lockfile
- cd gui
- bun install --frozen-lockfile
+ run: bash scripts/ci/bun-install-frozen.sh . gui
- name: Build GUI
run: |
@@ -802,10 +796,7 @@ jobs:
role: test-runner # See the Bun roles contract above test.
- name: Install dependencies
- run: |
- bun install --frozen-lockfile
- cd gui
- bun install --frozen-lockfile
+ run: bash scripts/ci/bun-install-frozen.sh . gui
- name: Build GUI
run: |
@@ -840,10 +831,7 @@ jobs:
role: test-runner # See the Bun roles contract above test.
- name: Install dependencies
- run: |
- bun install --frozen-lockfile
- cd gui
- bun install --frozen-lockfile
+ run: bash scripts/ci/bun-install-frozen.sh . gui
- name: GUI lint
if: needs.changes.outputs.gui == 'true'
@@ -897,29 +885,29 @@ jobs:
run: bun run src/cli/index.ts help
platform-macos:
- name: macos ${{ matrix.shard }}/3
+ name: macos ${{ matrix.shard }}/4
needs: changes
# Never on pull requests: macOS legs run after merge, on push to
# main/preview, and on dispatch and the schedule.
if: github.event_name != 'pull_request'
runs-on: macos-latest
- # Three shards. Unsharded, this job was the critical path on every green dev
- # push (mean 14.9 min against a 4.7 min Linux maximum; devlog
- # 260905_test_modularization_and_windows/003), so it was split in two. The
- # suite then outgrew two halves: across seventeen dev dispatches from
- # 2026-10-04 to 10-12 the slower half's test step went from 867s to 1155s,
- # and run 38214884055 cancelled macos 2/2 at this cap with 68 files left
- # while every test it had run was passing (#6978). Replaying the duration
- # assignment against those runs' measured per-file times puts the slowest of
- # three shards at 635-796s of tests, about 14 minutes with setup. The cap
- # stays 20: the suite needs another shard, not a longer wait, and a wedged
- # shard (#4956) is still cut off as early as before. The full-membership
- # control runs the same bounded batches without sharding on explicit dispatch.
+ # Four shards. Three replaced two after run 38214884055 hit this cap with
+ # 68 files left and no failing tests (#6978). The suite outgrew three too:
+ # run 40727740334 took 13.1-16.4m, 40743681086 12.7-13.7m, and
+ # 40756291846 15.0-17.1m. In dev lane=all run 40766068492 (b16bc0978b),
+ # macos 1/3 (123090588998) reached batch 73/88 and macos 2/3
+ # (123090589363) batch 93/93 before this cap cancelled them, with no failing
+ # tests. The cap stays 20: the suite needs another shard, not a longer wait,
+ # and a wedged shard (#4956) is still cut off as early as before. Four suite
+ # shards + control + widget/bundle request six macOS jobs for five runner
+ # slots: one queues, and each run pays one extra shard's setup overhead.
+ # The full-membership control runs the same bounded batches without sharding
+ # on explicit dispatch.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
- shard: [1, 2, 3]
+ shard: [1, 2, 3, 4]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
@@ -965,7 +953,7 @@ jobs:
- name: Test in fresh-process batches
env:
- TEST_SHARD: ${{ matrix.shard }}/3
+ TEST_SHARD: ${{ matrix.shard }}/4
BUN_TEST_FILE_SCOPE: all
BUN_TEST_BATCH_SIZE: '12'
BUN_TEST_PARALLEL: '1'
@@ -1077,7 +1065,7 @@ jobs:
# each landed with green pull-request CI while breaking Windows, which is why
# that dispatch is part of merging a Windows-sensitive pull request.
platform-windows:
- name: windows ${{ matrix.shard }}/9
+ name: windows ${{ matrix.shard }}/11
needs: [changes, select-windows-runner]
if: >-
github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' && (github.event.inputs.lane == '' || github.event.inputs.lane == 'all')
@@ -1108,21 +1096,18 @@ jobs:
# test deadline and all six shards, but left the whole batch and cleanup a
# 30-minute bound.
#
- # Six shards then grew into the 30-minute ceiling too. Across seven lane=all
- # dispatches the six shard totals were 114.3-133.2 minutes. The worst observed
- # shard imbalance was 1.43x its dispatch's per-shard average. Eight shards leave
- # no margin: 133.2 / 8 * 1.43 * 1.25 = 29.8 minutes after the already-observed
- # 25% run-to-run slowdown. Nine gives 133.2 / 9 * 1.43 * 1.25 = 26.5 minutes.
- # Keep the 30-minute bound and pay for three more concurrent runners plus their
- # repeated checkout/install/build setup so test work, rather than the ceiling,
- # shrinks. The per-batch timeout below is independent: it aborts a stuck Bun
- # process but adds no delay to a healthy one, so calibrating it does not change
- # this total-work projection.
+ # Nine shards also reached the ceiling: run 40756291846 cancelled 7/9 at batch
+ # 49/60; run 40743681086's successful shards took 22.1-29.5 minutes. Refreshing
+ # Windows weights from completed primary batches in both runs puts test work
+ # at 24.36/21.92/19.93 minutes with 9/10/11 shards. With the observed 2.4-minute
+ # maximum setup, eleven projects ~22.4 minutes; 25% test variance makes ~27.3.
+ # Pay for two extra setups (~3.3 runner-minutes at the older run's mean), keep
+ # the 30-minute job bound and all tests, and leave batch deadlines independent.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
- shard: [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
steps:
- name: Show selected runner
shell: bash
@@ -1202,7 +1187,7 @@ jobs:
# and service-manager guard before the lock boundary.
shell: bash
env:
- TEST_SHARD: ${{ matrix.shard }}/9
+ TEST_SHARD: ${{ matrix.shard }}/11
BUN_TEST_FILE_SCOPE: all
BUN_TEST_BATCH_SIZE: "6"
BUN_TEST_BATCH_TIMEOUT_SECONDS: "480"
@@ -1341,7 +1326,7 @@ jobs:
# aggregate reported success while nothing had read the change (#5001).
#
# Running the gate directly here, rather than adding `structure/**` to `ci`,
- # keeps a prose edit from starting nine Windows shards and two macOS shards for
+ # keeps a prose edit from starting the Windows shards and macOS shards for
# a check that takes seconds. It runs whenever `structure/` changes, including
# alongside a `src/` change where the suite covers it too; a duplicate of a
# seconds-long check is not worth a condition that could be wrong.
@@ -1893,7 +1878,7 @@ jobs:
if [ -z "$LANE" ] || [ "$LANE" = "all" ] || [ "$LANE" = "macos-control" ]; then
dispatch=requested
fi
- # Only the default/all lane requests the nine Windows suite shards.
+ # Only the default/all lane requests the Windows suite shards.
if [ -z "$LANE" ] || [ "$LANE" = "all" ]; then
windows=requested
fi
@@ -1965,10 +1950,10 @@ jobs:
# started: five successes and one skipped leg roll up to `success`. No matrix here
# carries a per-leg `if:`, so a leg can only be skipped when its whole job is, which
# the expectation table above already catches. What that table cannot catch is a
- # matrix that produced FEWER legs than the nine a dispatch is run to read, so count
- # them by name. The nine Windows legs are the entire output of that dispatch.
+ # matrix that produced FEWER legs than the eleven a dispatch is run to read, so count
+ # them by name. The eleven Windows legs are the entire output of that dispatch.
if [ "$windows" = requested ]; then
- shards=9
+ shards=11
# `filter=latest` (the default) is the latest execution of each job in the run,
# which is what a human reading the run sees. Asking for one ATTEMPT instead would
# fail every partial re-run: "Re-run failed jobs" puts only the repaired shard in
diff --git a/.github/workflows/cleanup-orphaned-workflows.yml b/.github/workflows/cleanup-orphaned-workflows.yml
index 6915a063ed2..f0360fb92eb 100644
--- a/.github/workflows/cleanup-orphaned-workflows.yml
+++ b/.github/workflows/cleanup-orphaned-workflows.yml
@@ -35,9 +35,9 @@ jobs:
persist-credentials: false
- name: Setup Bun
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- with:
- bun-version: 1.4.2
+ # The repository composite reads package.json dependencies.bun from the
+ # checkout above, so this job cannot drift from the runtime pin.
+ uses: ./.github/actions/setup-project-bun
- name: Remove stale workflow histories
env:
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 94c962193a3..9853a0b2744 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -29,7 +29,8 @@ jobs:
with:
path: ./docs-site
node-version: 22
- package-manager: bun@latest
+ # Must equal package.json dependencies.bun (asserted in ci-workflows.test.ts).
+ package-manager: bun@1.4.4
deploy:
needs: build
diff --git a/AGENTS.md b/AGENTS.md
index 2e1c13e6dca..f71dc2dd0f3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -195,7 +195,7 @@ it binds you regardless of which mechanism is within reach.
```bash
bun install
-bun run typecheck # bun x tsc --noEmit (strict)
+bun run typecheck # bun check --no-script --noEmit (strict)
bun run test:changed # import-graph tests against the resolved `dev` merge base
bun run test # full tests/ suite (default before review)
bun run lint:gui # GUI eslint
@@ -375,6 +375,20 @@ than nudged.
add `ci:full` for changes it cannot see (subprocesses, files read as data, generated output).
- Full cross-platform (macOS/Windows) CI runs **only** via manual
`workflow_dispatch` and on push to `main`/`preview` (release lines).
+- A manual `lane=all` dispatch is a **batch check on `dev`**, never a per-PR gate.
+ One run is about 32 jobs (11 Windows, at least 6 macOS, 15 Linux) and six runner-hours, and the
+ account has only five macOS slots (a macOS job can queue), so per-PR dispatches starve
+ every other check. On 2026-10-27, parallel agent lanes dispatched 48 of them in twelve hours (about 13,000
+ runner-minutes). The queue passed 200 runs, and ordinary PR checks waited hours to start.
+ - Merge a pull request, including a Windows- or macOS-sensitive one, on its green PR
+ CI. After a batch of merges, run one `gh workflow run ci.yml --ref dev` as described
+ under "Batched merges". If it is red, stop merging and fix `dev` from that batch.
+ - Do not dispatch `lane=all` for a PR branch unless a maintainer explicitly asks for it
+ on that PR. Even then, run it once, on the exact head about to merge. Never re-dispatch
+ because the head moved (an `origin/dev` merge or a review fix), and never rerun to
+ green. Cancel your own superseded dispatches with `gh run cancel`.
+ - Every description edit or label change starts three event-gate workflows. Batch those
+ edits, and keep the number of agents working pull requests in parallel small.
- **Never** add a `dev` push trigger, or `pull_request` runs for macOS/Windows
jobs, to any workflow. PRs that change this policy will be closed.
diff --git a/README.md b/README.md
index 5fba47f1840..8197f747220 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,11 @@
-
+
-
make codex open!
-
Universal provider proxy for OpenAI Codex, Claude Code, Claude Desktop & Grok Build
-Two commands, and every one of them runs any LLM you point it at.
+
One local gateway for your AI coding tools and models.
+
Use the models you choose with Codex, Claude Code, Claude Desktop and Grok Build,
+and connect OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code and more through managed configs.
+See every supported tool →
-opencodex is a lightweight local proxy that translates Codex's Responses API into whatever your
-provider speaks — streaming, tool calls, reasoning tokens, images, in both directions. Use Claude,
-Gemini, Grok, GLM, DeepSeek, Kimi, Qwen, Ollama, or any other LLM with Codex, Claude Code, Claude
-Desktop, and Grok Build. It can also manage a **ChatGPT account pool** for Codex auth: add accounts,
+opencodex is a lightweight local gateway between your coding tools and your model providers. It
+accepts the Responses, Chat Completions and Anthropic Messages APIs and translates each request into
+whatever your provider speaks, carrying streaming, tool calls, reasoning and images where the client,
+model and provider support them. Use Claude, Gemini, Grok, GLM, DeepSeek, Kimi, Qwen, Ollama and other
+providers with Codex, Claude Code, Claude Desktop, Grok Build and the tools listed on the
+[client support page](https://opencodex.me/reference/client-support/). It can also manage a **ChatGPT account pool** for Codex auth: add accounts,
refresh their quotas in the dashboard, and let new sessions auto-route to the lowest-usage healthy
account while existing threads stay pinned to the account that started them.
@@ -102,6 +105,22 @@ Open **http://localhost:10100** and configure everything in the web dashboard
(40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. `ocx gui`
re-opens the dashboard at any time.
+### Connect your tool
+
+Connect the tool you already use:
+
+| Tool | How to connect |
+|---|---|
+| Codex | Turn on the Codex switch on the dashboard's **Connect** page, then run `codex` as usual. On a local listener this points Codex's built-in provider at the proxy. From the terminal, run `ocx init` before adding providers in the dashboard and accept its Codex prompt. |
+| Claude Code | With the `claude` CLI installed, run `ocx claude`. It starts the proxy if needed and launches Claude Code through it. |
+| Claude Desktop | Run `ocx claude desktop`. On a new install it sets up the gateway profile for the whole app; an existing first-party setup is kept, and `ocx claude desktop apply --gateway` switches explicitly. See the [Claude Code guide](https://opencodex.me/guides/claude-code/). Fully quit and reopen Claude Desktop after a switch. |
+| Grok Build | On a local listener with Grok integration enabled, `ocx start` registers the models when `~/.grok` exists; pick an `ocx-*` model in Grok Build. Remote setups: see the [Grok Build guide](https://opencodex.me/guides/grok-build/). After adding providers, restart the proxy (or run `ocx ensure` when Codex autostart is enabled) to refresh the list. |
+| Tools with a managed integration | Use the dashboard **Connect** page, or `ocx integration client enable --client `. |
+| Any other tool | Set a custom OpenAI-compatible base URL of `http://localhost:10100/v1`, or an Anthropic base URL of `http://localhost:10100`. |
+
+The [client support page](https://opencodex.me/reference/client-support/) lists which path each tool has
+and which protocol it speaks.
+
Desktop app (beta)
diff --git a/bin/ocx.mjs b/bin/ocx.mjs
index 22c391d4b42..2be5336d9b5 100755
--- a/bin/ocx.mjs
+++ b/bin/ocx.mjs
@@ -1,4 +1,5 @@
#!/usr/bin/env node
+import { guardedRun, guardedSpawn, guardedSpawnSync, guardedExit, probeProcessAlive, relayOwnSignal } from "../src/update/mutation-guard.mjs";
/**
* opencodex published-package bin launcher.
*
@@ -8,13 +9,15 @@
* Node shim. (Dev still runs `bun run src/cli/index.ts` directly via the shebang on
* src/cli/index.ts — only the published npm/pnpm `bin` routes through here.)
*/
-import { spawn, spawnSync } from "node:child_process";
+import { probeSpawn, probeSpawnSync } from "../src/update/mutation-guard.mjs";
import { createSupervisionLatch, inspectDesktopSupervision } from "../src/service/desktop-supervision.mjs";
import { planDesktopCliHandoff, runDesktopCliHandoff } from "../src/lib/desktop-cli-handoff.mjs";
import { desktopCliRecordPath } from "../src/lib/desktop-cli-record.mjs";
import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../src/update/stop-contract.mjs";
import { probeProxyLiveness } from "../src/update/proxy-liveness-probe.mjs";
import { decidePostStopUpdate } from "../src/update/stop-decision.mjs";
+import { createUpdateServiceHomePolicy, isUpdateHomeProbe, parseUpdateWorkerHome, UPDATE_HOME_LAUNCHER_PROTOCOL, UPDATE_PACKAGE_ONLY_EXIT_CODE, UPDATE_PACKAGE_ONLY_PROTOCOL_VERSION } from "../src/update/service-home-policy.mjs";
+import { probeUpdateServiceHome, supportsUpdateHomeProbe, updateHomeProbeEnvironment } from "../src/update/service-home-probe.mjs";
import {
inspectPackageRuntimeLiveness,
planStoppedRuntimeRecovery,
@@ -64,6 +67,7 @@ import {
CODEX_CLI_TRANSPORT_ENV_KEYS,
CODEX_CLI_VERSION_MANAGER_ROOT_ENV_SLOTS,
codexCliInspectionLookupSeeds,
+ carriesCodexCliUpdateInspectionContext,
isCodexCliUpdateInspectionArgv,
} from "../src/update/codex-cli-update-launch-policy.mjs";
import {
@@ -84,7 +88,6 @@ try {
/* best-effort */
}
}
-const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));
const installOwnership = detectInstallOwnershipFromPath(here, { exists: existsSync });
const installMethod = installOwnership.installer;
@@ -108,9 +111,9 @@ function currentPackageVersion() {
}
}
-function pinnedBunVersion() {
+function pinnedBunVersion(launcherPath) {
try {
- const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
+ const pkg = JSON.parse(readFileSync(join(launcherPath ? dirname(launcherPath) : here, "..", "package.json"), "utf8"));
return typeof pkg.dependencies?.bun === "string" ? pkg.dependencies.bun : "";
} catch {
return "";
@@ -139,6 +142,19 @@ function configDir() {
return resolve(raw ? expandUserPath(raw) : join(homedir(), ".opencodex"));
}
+// Direct Bun entry only: never recurse through another Node launcher or ordinary CLI startup.
+function readNodeUpdateHome(launcherPath) {
+ if (!supportsUpdateHomeProbe(launcherPath)) return { kind: "unknown" };
+ const bun = resolveBun({ allowInstall: false, launcherPath, allowMissing: true });
+ if (!bun) return { kind: "unknown" };
+ const env = updateHomeProbeEnvironment(process.env, {
+ codexHome: process.env.CODEX_HOME ?? "", opencodexHome: configDir(),
+ });
+ return probeUpdateServiceHome(probeSpawnSync, bun.path,
+ ["--no-env-file", join(dirname(launcherPath), "..", "src", "update", "service-home-probe-entry.ts")], { env });
+}
+
+
function shouldRepairCodexShim() {
return existsSync(join(configDir(), "codex-shim.json"));
}
@@ -155,9 +171,10 @@ function historyRestoreIncomplete() {
}
}
-function repairCodexShimIfNeeded(launcherPath = fileURLToPath(import.meta.url)) {
+function repairCodexShimIfNeeded(launcherPath, authorizeMutation) {
if (!shouldRepairCodexShim()) return;
- const res = spawnSync(process.execPath, [launcherPath, "codex-shim", "install"], {
+ if (!authorizeMutation("shim")) return false;
+ const res = guardedSpawnSync(authorizeMutation, "shim", process.execPath, [launcherPath, "codex-shim", "install"], {
stdio: "inherit",
windowsHide: true,
});
@@ -173,15 +190,16 @@ function trayInstallState() {
try {
const heartbeat = JSON.parse(readFileSync(join(configDir(), "tray-heartbeat.json"), "utf8"));
if (Number.isSafeInteger(heartbeat.pid) && Date.now() - Number(heartbeat.timestamp) < 15_000) {
- process.kill(heartbeat.pid, 0);
+ probeProcessAlive(heartbeat.pid);
running = true;
}
} catch { /* installed but not running */ }
return { installed: true, running };
}
-function runTrayLifecycle(launcher, action) {
- return spawnSync(process.execPath, [launcher, "tray", action], {
+function runTrayLifecycle(launcher, action, authorizeMutation) {
+ if (!authorizeMutation(action === "start" ? "own-tray" : "tray")) return { status: 1 };
+ return guardedSpawnSync(authorizeMutation, action === "start" ? "own-tray" : "tray", process.execPath, [launcher, "tray", action], {
stdio: "inherit",
windowsHide: true,
});
@@ -227,7 +245,7 @@ async function runPackageManagerSelfUpdate(manager) {
runPnpm: (commandPath, args, capture = false) => {
const invocation = pnpmInvocationForPath(commandPath, args);
if (!invocation) return { status: 1 };
- return spawnSync(invocation.file, invocation.args, {
+ return probeSpawnSync(invocation.file, invocation.args, {
stdio: capture ? "pipe" : "ignore",
encoding: "utf8",
timeout: 20_000,
@@ -270,7 +288,7 @@ async function runPackageManagerSelfUpdate(manager) {
console.error(`opencodex: could not resolve ${manager} from a trusted absolute PATH entry; aborting before stopping the proxy.`);
process.exit(1);
}
- const latestResult = spawnSync(latestInvocation.file, latestInvocation.args, readProbeOptions(latestInvocation));
+ const latestResult = probeSpawnSync(latestInvocation.file, latestInvocation.args, readProbeOptions(latestInvocation));
const latest = latestResult.status === 0 && typeof latestResult.stdout === "string" ? latestResult.stdout.trim() : "";
console.log(`opencodex v${current} (installed via ${manager}, tag ${tag})`);
@@ -282,7 +300,7 @@ async function runPackageManagerSelfUpdate(manager) {
const integrity = checkRegistryPackageIntegrity(PKG, latest || null, args => {
const invocation = managerInvocation(args);
if (!invocation) return { status: 1 };
- return spawnSync(invocation.file, invocation.args, readProbeOptions(invocation));
+ return probeSpawnSync(invocation.file, invocation.args, readProbeOptions(invocation));
});
if (integrity.ok === false) {
console.error(`opencodex: ${integrity.reason}; aborting before stopping the proxy.`);
@@ -319,7 +337,10 @@ async function runPackageManagerSelfUpdate(manager) {
const serviceStatePaths = process.env.OCX_TEST_HOME_GUARD === "1"
? allServiceStatePaths.slice(0, 1)
: allServiceStatePaths;
- const serviceWasInstalled = serviceStatePaths.some(path => existsSync(path));
+ let authorityLauncher = fileURLToPath(import.meta.url);
+ const homePolicy = createUpdateServiceHomePolicy(() => readNodeUpdateHome(authorityLauncher), message => console.warn(message), process.argv.includes("--update-package-only"), parseUpdateWorkerHome(process.argv));
+ if (!homePolicy.authorizeMutation("package")) process.exit(1);
+ const serviceWasInstalled = homePolicy.serviceAllowed && serviceStatePaths.some(path => existsSync(path));
// What this update may do to the runtime. The same rule the Bun updater applies, from the
// same module: a desktop takeover vetoes both the stop and the service refresh below.
const readServiceState = () => selectAuthoritativeServiceState(
@@ -355,7 +376,7 @@ async function runPackageManagerSelfUpdate(manager) {
process.exit(1);
}
const trayBeforeUpdate = planWindowsTrayUpdate(
- process.platform === "win32" ? trayInstallState() : { installed: false, running: false },
+ !homePolicy.packageOnly && process.platform === "win32" ? trayInstallState() : { installed: false, running: false },
);
/**
* Refresh the existing service in place. `service repair` discovers the installed backend;
@@ -386,7 +407,7 @@ async function runPackageManagerSelfUpdate(manager) {
*/
function readServiceInstalledFromStatus(launcherPath) {
try {
- const st = spawnSync(process.execPath, [launcherPath, "status", "--json"], {
+ const st = probeSpawnSync(process.execPath, [launcherPath, "status", "--json"], {
encoding: "utf8",
timeout: 20_000,
windowsHide: true,
@@ -481,13 +502,20 @@ async function runPackageManagerSelfUpdate(manager) {
/** Returns true, false, or "vetoed" when another owner holds the runtime (not a failure). */
function startProxyDirectly() {
+ try { reacquireUpdateLease(); } catch {
+ console.warn("opencodex: recovery could not acquire runtime ownership; run 'ocx service repair' or 'ocx start' manually after the other operation finishes.");
+ return false;
+ }
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ const currentOwnership = readOwnership();
const supervision = observeSupervision();
- const plan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: false, supervision });
- if (!plan.mayStopRuntime) {
+ const plan = planUpdateRuntimeHandling({ ...currentOwnership, serviceInstalled: false, supervision });
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ if (ownershipIdentity(currentOwnership) !== ownershipIdentity(initialOwnership) || !plan.mayStopRuntime) {
console.warn(supervision
? "OpenCodex Desktop supervises the proxy; no CLI runtime was restored."
: plan.notice);
- return "vetoed";
+ return homePolicy.authorizeMutation("publish") ? "vetoed" : "refused";
}
if (!postUpdateLauncherUsable || !existsSync(postUpdateLauncher)) {
console.error("opencodex: cannot restart the proxy because the launcher is missing; reinstall opencodex manually.");
@@ -499,7 +527,8 @@ async function runPackageManagerSelfUpdate(manager) {
delete env.OCX_SIBLING_OF_PORT;
delete env.OCX_SIBLING_HANDOFF_NONCE;
console.log(`Attempting to restart the proxy on port ${bakePort}.`);
- const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(bakePort)], {
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ const child = guardedSpawn(homePolicy.authorizeMutation, "runtime", process.execPath, [postUpdateLauncher, "start", "--port", String(bakePort)], {
detached: true,
stdio: "ignore",
windowsHide: true,
@@ -513,7 +542,7 @@ async function runPackageManagerSelfUpdate(manager) {
while (Date.now() < deadline) {
const current = readCurrentRuntimeTarget();
if (current.kind === "target"
- && probeProxyLiveness(current.target.port, current.target.hostname ?? bakeHostname) === "live") return true;
+ && probeProxyLiveness(current.target.port, current.target.hostname ?? bakeHostname) === "live") return homePolicy.authorizeMutation("publish") ? true : "refused";
Atomics.wait(UPDATE_RECOVERY_SLEEP, 0, 0, UPDATE_RECOVERY_POLL_MS);
}
console.error("opencodex: the recovery proxy did not publish a healthy runtime before the recovery deadline.");
@@ -525,10 +554,19 @@ async function runPackageManagerSelfUpdate(manager) {
// "supervised", "failed", or "unsettled" when the manager may still finish the restart.
function restartThroughServiceManager() {
if (!hasRestartableServiceManager()) return "failed";
+ releaseUpdateLease();
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ if (!homePolicy.serviceAllowed) return "failed";
+ const owner = readOwnership();
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ if (ownershipIdentity(owner) !== ownershipIdentity(initialOwnership)
+ || !planUpdateRuntimeHandling({ ...owner, serviceInstalled: true, supervision: observeSupervision() }).mayRestoreService) return homePolicy.authorizeMutation("publish") ? "vetoed" : "refused";
console.log("opencodex: asking the service manager to restart the background service.");
const before = readCurrentRuntimeTarget();
const staleRecordPid = before.kind === "target" ? before.target.pid : null;
- const restarted = restartServiceManagerJob();
+ if (!homePolicy.authorizeMutation("service")) return homePolicy.authorizeMutation("runtime") ? "failed" : "refused";
+ const restarted = restartServiceManagerJob({}, homePolicy.authorizeMutation);
+ if (!restarted.ok && restarted.refused) return "refused";
const unsettled = !restarted.ok && restarted.unsettled === true;
let verdict;
if (!restarted.ok && !unsettled) {
@@ -549,7 +587,8 @@ async function runPackageManagerSelfUpdate(manager) {
verdict = supervisionVerdict({ servingPid, managerPid: readServiceManagerPid(), parentPid: pid => readParentPid(pid) });
}
if (verdict.kind === "supervised") {
- console.log(`opencodex: the proxy is healthy on port ${bakePort} under the service manager (pid ${verdict.managerPid}).`);
+ if (!homePolicy.authorizeMutation("publish")) return "refused";
+ guardedRun(homePolicy.authorizeMutation, "publish", console.log, [`opencodex: the proxy is healthy on port ${bakePort} under the service manager (pid ${verdict.managerPid}).`]);
return "supervised";
}
if (unsettled) {
@@ -565,21 +604,34 @@ async function runPackageManagerSelfUpdate(manager) {
* manager may still finish its restart; callers exit non-zero for both.
*/
function refreshBackgroundServiceOrStartDirect() {
+ authorityLauncher = postUpdateLauncher;
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ if (!homePolicy.serviceAllowed) return [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
+ let homeRefused = false;
const mayRefresh = () => {
+ if (!homePolicy.authorizeMutation("runtime")) { homeRefused = true; return false; }
const supervision = observeSupervision();
const plan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true, supervision });
+ if (!homePolicy.authorizeMutation("runtime")) { homeRefused = true; return false; }
if (!plan.mayRestoreService) console.warn(supervision
? "OpenCodex Desktop supervises the proxy; CLI recovery was skipped. Use the app's updater (tray → Check for Updates)."
: plan.notice);
- return plan.mayRestoreService;
+ return plan.mayRestoreService && homePolicy.serviceAllowed;
};
- if (!mayRefresh()) return;
+ if (!mayRefresh()) {
+ if (homeRefused) return "refused";
+ if (!homePolicy.serviceAllowed) return [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
+ return;
+ }
const prevBake = process.env.OCX_BAKE_PORT;
process.env.OCX_BAKE_PORT = String(bakePort);
try {
- let svc = spawnSync(process.execPath, serviceRefreshArgs(), {
+ if (!homePolicy.authorizeMutation("service")) return homePolicy.authorizeMutation("runtime") && [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
+ let svc = guardedSpawnSync(homePolicy.authorizeMutation, "service", process.execPath, serviceRefreshArgs(), {
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
});
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
+ if (!homePolicy.serviceAllowed) return [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
// `serviceWasInstalled` is inferred from service-state.json alone, which can be
// STALE — present while the registration is gone. Repair refuses that case by
// design, and its thrown Error is indistinguishable from any other failure at
@@ -589,18 +641,25 @@ async function runPackageManagerSelfUpdate(manager) {
// failure would resurrect the elevation prompt this change exists to avoid, and
// could re-register a service the user just uninstalled.
if (svc.status !== 0 && readServiceInstalledFromStatus(postUpdateLauncher) === false) {
- if (!mayRefresh()) return;
+ if (!mayRefresh()) {
+ if (homeRefused) return "refused";
+ if (!homePolicy.serviceAllowed) return [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
+ return;
+ }
console.log("No registered service found — installing it instead.");
- svc = spawnSync(process.execPath, serviceInstallArgs(), {
+ const serviceInstallArgsForSpawn = serviceInstallArgs();
+ if (!homePolicy.authorizeMutation("service")) return homePolicy.authorizeMutation("runtime") && [true, "vetoed"].includes(startProxyDirectly()) ? undefined : "refused";
+ svc = guardedSpawnSync(homePolicy.authorizeMutation, "service", process.execPath, serviceInstallArgsForSpawn, {
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
});
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
}
let needDirectStart = svc.status !== 0;
if (!needDirectStart) {
// Exit 0 can still leave stale/missing assets that never bring the proxy
// back — match the GUI/CLI fallthrough so /healthz is not left dead.
try {
- const st = spawnSync(process.execPath, [postUpdateLauncher, "status", "--json"], {
+ const st = probeSpawnSync(process.execPath, [postUpdateLauncher, "status", "--json"], {
encoding: "utf8",
timeout: 20_000,
windowsHide: true,
@@ -624,6 +683,7 @@ async function runPackageManagerSelfUpdate(manager) {
// just returned is indistinguishable from any other failure at this layer.
const supervision = observeSupervision();
const nowOwned = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: true, supervision });
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
if (!nowOwned.mayStopRuntime) {
console.warn(supervision
? "OpenCodex Desktop supervises the proxy; CLI recovery was skipped. Use the app's updater (tray → Check for Updates)."
@@ -631,9 +691,11 @@ async function runPackageManagerSelfUpdate(manager) {
return;
}
// #7043: the manager first. Only a proxy it owns counts as recovered.
+ if (!homePolicy.authorizeMutation("runtime")) return "refused";
const managed = restartThroughServiceManager();
- if (managed === "supervised") return;
+ if (managed === "supervised" || managed === "vetoed") return;
if (managed === "unsettled") return "unsettled";
+ if (managed === "refused") return "refused";
// Repair normally avoids elevation for a healthy registration, but a stale Windows
// scheduler definition can require it. It can also fail — or exit 0 while leaving
// a non-viable manager. Fall back to a direct detached proxy start so the
@@ -647,6 +709,7 @@ async function runPackageManagerSelfUpdate(manager) {
const started = startProxyDirectly();
// An ownership veto is the correct outcome, not a degraded one.
if (started === "vetoed") return;
+ if (started === "refused") return "refused";
if (started) console.warn(unsupervisedRuntimeWarning(bakePort));
return "unsupervised";
}
@@ -656,7 +719,7 @@ async function runPackageManagerSelfUpdate(manager) {
}
}
- const updateLease = acquireOwnershipMutationLease(serviceStatePaths);
+ let updateLease = acquireOwnershipMutationLease(serviceStatePaths);
delegatedOwnershipMutationToken = updateLease.token;
let updateLeaseReleased = false;
const releaseUpdateLease = () => {
@@ -665,15 +728,22 @@ async function runPackageManagerSelfUpdate(manager) {
delegatedOwnershipMutationToken = null;
updateLease.release();
};
+ const reacquireUpdateLease = () => {
+ if (!updateLeaseReleased) return;
+ updateLease = acquireOwnershipMutationLease(serviceStatePaths);
+ updateLeaseReleased = false;
+ delegatedOwnershipMutationToken = updateLease.token;
+ };
let res;
+ let stopAttempted = false;
let npmFailure = null;
try {
// Stop authority is decided under the same lease the child joins. A takeover between the
// earlier preflight and this boundary therefore blocks stop before it is sent.
const lockedOwnership = readOwnership();
const lockedPlan = planUpdateRuntimeHandling({ ...lockedOwnership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
- if (lockedOwnership.subjectToken !== initialOwnership.subjectToken || !lockedPlan.mayReplacePackage) {
+ if (!homePolicy.authorizeMutation("package") || lockedOwnership.subjectToken !== initialOwnership.subjectToken || !lockedPlan.mayReplacePackage) {
releaseUpdateLease();
console.error(lockedPlan.notice
?? "opencodex: update stopped because runtime ownership changed before stop authorization; rerun from the beginning.");
@@ -686,15 +756,19 @@ async function runPackageManagerSelfUpdate(manager) {
// semantics: graceful drain, service stop, native Codex restore). Gate on the service
// and the runtime-port record too: a service-managed or orphaned proxy can be live
// while ocx.pid is stale/missing.
- if (trayBeforeUpdate.stopBeforeReplacement) {
+ if (!homePolicy.packageOnly && trayBeforeUpdate.stopBeforeReplacement) {
console.log("⏹ Handing off the Windows tray before updating...");
try {
+ if (!homePolicy.authorizeMutation("tray")) { releaseUpdateLease(); process.exit(1); }
+ let stopAttempted = false;
handoffWindowsTrayForUpdate(trayBeforeUpdate, {
stop: () => {
- const stopped = runTrayLifecycle(launcher, "stop");
+ if (!homePolicy.authorizeMutation("tray")) throw new Error("Automatic update tray handoff refused.");
+ stopAttempted = true;
+ const stopped = runTrayLifecycle(launcher, "stop", homePolicy.authorizeMutation);
return { exitStatus: stopped.status, running: trayInstallState().running };
},
- start: () => runTrayLifecycle(launcher, "start"),
+ start: () => { if (stopAttempted && homePolicy.authorizeMutation("own-tray")) return runTrayLifecycle(launcher, "start", homePolicy.authorizeMutation); },
});
} catch {
releaseUpdateLease();
@@ -704,9 +778,10 @@ async function runPackageManagerSelfUpdate(manager) {
}
const hasRuntimeState =
existsSync(join(configDir(), "ocx.pid")) || existsSync(join(configDir(), "runtime-port.json"));
- let stopAttempted = false;
function recoverStoppedRuntimeAfterFailure(reason) {
+ authorityLauncher = postUpdateLauncher;
+ if (!homePolicy.authorizeMutation("runtime")) return;
const planRecovery = () => {
const recoveryOwnership = readOwnership();
const liveness = currentPackageRuntimeLiveness();
@@ -730,6 +805,7 @@ async function runPackageManagerSelfUpdate(manager) {
// lease, and holding the lease through the repair's health wait keeps that proxy from
// starting (#5760). Release it as the successful path does, then decide again.
releaseUpdateLease();
+ if (!homePolicy.authorizeMutation("runtime")) return;
({ liveness: recoveryLiveness, plan: recovery } = planRecovery());
}
if (recovery.reason === "desktop-supervised") {
@@ -759,7 +835,12 @@ async function runPackageManagerSelfUpdate(manager) {
// is the whole test here — the launcher cannot parse it, and `ocx stop` is what decides
// whether the obligation is safe to finish.
const hasPendingTeardown = hasPendingTeardownIn(readdirSync, configDir());
- const stopNeeded = serviceWasInstalled || hasRuntimeState || hasPendingTeardown;
+ if (homePolicy.packageOnly && hasPendingTeardown) {
+ releaseUpdateLease();
+ console.error("opencodex: pending teardown requires manual recovery before package replacement; run 'ocx stop' from the owning installation.");
+ process.exit(1);
+ }
+ const stopNeeded = !homePolicy.packageOnly && (serviceWasInstalled || hasRuntimeState || hasPendingTeardown);
if (stopNeeded && !runtimePlan.mayStopRuntime) {
releaseUpdateLease();
console.error(runtimePlan.notice
@@ -767,6 +848,7 @@ async function runPackageManagerSelfUpdate(manager) {
process.exit(1);
}
if (stopNeeded) {
+ if (!homePolicy.authorizeMutation("runtime")) { releaseUpdateLease(); process.exit(1); }
const preStopPlan = planUpdateRuntimeHandling({
...readOwnership(), serviceInstalled: serviceWasInstalled, supervision: observeSupervision(),
});
@@ -775,9 +857,16 @@ async function runPackageManagerSelfUpdate(manager) {
releaseUpdateLease();
process.exit(1);
}
+ if (!homePolicy.authorizeMutation("runtime")) { releaseUpdateLease(); process.exit(1); }
+ if (serviceWasInstalled && !homePolicy.serviceAllowed) {
+ console.warn("Service authority changed before proxy stop; check 'ocx service status' before retrying the update.");
+ releaseUpdateLease();
+ process.exit(1);
+ }
stopAttempted = true;
console.log("⏹ Stopping the running proxy before updating...");
- const stopRes = spawnSync(process.execPath, [launcher, "stop"], {
+ if (!homePolicy.authorizeMutation(serviceWasInstalled ? "service" : "runtime")) { releaseUpdateLease(); process.exit(1); }
+ const stopRes = guardedSpawnSync(homePolicy.authorizeMutation, serviceWasInstalled ? "service" : "runtime", process.execPath, [launcher, "stop"], {
stdio: "inherit", windowsHide: true, env: mutationChildEnvironment(),
});
const stillHasRuntimeState =
@@ -801,7 +890,7 @@ async function runPackageManagerSelfUpdate(manager) {
});
const historyOnlyStop = decision.reason === "history-only";
if (!decision.proceed) {
- if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
+ if (trayBeforeUpdate.restoreOnFailure && homePolicy.authorizeMutation("own-tray")) runTrayLifecycle(launcher, "start", homePolicy.authorizeMutation);
if (decision.reason === "teardown-outstanding") {
console.error("opencodex: a shared teardown from an earlier stop is still outstanding and needs manual review; aborting the update.");
console.error("opencodex: confirm no proxy is running, run 'ocx restore', then remove the pending-teardown file in the opencodex home.");
@@ -833,12 +922,12 @@ async function runPackageManagerSelfUpdate(manager) {
const replacementOwnership = readOwnership();
const replacementPlan = planUpdateRuntimeHandling({ ...replacementOwnership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
const replacementLiveness = currentPackageRuntimeLiveness();
- if (replacementOwnership.subjectToken !== initialOwnership.subjectToken
+ if (!homePolicy.authorizeMutation("package") || replacementOwnership.subjectToken !== initialOwnership.subjectToken
|| !replacementPlan.mayReplacePackage
|| replacementLiveness !== "dead") {
recoverStoppedRuntimeAfterFailure("replacement was refused");
releaseUpdateLease();
- if (trayBeforeUpdate.restoreOnFailure) runTrayLifecycle(launcher, "start");
+ if (trayBeforeUpdate.restoreOnFailure && homePolicy.authorizeMutation("own-tray")) runTrayLifecycle(launcher, "start", homePolicy.authorizeMutation);
console.error(replacementPlan.notice
?? "opencodex: update stopped because runtime ownership or liveness changed after the stop decision; rerun from the beginning.");
process.exit(1);
@@ -852,19 +941,30 @@ async function runPackageManagerSelfUpdate(manager) {
if (manager === "npm") {
const packageDir = resolve(here, "..");
const transactionalNpmUpdate = process.platform === "win32" ? transactionalNpmUpdateAsync : transactionalNpmUpdateSync;
+ if (!homePolicy.authorizeMutation("package")) throw new Error("Automatic package update refused.");
const tx = await transactionalNpmUpdate({
packageDir,
pkgName: PKG,
targetVersion: latest || undefined,
tag,
cachePath: npmCachePath,
+ mayReplacePackage: () => {
+ const ownership = readOwnership();
+ const plan = planUpdateRuntimeHandling({ ...ownership, serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
+ const liveness = currentPackageRuntimeLiveness();
+ return homePolicy.authorizeMutation("package") && ownership.subjectToken === initialOwnership.subjectToken
+ && plan.mayReplacePackage && liveness === "dead";
+ },
runNpm: (args) => {
const invocation = npmInvocation(args);
+ if (!homePolicy.authorizeMutation("package")) return { status: 1 };
if (process.platform === "win32") return runNpmInstall(invocation, {
+ authorizeMutation: homePolicy.authorizeMutation,
env: unprivilegedOwnershipMutationEnvironment(invocation?.options?.env ?? process.env),
});
if (!invocation) return { status: 1 };
- return spawnSync(invocation.file, invocation.args, {
+ if (!homePolicy.authorizeMutation("package")) return { status: 1 };
+ return guardedSpawnSync(homePolicy.authorizeMutation, "package", invocation.file, invocation.args, {
...invocation.options,
stdio: "inherit",
timeout: 180000,
@@ -878,7 +978,7 @@ async function runPackageManagerSelfUpdate(manager) {
if (!tx.ok) npmFailure = tx;
if (tx.ok) {
res = { status: 0 };
- } else if (tx.phase === "stage" || tx.phase === "verify") {
+ } else if (tx.phase === "stage" || tx.phase === "verify" || tx.phase === "authorize") {
// Live tree untouched: report and stop. Nothing to roll back.
console.error(`opencodex: update aborted before touching the live install (${tx.phase}): ${tx.error}`);
res = { status: 1 };
@@ -887,6 +987,7 @@ async function runPackageManagerSelfUpdate(manager) {
res = { status: 1 };
}
} else {
+ if (!homePolicy.authorizeMutation("package")) throw new Error("Automatic package update refused.");
const update = runPnpmGlobalUpdate({
packageName: PKG,
currentVersion: current,
@@ -897,7 +998,7 @@ async function runPackageManagerSelfUpdate(manager) {
runPnpm: (args, capture = false) => {
const invocation = pnpmOwnerInvocation(owner, args);
if (!invocation) return { status: 1 };
- return withPnpmCommandCwd(args, cwd => spawnSync(invocation.file, invocation.args, {
+ return withPnpmCommandCwd(args, cwd => homePolicy.authorizeMutation("package") ? guardedSpawnSync(homePolicy.authorizeMutation, "package", invocation.file, invocation.args, {
...invocation.options,
stdio: capture ? "pipe" : "inherit",
encoding: "utf8",
@@ -907,7 +1008,7 @@ async function runPackageManagerSelfUpdate(manager) {
// keep a cwd handle inside the package Windows is replacing.
cwd,
env: pnpmReadEnvironment(unprivilegedOwnershipMutationEnvironment(invocation.env ?? process.env)),
- }));
+ }) : { status: 1 });
},
log: line => console.log(line),
});
@@ -944,31 +1045,63 @@ async function runPackageManagerSelfUpdate(manager) {
}
const postInstallPlan = planUpdateRuntimeHandling({ ...readOwnership(), serviceInstalled: serviceWasInstalled, supervision: observeSupervision() });
if (res.status === 0) {
- console.log(`\nUpdated${latest ? ` to v${latest}` : ""}.`);
+ authorityLauncher = postUpdateLauncher;
+ if (homePolicy.packageOnly) {
+ if (!homePolicy.authorizeMutation("package-publish")) process.exit(1);
+ guardedRun(homePolicy.authorizeMutation, "package-publish", console.log, [`\nUpdated${latest ? ` to v${latest}` : ""}.`]);
+ homePolicy.check(homePolicy.initial);
+ console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, bakePort)}`);
+ if (!homePolicy.authorizeMutation("package-publish")) process.exit(1);
+ guardedExit(homePolicy.authorizeMutation, "package-publish", process.argv.includes("--update-worker-result") ? UPDATE_PACKAGE_ONLY_EXIT_CODE : 0);
+ }
+ const restoreOwnStoppedTray = () => {
+ if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable && existsSync(postUpdateLauncher)) {
+ try { if (homePolicy.authorizeMutation("own-tray")) runTrayLifecycle(postUpdateLauncher, "start", homePolicy.authorizeMutation); } catch { /* preserve the authority refusal */ }
+ }
+ };
+ if (!homePolicy.authorizeMutation("publish")) {
+ restoreOwnStoppedTray();
+ process.exit(1);
+ }
+ guardedRun(homePolicy.authorizeMutation, "publish", console.log, [`\nUpdated${latest ? ` to v${latest}` : ""}.`]);
if (!postInstallPlan.mayStopRuntime) console.warn("Runtime authority does not permit CLI recovery; no service was refreshed or proxy started.");
- repairCodexShimIfNeeded(postUpdateLauncher);
+ if (!homePolicy.authorizeMutation("shim") || repairCodexShimIfNeeded(postUpdateLauncher, homePolicy.authorizeMutation) === false) { restoreOwnStoppedTray(); process.exit(1); }
if (trayBeforeUpdate.refreshAfterReplacement) {
- const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayBeforeUpdate.installArgs], {
+ if (!homePolicy.authorizeMutation("tray")) {
+ restoreOwnStoppedTray();
+ process.exit(1);
+ }
+ const tray = guardedSpawnSync(homePolicy.authorizeMutation, "tray", process.execPath, [postUpdateLauncher, ...trayBeforeUpdate.installArgs], {
stdio: "inherit",
windowsHide: true,
});
if (tray.status !== 0) {
console.warn("opencodex: Windows tray refresh failed. Run: ocx tray install");
- if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable) runTrayLifecycle(postUpdateLauncher, "start");
+ if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable && homePolicy.authorizeMutation("own-tray")) runTrayLifecycle(postUpdateLauncher, "start", homePolicy.authorizeMutation);
}
}
// The stop above unloaded any managed service; refresh via the freshly-installed
// launcher so the new files write the baked paths and the service restarts.
if (postInstallPlan.mayRestoreService) {
console.log("Refreshing the background service with the updated files...");
- const refreshed = refreshBackgroundServiceOrStartDirect();
- if (refreshed === "unsupervised" || refreshed === "unsettled") process.exit(1);
+ let refreshed;
+ try { refreshed = refreshBackgroundServiceOrStartDirect(); }
+ finally { releaseUpdateLease(); }
+ if (refreshed === "unsupervised" || refreshed === "unsettled" || refreshed === "refused") process.exit(1);
} else if (postInstallPlan.mayStopRuntime) {
- console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, bakePort)}`);
+ if (!homePolicy.authorizeMutation("runtime")) process.exit(1);
+ authorityLauncher = postUpdateLauncher;
+ if (stopAttempted) {
+ let started;
+ try { started = startProxyDirectly(); }
+ finally { releaseUpdateLease(); }
+ if (started !== true && started !== "vetoed") process.exit(1);
+ } else console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, bakePort)}`);
}
- process.exit(0);
+ if (!homePolicy.authorizeMutation("publish")) process.exit(1);
+ guardedExit(homePolicy.authorizeMutation, "publish", 0);
}
- if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable) runTrayLifecycle(postUpdateLauncher, "start");
+ if (trayBeforeUpdate.restoreOnFailure && postUpdateLauncherUsable && homePolicy.authorizeMutation("own-tray")) runTrayLifecycle(postUpdateLauncher, "start", homePolicy.authorizeMutation);
if (npmFailure) {
// Phase-specific next step (#5624): whether the previous version is still in place decides
// between "retry" and "restore", and a bare reinstall must follow a stop (#5496).
@@ -987,10 +1120,11 @@ async function runPackageManagerSelfUpdate(manager) {
process.exit(1);
}
-function bunBinDir() {
+function bunBinDir(launcherPath) {
// Resolve the `bun` dependency's directory without hardcoding the platform
// package — the package manager's os/cpu/libc resolution already picked the right @oven/bun-*.
- return dirname(require.resolve("bun/package.json"));
+ const resolver = createRequire(launcherPath || import.meta.url);
+ return dirname(resolver.resolve("bun/package.json"));
}
const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
@@ -1039,7 +1173,7 @@ function fail(msg) {
process.exit(1);
}
-function resolveBun({ allowInstall = true } = {}) {
+function resolveBun({ allowInstall = true, launcherPath, allowMissing = false } = {}) {
// Keep direct package-launcher starts aligned with durable service/shim installs:
// a valid explicit runtime must win even when the bundled dependency exists.
const override = process.env[BUN_OVERRIDE_ENV]?.trim();
@@ -1053,7 +1187,7 @@ function resolveBun({ allowInstall = true } = {}) {
let bunDir = null;
try {
- bunDir = bunBinDir();
+ bunDir = bunBinDir(launcherPath);
} catch { /* Missing dependency can still fall back to a validated PATH Bun. */ }
let bin = bunDir ? findBunBinary(bunDir) : null;
@@ -1063,7 +1197,7 @@ function resolveBun({ allowInstall = true } = {}) {
// ~450-byte placeholder stub. Run the bun package's own installer once.
const installJs = bunDir ? join(bunDir, "install.js") : null;
if (allowInstall && installJs && existsSync(installJs)) {
- const r = spawnSync(process.execPath, [installJs], { stdio: "inherit" });
+ const r = probeSpawnSync(process.execPath, [installJs], { stdio: "inherit" });
if (r.status === 0) bin = findBunBinary(bunDir);
}
if (bin) return { path: bin, source: "bundled", version: bundledBunVersion(bunDir) };
@@ -1071,11 +1205,12 @@ function resolveBun({ allowInstall = true } = {}) {
// Reached only when the bundled runtime is unusable. The two probes start the candidate Bun
// cold, and on Windows a first run of a copied bun.exe is often held by an on-access scan for
// well over a second, so the total budget is generous rather than interactive-tight.
- const pathBun = findPathBun({ pinnedVersion: pinnedBunVersion(), deadlineMs: PATH_BUN_PROBE_BUDGET_MS });
+ const pathBun = findPathBun({ pinnedVersion: pinnedBunVersion(launcherPath), deadlineMs: PATH_BUN_PROBE_BUDGET_MS });
if (pathBun) {
console.error(`opencodex: using PATH Bun ${pathBun.version}.`);
return { path: pathBun.path, source: "process", version: pathBun.version };
}
+ if (allowMissing) return null;
fail(bunDir ? "Bun binary missing after install attempt." : "the `bun` dependency is not installed.");
}
@@ -1090,7 +1225,24 @@ if (updateHelpRequested) {
process.exit(0);
}
+// This intercept precedes Desktop handoff, boot restore and install-capable Bun resolution.
+// Capability only: no Bun lookup, home inspection, dotenv or ordinary bootstrap.
+if (process.argv[2] === "__update-package-only-probe") {
+ if (process.argv.length !== 3) process.exit(2);
+ console.log(JSON.stringify({ schemaVersion: 1, packageOnlyProtocolVersion: UPDATE_PACKAGE_ONLY_PROTOCOL_VERSION }));
+ process.exit(0);
+}
+if (process.argv[2] === "__update-home-probe") {
+ if (!isUpdateHomeProbe(process.argv.slice(2))) process.exit(1);
+ console.log(JSON.stringify({ schemaVersion: 1, ...readNodeUpdateHome(fileURLToPath(import.meta.url)), launcherProtocolVersion: UPDATE_HOME_LAUNCHER_PROTOCOL }));
+ process.exit(0);
+}
+
const codexCliUpdateInspection = isCodexCliUpdateInspectionArgv(process.argv);
+// Windows inspection currently refuses before candidate I/O, so a long-lived
+// Windows server does not duplicate PATH into its inherited environment block.
+const codexCliUpdateInspectionContext = codexCliUpdateInspection
+ || (process.platform !== "win32" && carriesCodexCliUpdateInspectionContext(process.argv));
if (codexCliUpdateInspection && typeof process.versions.bun === "string") {
console.error("opencodex: codex-cli-update inspection must use the published Node launcher.");
process.exit(1);
@@ -1116,7 +1268,7 @@ if (desktopPlan.kind === "handoff") {
}
if (result.kind === "exit") {
if (result.signal) {
- process.kill(process.pid, result.signal);
+ relayOwnSignal(result.signal);
} else {
process.exit(result.code);
}
@@ -1222,7 +1374,7 @@ function createNodeLaunchContext() {
version: 1,
proof: launchProof,
anthropicEnvSlots: preBunAnthropicSlots,
- codexCliInspectionEnv: codexCliUpdateInspection ? {
+ codexCliInspectionEnv: codexCliUpdateInspectionContext ? {
codexCliPath: preBunCodexCliPath,
path: preBunPath,
pathExt: preBunPathExt,
@@ -1272,7 +1424,7 @@ function createNodeLaunchContext() {
return { launchProof, launchContext, inheritedEnv };
}
-const child = spawn(bun, [cliPath, `${NODE_LAUNCH_PROOF_PREFIX}${launchProof}`, ...process.argv.slice(2).filter(arg => !arg.startsWith(NODE_LAUNCH_PROOF_PREFIX))], {
+const child = probeSpawn(bun, [cliPath, `${NODE_LAUNCH_PROOF_PREFIX}${launchProof}`, ...process.argv.slice(2).filter(arg => !arg.startsWith(NODE_LAUNCH_PROOF_PREFIX))], {
stdio: "inherit",
// A headless Windows parent (Task Scheduler, dashboard restart, shortcut) has no
// console to inherit. Without this flag Windows allocates a visible console for
@@ -1314,7 +1466,7 @@ child.on("exit", (code, signal) => {
clearHandlers();
// Mirror the child's terminating signal/exit code so this launcher's status matches.
if (signal) {
- process.kill(process.pid, signal);
+ relayOwnSignal(signal);
return;
}
process.exit(code ?? 1);
diff --git a/bun.lock b/bun.lock
index fd75af84840..cdfea17848a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,7 +12,7 @@
"zod": "4.6.6",
},
"devDependencies": {
- "@types/bun": "1.4.0",
+ "@types/bun": "1.4.4",
"typescript": "7.0.2",
},
"optionalDependencies": {
@@ -88,7 +88,7 @@
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-a3LNctBVO8PjePsFZfwDleF2JXYf2BfQ1u/PLRnBDLnC2Riml+4+LgJpKj4xRGhB92KweGyQI5PXaTkP9qypMQ=="],
- "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
+ "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="],
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
@@ -142,7 +142,7 @@
"bun": ["bun@1.4.4", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.4", "@oven/bun-darwin-x64": "1.4.4", "@oven/bun-freebsd-aarch64": "1.4.4", "@oven/bun-freebsd-x64": "1.4.4", "@oven/bun-linux-aarch64": "1.4.4", "@oven/bun-linux-aarch64-android": "1.4.4", "@oven/bun-linux-aarch64-musl": "1.4.4", "@oven/bun-linux-x64": "1.4.4", "@oven/bun-linux-x64-android": "1.4.4", "@oven/bun-linux-x64-musl": "1.4.4", "@oven/bun-windows-aarch64": "1.4.4", "@oven/bun-windows-x64": "1.4.4" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-hj0ASdG6PNjLZGFAXktWb3Uaj/dcv/1wCsIttoEXADNIQBsGjTr+5uLKYbrDkuc7NT0Tkj4yPIqJI2jDxXdvVg=="],
- "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
+ "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index e76d2f47ac1..ed14eef8e8a 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -2645,7 +2645,7 @@ dependencies = [
[[package]]
name = "opencodex-desktop"
-version = "2.93.0"
+version = "2.94.0"
dependencies = [
"base64 0.22.1",
"dbus",
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index e721358a158..322c1d8f7a4 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "opencodex-desktop"
-version = "2.93.0"
+version = "2.94.0"
description = "OpenCodex desktop shell"
authors = ["OpenCodex contributors"]
license = "MIT"
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 255954ed767..ed610844745 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -48,6 +48,9 @@ mod tray_login;
mod popup_portable_test;
mod process_spawn;
mod proxy;
+mod recovery;
+mod recovery_activity;
+mod recovery_presence;
mod resolve;
mod runtime_stop;
mod sidecar;
@@ -419,6 +422,7 @@ pub fn run() {
window::set_tray_policy(app.handle(), false);
}
+ recovery::register(app.handle());
startup::begin(app.handle());
if !cfg!(debug_assertions) {
diff --git a/desktop/src-tauri/src/recovery.rs b/desktop/src-tauri/src/recovery.rs
new file mode 100644
index 00000000000..df60a6149ea
--- /dev/null
+++ b/desktop/src-tauri/src/recovery.rs
@@ -0,0 +1,92 @@
+//! Publish the shell's current intent independently of the supervised runtime's lifetime.
+use crate::{
+ exit::{ExitCoordinator, ExitPhase},
+ recovery_activity::{Activity, Reservation},
+ recovery_presence::{Presence, Snapshot},
+};
+use std::{
+ path::Path,
+ sync::{Arc, Mutex},
+};
+use tauri::{AppHandle, Manager};
+
+struct Recovery {
+ home: Arc>>,
+ activity: Arc,
+ _presence: Presence,
+}
+
+/// Register before startup can resolve or spawn. A failure grants no consumer-side authority.
+pub fn register(app: &AppHandle) {
+ let home = Arc::new(Mutex::new(None));
+ let observed_home = Arc::clone(&home);
+ let activity = Arc::new(Activity::default());
+ let observed_activity = Arc::clone(&activity);
+ let handle = app.clone();
+ let snapshot = Arc::new(move || {
+ let state = handle
+ .try_state::()
+ .map(|exit| exit.supervision());
+ Snapshot {
+ home: observed_home.lock().ok().and_then(|home| home.clone()),
+ wanted: observed_activity.recovering(state.is_some_and(|s| {
+ s.wanted
+ && !s.reason_set
+ && matches!(s.phase, ExitPhase::Idle | ExitPhase::Spawning)
+ })),
+ }
+ });
+ match Presence::start(uuid::Uuid::new_v4().simple().to_string(), snapshot) {
+ Ok(presence) => {
+ app.manage(Recovery {
+ home,
+ activity,
+ _presence: presence,
+ });
+ }
+ Err(_) => crate::logging::log_once(
+ "Desktop recovery evidence is unavailable",
+ "the native listener could not be registered",
+ ),
+ }
+}
+
+fn update_activity(app: &AppHandle, update: impl FnOnce(&Activity)) {
+ if let Some(recovery) = app.try_state::() {
+ update(&recovery.activity);
+ }
+}
+
+pub fn begin_run(app: &AppHandle, generation: u64, recovering: bool) {
+ update_activity(app, |activity| activity.begin_run(generation, recovering));
+}
+
+pub fn ready(app: &AppHandle, generation: u64, owned: bool) {
+ update_activity(app, |activity| activity.ready(generation, owned));
+}
+
+pub fn failed(app: &AppHandle, generation: u64) {
+ update_activity(app, |activity| activity.failed(generation));
+}
+
+pub fn defer_recovery(app: &AppHandle) {
+ update_activity(app, Activity::defer_recovery);
+}
+
+pub fn finish_run(app: &AppHandle, generation: u64, ready: bool) {
+ update_activity(app, |activity| activity.finish_run(generation, ready));
+}
+
+pub fn reserve(app: &AppHandle) -> Option {
+ app.try_state::()
+ .and_then(|recovery| recovery.activity.reserve())
+}
+
+/// Only the bundled CLI's resolved home selects the scope; neither request bytes nor a locator do.
+pub fn bind_home(app: &AppHandle, home: &Path) {
+ if let Some(recovery) = app.try_state::() {
+ if let Ok(mut current) = recovery.home.lock() {
+ *current = Some(home.to_path_buf());
+ }
+ }
+}
diff --git a/desktop/src-tauri/src/recovery_activity.rs b/desktop/src-tauri/src/recovery_activity.rs
new file mode 100644
index 00000000000..b3d078287b3
--- /dev/null
+++ b/desktop/src-tauri/src/recovery_activity.rs
@@ -0,0 +1,256 @@
+//! Recovery work is narrower than the person's general wish to have a runtime.
+use std::sync::{Arc, Mutex};
+
+#[derive(Clone, Copy, Default)]
+enum Run {
+ #[default]
+ Idle,
+ Starting {
+ retry: bool,
+ },
+ Failed {
+ retry: bool,
+ },
+ Ready {
+ owned: bool,
+ },
+ Supervised,
+}
+
+#[derive(Default)]
+struct Inner {
+ generation: u64,
+ run: Run,
+ reservations: usize,
+}
+
+/// One view shared by accepted startup reports and the supervisor's actual work.
+#[derive(Default)]
+pub struct Activity {
+ inner: Mutex,
+}
+
+impl Activity {
+ pub fn begin_run(&self, generation: u64, recovering: bool) {
+ if let Ok(mut inner) = self.inner.lock() {
+ if generation > inner.generation {
+ inner.generation = generation;
+ inner.run = Run::Starting { retry: recovering };
+ }
+ }
+ }
+
+ fn update_run(&self, generation: u64, update: impl FnOnce(&mut Run)) {
+ if let Ok(mut inner) = self.inner.lock() {
+ if inner.generation == generation {
+ update(&mut inner.run);
+ }
+ }
+ }
+
+ /// Only an accepted Ready report supplies the binding's confirmed ownership.
+ pub fn ready(&self, generation: u64, owned: bool) {
+ self.update_run(generation, |run| {
+ if matches!(run, Run::Starting { .. }) {
+ *run = Run::Ready { owned };
+ }
+ });
+ }
+
+ /// A terminal launch failure loses credit even while its async cleanup is running.
+ pub fn failed(&self, generation: u64) {
+ self.update_run(generation, |run| {
+ if let Run::Starting { retry } = *run {
+ *run = Run::Failed { retry };
+ }
+ });
+ }
+
+ /// An owned child's exit during a run gives that run a real follow-up obligation.
+ pub fn defer_recovery(&self) {
+ if let Ok(mut inner) = self.inner.lock() {
+ match &mut inner.run {
+ Run::Starting { retry } | Run::Failed { retry } => *retry = true,
+ _ => {}
+ }
+ }
+ }
+
+ /// Failed runs finish only after their follow-up has acquired any new reservation.
+ pub fn finish_run(&self, generation: u64, ready: bool) {
+ self.update_run(generation, |run| {
+ *run = if ready && matches!(run, Run::Ready { owned: true } | Run::Supervised) {
+ Run::Supervised
+ } else {
+ Run::Idle
+ };
+ });
+ }
+
+ /// Keep the scheduled work alive across pending -> startup, including cancellation.
+ pub fn reserve(self: &Arc) -> Option {
+ let mut inner = self.inner.lock().ok()?;
+ inner.reservations = inner.reservations.checked_add(1)?;
+ Some(Reservation(Arc::clone(self)))
+ }
+
+ /// The exit coordinator can revoke every kind of work without waiting for its cleanup.
+ pub fn recovering(&self, allowed: bool) -> bool {
+ allowed
+ && self.inner.lock().is_ok_and(|inner| {
+ inner.reservations > 0
+ || matches!(
+ inner.run,
+ Run::Starting { .. }
+ | Run::Failed { retry: true }
+ | Run::Ready { owned: true }
+ | Run::Supervised
+ )
+ })
+ }
+}
+
+/// Each task releases only its own reservation, including an early return or cancellation.
+pub struct Reservation(Arc);
+
+impl Drop for Reservation {
+ fn drop(&mut self) {
+ if let Ok(mut inner) = self.0.inner.lock() {
+ inner.reservations -= 1;
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn an_accepted_start_covers_the_resolved_home_before_spawn() {
+ let activity = Activity::default();
+ assert!(!activity.recovering(true));
+ activity.begin_run(1, false);
+ assert!(activity.recovering(true));
+ }
+
+ #[test]
+ fn a_terminal_launch_failure_stops_credit_before_cleanup_finishes() {
+ let activity = Activity::default();
+ activity.begin_run(1, false);
+ assert!(activity.recovering(true));
+ // Both an ordinary failure and the deadline guard publish this same transition.
+ activity.failed(1);
+ assert!(!activity.recovering(true));
+ activity.finish_run(1, false);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn guest_ready_does_not_claim_recovery_while_its_watchdog_waits() {
+ let activity = Arc::new(Activity::default());
+ activity.begin_run(1, false);
+ activity.ready(1, false);
+ assert!(!activity.recovering(true));
+ activity.finish_run(1, true);
+ assert!(!activity.recovering(true));
+ // The guest watchdog may later schedule actual work.
+ let reservation = activity.reserve().unwrap();
+ assert!(activity.recovering(true));
+ drop(reservation);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn confirmed_own_child_attachment_stays_supervised_after_ready() {
+ let activity = Arc::new(Activity::default());
+ activity.begin_run(1, false);
+ activity.ready(1, true);
+ activity.finish_run(1, true);
+ assert!(activity.recovering(true));
+ // Releasing the dead child's handle cannot erase this accepted Ready ownership.
+ let reservation = activity.reserve().unwrap();
+ activity.begin_run(2, true);
+ drop(reservation);
+ assert!(activity.recovering(true));
+ }
+
+ #[test]
+ fn recovery_failure_transfers_credit_through_backoff_to_the_next_run() {
+ let activity = Arc::new(Activity::default());
+ activity.begin_run(1, true);
+ activity.failed(1);
+ assert!(activity.recovering(true));
+ let reservation = activity.reserve().unwrap();
+ activity.finish_run(1, false);
+ assert!(activity.recovering(true));
+ // The timer has cleared pending, but has not called begin_with yet.
+ assert!(activity.recovering(true));
+ activity.begin_run(2, true);
+ drop(reservation);
+ assert!(activity.recovering(true));
+ }
+
+ #[test]
+ fn a_deferred_owned_exit_preserves_only_the_follow_up_obligation() {
+ let activity = Arc::new(Activity::default());
+ activity.begin_run(1, false);
+ activity.defer_recovery();
+ activity.failed(1);
+ assert!(activity.recovering(true));
+ let reservation = activity.reserve().unwrap();
+ activity.finish_run(1, false);
+ assert!(activity.recovering(true));
+ drop(reservation);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn a_held_or_cancelled_retry_without_scheduled_work_loses_credit() {
+ let activity = Activity::default();
+ activity.begin_run(1, true);
+ activity.failed(1);
+ assert!(activity.recovering(true));
+ activity.finish_run(1, false);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn an_old_terminal_report_or_finalizer_cannot_clear_a_new_start() {
+ let activity = Activity::default();
+ activity.begin_run(1, false);
+ activity.failed(1);
+ activity.begin_run(2, false);
+ activity.ready(1, false);
+ activity.failed(1);
+ activity.finish_run(1, false);
+ assert!(activity.recovering(true));
+ activity.ready(2, false);
+ activity.begin_run(1, true);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn cancelling_an_older_task_does_not_release_a_newer_reservation() {
+ let activity = Arc::new(Activity::default());
+ let older = activity.reserve().unwrap();
+ let newer = activity.reserve().unwrap();
+ drop(older);
+ assert!(activity.recovering(true));
+ drop(newer);
+ assert!(!activity.recovering(true));
+ }
+
+ #[test]
+ fn exit_revocation_overrides_startup_owned_and_reserved_work() {
+ let activity = Arc::new(Activity::default());
+ activity.begin_run(1, false);
+ assert!(!activity.recovering(false));
+ activity.ready(1, true);
+ activity.finish_run(1, true);
+ assert!(!activity.recovering(false));
+ let _reservation = activity.reserve().unwrap();
+ activity.begin_run(2, true);
+ activity.failed(2);
+ assert!(!activity.recovering(false));
+ }
+}
diff --git a/desktop/src-tauri/src/recovery_presence.rs b/desktop/src-tauri/src/recovery_presence.rs
new file mode 100644
index 00000000000..f4b668c9a4f
--- /dev/null
+++ b/desktop/src-tauri/src/recovery_presence.rs
@@ -0,0 +1,429 @@
+//! Read-only, process-lifetime recovery evidence with no persistent discovery record.
+//! Consumers must independently identify its executable and the accepted socket's owner.
+
+use std::{
+ fs,
+ io::{self, Read, Write},
+ net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream},
+ path::PathBuf,
+ sync::{
+ atomic::{AtomicBool, AtomicUsize, Ordering},
+ Arc,
+ },
+ thread,
+ time::{Duration, Instant},
+};
+
+const DOMAIN: &str = "ocx-desktop-recovery-v1";
+const CONNECTION_BUDGET: Duration = Duration::from_secs(3);
+const MAX_CONNECTIONS: usize = 4;
+const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(50);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Snapshot {
+ /// None until the bundled CLI has resolved this shell's configuration home.
+ pub home: Option,
+ /// Stop, Quit and update drain revoke this directly from the exit coordinator.
+ pub wanted: bool,
+}
+
+pub struct Presence {
+ stopped: Arc,
+ address: SocketAddr,
+}
+
+fn hex(bytes: &[u8]) -> String {
+ const DIGITS: &[u8; 16] = b"0123456789abcdef";
+ bytes
+ .iter()
+ .flat_map(|b| {
+ [
+ DIGITS[(b >> 4) as usize] as char,
+ DIGITS[(b & 15) as usize] as char,
+ ]
+ })
+ .collect()
+}
+
+fn token(text: &str, length: usize) -> bool {
+ text.len() == length
+ && text
+ .bytes()
+ .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
+}
+
+fn answer(request: &str, instance: &str, pid: u32, snapshot: Snapshot) -> Option {
+ let nonce = request
+ .strip_prefix(&format!("{DOMAIN} "))?
+ .strip_suffix('\n')?;
+ if !token(nonce, 64) {
+ return None;
+ }
+ let home = snapshot
+ .home
+ .and_then(|path| fs::canonicalize(path).ok())
+ .and_then(|path| path.to_str().map(str::to_owned));
+ let (state, home) = match home {
+ Some(home) if home.len() <= 4096 => (
+ if snapshot.wanted {
+ "recovering"
+ } else {
+ "stopped"
+ },
+ hex(home.as_bytes()),
+ ),
+ _ => ("unknown", "-".to_owned()),
+ };
+ Some(format!(
+ "{DOMAIN} {nonce} {instance} {pid} {state} {home}\n"
+ ))
+}
+
+fn serve(mut stream: TcpStream, instance: &str, snapshot: &dyn Fn() -> Snapshot) -> io::Result<()> {
+ if !stream.peer_addr()?.ip().is_loopback() {
+ return Ok(());
+ }
+ stream.set_write_timeout(Some(Duration::from_secs(1)))?;
+ let deadline = Instant::now() + CONNECTION_BUDGET;
+ // Two fresh challenges let the client bracket its OS observations without releasing the
+ // accepted socket. Nothing serves HTTP, mutates state or accepts a reusable credential.
+ for _ in 0..2 {
+ let mut request = Vec::new();
+ loop {
+ let remaining = deadline.saturating_duration_since(Instant::now());
+ if remaining.is_zero() {
+ return Ok(());
+ }
+ stream.set_read_timeout(Some(remaining))?;
+ let mut byte = [0_u8; 1];
+ if stream.read(&mut byte)? == 0 {
+ return Ok(());
+ }
+ request.push(byte[0]);
+ if request.len() > 128 {
+ return Ok(());
+ }
+ if byte[0] == b'\n' {
+ break;
+ }
+ }
+ let Some(response) = std::str::from_utf8(&request)
+ .ok()
+ .and_then(|request| answer(request, instance, std::process::id(), snapshot()))
+ else {
+ return Ok(());
+ };
+ stream.write_all(response.as_bytes())?;
+ }
+ // Retain ownership of the exact accepted socket while the client takes its final snapshot.
+ let remaining = deadline.saturating_duration_since(Instant::now());
+ if !remaining.is_zero() {
+ stream.set_read_timeout(Some(remaining))?;
+ let _ = stream.read(&mut [0_u8; 1]);
+ }
+ Ok(())
+}
+
+impl Presence {
+ pub fn start(
+ instance: String,
+ snapshot: Arc Snapshot + Send + Sync>,
+ ) -> io::Result {
+ Self::start_with_accept(instance, snapshot, TcpListener::accept)
+ }
+
+ fn start_with_accept(
+ instance: String,
+ snapshot: Arc Snapshot + Send + Sync>,
+ mut accept: impl FnMut(&TcpListener) -> io::Result<(TcpStream, SocketAddr)> + Send + 'static,
+ ) -> io::Result {
+ if !token(&instance, 32) {
+ return Err(io::ErrorKind::InvalidInput.into());
+ }
+ let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
+ let address = listener.local_addr()?;
+ let stopped = Arc::new(AtomicBool::new(false));
+ let stop = Arc::clone(&stopped);
+ let presence = Self { stopped, address };
+ thread::Builder::new()
+ .name("desktop-recovery".into())
+ .spawn(move || {
+ let active = Arc::new(AtomicUsize::new(0));
+ while !stop.load(Ordering::Acquire) {
+ match accept(&listener) {
+ Ok((stream, _)) => {
+ if stop.load(Ordering::Acquire) {
+ break;
+ }
+ if active.load(Ordering::Acquire) >= MAX_CONNECTIONS {
+ continue;
+ }
+ active.fetch_add(1, Ordering::AcqRel);
+ let connection_active = Arc::clone(&active);
+ let snapshot = Arc::clone(&snapshot);
+ let instance = instance.clone();
+ if thread::Builder::new()
+ .name("desktop-recovery-read".into())
+ .spawn(move || {
+ let _ = serve(stream, &instance, snapshot.as_ref());
+ connection_active.fetch_sub(1, Ordering::AcqRel);
+ })
+ .is_err()
+ {
+ active.fetch_sub(1, Ordering::AcqRel);
+ }
+ }
+ Err(_) => {
+ // Retain the listener after transient aborts or resource exhaustion.
+ // Pace all errors, including interruptions, then recheck stop.
+ thread::sleep(ACCEPT_ERROR_BACKOFF);
+ }
+ }
+ }
+ })?;
+ Ok(presence)
+ }
+}
+
+impl Drop for Presence {
+ fn drop(&mut self) {
+ self.stopped.store(true, Ordering::Release);
+ // Wake the blocking accept without an idle polling timer or an unbounded shutdown wait.
+ let _ = TcpStream::connect_timeout(&self.address, Duration::from_millis(50));
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::{
+ io::{BufRead, BufReader},
+ sync::{mpsc, Mutex},
+ };
+
+ struct Finished(mpsc::Sender<()>);
+
+ impl Drop for Finished {
+ fn drop(&mut self) {
+ let _ = self.0.send(());
+ }
+ }
+
+ fn unresolved_snapshot() -> Snapshot {
+ Snapshot {
+ home: None,
+ wanted: true,
+ }
+ }
+
+ fn challenge(stream: &mut TcpStream) -> io::Result {
+ stream.set_read_timeout(Some(Duration::from_secs(1)))?;
+ stream.write_all(format!("{DOMAIN} {}\n", "b".repeat(64)).as_bytes())?;
+ let mut response = String::new();
+ BufReader::new(stream).read_line(&mut response)?;
+ Ok(response)
+ }
+
+ #[test]
+ fn accept_errors_retry_before_a_real_connection() {
+ for kind in [
+ io::ErrorKind::ConnectionAborted,
+ io::ErrorKind::Other,
+ io::ErrorKind::Interrupted,
+ ] {
+ let (injected, observed) = mpsc::channel();
+ let (finished, completion) = mpsc::channel();
+ let finished = Finished(finished);
+ let mut first = true;
+ let presence = Presence::start_with_accept(
+ "a".repeat(32),
+ Arc::new(unresolved_snapshot),
+ move |listener| {
+ let _ = &finished;
+ if first {
+ first = false;
+ let _ = injected.send(());
+ return Err(kind.into());
+ }
+ listener.accept()
+ },
+ )
+ .unwrap();
+ observed.recv_timeout(Duration::from_secs(1)).unwrap();
+ // Only the error is injected; the next connection and response use the real socket.
+ let response =
+ TcpStream::connect(presence.address).and_then(|mut stream| challenge(&mut stream));
+ drop(presence);
+ completion.recv_timeout(Duration::from_secs(1)).unwrap();
+ let response = response.unwrap_or_else(|error| panic!("{kind:?}: {error}"));
+ assert!(response.starts_with(&format!("{DOMAIN} {} ", "b".repeat(64))));
+ assert!(response.ends_with(" unknown -\n"));
+ }
+ }
+
+ fn assert_persistent_accept_error_is_paced_and_stops(kind: io::ErrorKind) {
+ let (attempted, attempts) = mpsc::channel();
+ let (finished, completion) = mpsc::channel();
+ let finished = Finished(finished);
+ let mut count = 0;
+ let presence =
+ Presence::start_with_accept("a".repeat(32), Arc::new(unresolved_snapshot), move |_| {
+ let _ = &finished;
+ // Do not throttle the loop in the injected operation, even when it spins.
+ if count < 3 {
+ let _ = attempted.send(Instant::now());
+ }
+ count += 1;
+ Err(kind.into())
+ })
+ .unwrap();
+ let times: Result, _> = (0..3)
+ .map(|_| attempts.recv_timeout(Duration::from_secs(1)))
+ .collect();
+ // Join the listener's lifetime signal before checking assertions, including a failed read.
+ drop(presence);
+ completion.recv_timeout(Duration::from_secs(1)).unwrap();
+ let times = times.unwrap_or_else(|error| panic!("{kind:?}: {error}"));
+ for pair in times.windows(2) {
+ assert!(
+ pair[1].duration_since(pair[0]) >= ACCEPT_ERROR_BACKOFF,
+ "{kind:?} retried without an error backoff"
+ );
+ }
+ }
+
+ #[test]
+ fn persistent_aborted_accept_errors_are_paced_and_stop() {
+ assert_persistent_accept_error_is_paced_and_stops(io::ErrorKind::ConnectionAborted);
+ }
+
+ #[test]
+ fn persistent_resource_accept_errors_are_paced_and_stop() {
+ assert_persistent_accept_error_is_paced_and_stops(io::ErrorKind::Other);
+ }
+
+ #[test]
+ fn persistent_interrupted_accept_errors_are_paced_and_stop() {
+ assert_persistent_accept_error_is_paced_and_stops(io::ErrorKind::Interrupted);
+ }
+
+ #[test]
+ fn a_fifth_reader_is_refused_while_four_challenged_readers_remain_open() {
+ let presence = Presence::start("a".repeat(32), Arc::new(unresolved_snapshot)).unwrap();
+ let mut readers = Vec::new();
+ let result = (|| -> io::Result {
+ for _ in 0..MAX_CONNECTIONS {
+ let mut stream = TcpStream::connect(presence.address)?;
+ let response = challenge(&mut stream)?;
+ assert!(response.ends_with(" unknown -\n"));
+ // A completed challenge proves each reader was admitted before trying the fifth.
+ readers.push(stream);
+ }
+ let mut extra = TcpStream::connect(presence.address)?;
+ extra.set_read_timeout(Some(Duration::from_secs(1)))?;
+ extra.read(&mut [0_u8; 1])
+ })();
+ drop(readers);
+ drop(presence);
+ assert_eq!(result.unwrap(), 0);
+ }
+
+ #[test]
+ fn fresh_challenges_observe_stop_without_replacing_the_listener() {
+ let root = std::env::temp_dir().join(format!("ocx-recovery-wire-{}", std::process::id()));
+ fs::create_dir_all(&root).unwrap();
+ let home = root.join("home");
+ fs::create_dir_all(&home).unwrap();
+ let state = Arc::new(Mutex::new(Snapshot {
+ home: Some(home),
+ wanted: true,
+ }));
+ let observed = Arc::clone(&state);
+ let presence = Presence::start(
+ "a".repeat(32),
+ Arc::new(move || observed.lock().unwrap().clone()),
+ )
+ .unwrap();
+ let mut stream = TcpStream::connect(presence.address).unwrap();
+ stream
+ .set_read_timeout(Some(Duration::from_secs(2)))
+ .unwrap();
+ for (nonce, wanted) in [('b', true), ('c', false)] {
+ state.lock().unwrap().wanted = wanted;
+ let nonce = nonce.to_string().repeat(64);
+ stream
+ .write_all(format!("{DOMAIN} {nonce}\n").as_bytes())
+ .unwrap();
+ let mut response = String::new();
+ BufReader::new(&stream).read_line(&mut response).unwrap();
+ assert!(response.starts_with(&format!(
+ "{DOMAIN} {nonce} {} {} ",
+ "a".repeat(32),
+ std::process::id()
+ )));
+ assert!(response.contains(if wanted { " recovering " } else { " stopped " }));
+ }
+ drop(stream);
+ drop(presence);
+ assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn idle_connections_release_every_slot_within_the_connection_budget() {
+ let presence = Presence::start(
+ "a".repeat(32),
+ Arc::new(|| Snapshot {
+ home: None,
+ wanted: true,
+ }),
+ )
+ .unwrap();
+ let deadline = Instant::now() + Duration::from_secs(4);
+ let mut idle: Vec<_> = (0..MAX_CONNECTIONS)
+ .map(|_| TcpStream::connect(presence.address).unwrap())
+ .collect();
+ for stream in &mut idle {
+ let remaining = deadline.saturating_duration_since(Instant::now());
+ assert!(
+ !remaining.is_zero(),
+ "idle connection exhausted the client budget"
+ );
+ stream.set_read_timeout(Some(remaining)).unwrap();
+ assert_eq!(stream.read(&mut [0_u8; 1]).unwrap(), 0);
+ }
+ assert!(Instant::now() < deadline);
+ // A fresh challenge is admitted after the entire idle cohort has expired.
+ let mut stream = TcpStream::connect(presence.address).unwrap();
+ stream
+ .set_read_timeout(Some(Duration::from_secs(1)))
+ .unwrap();
+ let nonce = "b".repeat(64);
+ stream
+ .write_all(format!("{DOMAIN} {nonce}\n").as_bytes())
+ .unwrap();
+ let mut response = String::new();
+ BufReader::new(&stream).read_line(&mut response).unwrap();
+ assert!(response.starts_with(&format!("{DOMAIN} {nonce} ")));
+ assert!(response.ends_with(" unknown -\n"));
+ }
+
+ #[test]
+ fn unresolved_home_and_invalid_requests_never_assert_recovery() {
+ let snapshot = Snapshot {
+ home: None,
+ wanted: true,
+ };
+ let request = format!("{DOMAIN} {}\n", "a".repeat(64));
+ assert!(answer(&request, &"b".repeat(32), 123, snapshot.clone())
+ .unwrap()
+ .ends_with(" unknown -\n"));
+ for request in [
+ "GET / HTTP/1.1\n",
+ "ocx-desktop-recovery-v1 short\n",
+ "ocx-desktop-recovery-v1 a\r\n",
+ ] {
+ assert!(answer(request, &"b".repeat(32), 123, snapshot.clone()).is_none());
+ }
+ }
+}
diff --git a/desktop/src-tauri/src/startup.rs b/desktop/src-tauri/src/startup.rs
index 8ceb94fbd48..0b3578e05ce 100644
--- a/desktop/src-tauri/src/startup.rs
+++ b/desktop/src-tauri/src/startup.rs
@@ -696,6 +696,7 @@ pub fn begin_with(app: &AppHandle, mode: Mode) -> bool {
.store(mode == Mode::Recover, Ordering::Release);
startup.restart();
let generation = startup.generation.fetch_add(1, Ordering::AcqRel) + 1;
+ crate::recovery::begin_run(app, generation, mode == Mode::Recover);
let started = Instant::now();
startup.set_deadline(started + DEADLINE);
let app = app.clone();
@@ -725,6 +726,7 @@ pub fn begin_with(app: &AppHandle, mode: Mode) -> bool {
),
);
if let Expiry::Fired(progress) = &expiry {
+ crate::recovery::failed(&guard, generation);
let _ = guard.emit(PHASE_EVENT, progress);
}
expiry
@@ -769,7 +771,7 @@ pub fn begin_with(app: &AppHandle, mode: Mode) -> bool {
(false, Some(held)) => crate::supervisor::RunOutcome::Held(held),
(false, None) => crate::supervisor::RunOutcome::Failed,
};
- crate::supervisor::run_finished(&app, mode, ended, detail.as_deref());
+ crate::supervisor::run_finished(&app, generation, mode, ended, detail.as_deref());
});
true
}
@@ -784,6 +786,7 @@ fn settle(app: &AppHandle, started: Instant, generation: u64, reason: String) {
};
startup.with_reporting(|| {
if let Some(progress) = startup.settle(started, generation, reason) {
+ crate::recovery::failed(app, generation);
let _ = app.emit(PHASE_EVENT, progress);
}
});
@@ -833,6 +836,7 @@ async fn run(app: &AppHandle, started: Instant) {
endpoint,
home: answer.home(),
};
+ crate::recovery::bind_home(app, &target.home);
let proxy = match ProxyClient::new(endpoint, Auth::new(answer.home())) {
Ok(proxy) => proxy,
Err(error) => {
@@ -1810,6 +1814,17 @@ fn emit(app: &AppHandle, mut progress: Progress, failed_in: Option) -> bo
if !startup.publish(&mut progress, failed_in) {
return false;
}
+ let generation = startup.generation.load(Ordering::Acquire);
+ if progress.phase == Phase::Ready.id() {
+ crate::recovery::ready(
+ app,
+ generation,
+ app.try_state::()
+ .is_some_and(|state| state.owns_runtime()),
+ );
+ } else if progress.phase == Phase::Failed.id() {
+ crate::recovery::failed(app, generation);
+ }
let _ = app.emit(PHASE_EVENT, progress);
true
});
diff --git a/desktop/src-tauri/src/supervisor.rs b/desktop/src-tauri/src/supervisor.rs
index ffccc60b52a..df62c72b51f 100644
--- a/desktop/src-tauri/src/supervisor.rs
+++ b/desktop/src-tauri/src/supervisor.rs
@@ -383,7 +383,7 @@ fn on_exit(app: &AppHandle, pid: u32, exit: SidecarExit) {
));
match verdict {
Verdict::Ignore(_) => {}
- Verdict::Defer => supervisor.state().owed = true,
+ Verdict::Defer => defer_recovery(app, &supervisor),
Verdict::RespawnAfter(delay) => {
// The child is gone: let go of its handle without signalling anything, so nothing
// later reads it as a runtime this app still owns.
@@ -396,6 +396,12 @@ fn on_exit(app: &AppHandle, pid: u32, exit: SidecarExit) {
}
}
+fn defer_recovery(app: &AppHandle, supervisor: &Supervisor) {
+ let mut state = supervisor.state();
+ state.owed = true;
+ crate::recovery::defer_recovery(app);
+}
+
fn describe(verdict: Verdict, attempts: u32) -> String {
match verdict {
Verdict::Ignore(reason) => format!("left alone: {reason}"),
@@ -413,19 +419,23 @@ fn schedule(app: &AppHandle, delay: Duration) {
let Some(supervisor) = app.try_state::() else {
return;
};
- {
+ let reservation = {
let mut state = supervisor.state();
if state.pending {
return;
}
+ // A duplicate scheduler must not see pending before its recovery credit exists.
+ let reservation = crate::recovery::reserve(app);
state.pending = true;
state.attempts = state.attempts.saturating_add(1);
state.streak = 0;
state.healthy_since = None;
state.parked = None;
- }
+ reservation
+ };
let app = app.clone();
tauri::async_runtime::spawn(async move {
+ let _reservation = reservation;
sleep(delay).await;
let Some(supervisor) = app.try_state::() else {
return;
@@ -439,10 +449,10 @@ fn schedule(app: &AppHandle, delay: Duration) {
match decide(input(&app, false, false, attempts)) {
Verdict::RespawnAfter(_) => {
if !startup::begin_with(&app, Mode::Recover) {
- supervisor.state().owed = true;
+ defer_recovery(&app, &supervisor);
}
}
- Verdict::Defer => supervisor.state().owed = true,
+ Verdict::Defer => defer_recovery(&app, &supervisor),
Verdict::Ignore(reason) => supervisor.note(&format!("recovery skipped: {reason}")),
}
});
@@ -452,8 +462,15 @@ fn schedule(app: &AppHandle, delay: Duration) {
/// [`follow_up`] says: another attempt after the backoff, a parked watch when a listener this app
/// cannot use holds the port, or nothing until the person retries. `detail` is the run's own last
/// word, for the log.
-pub fn run_finished(app: &AppHandle, mode: Mode, outcome: RunOutcome, detail: Option<&str>) {
+pub fn run_finished(
+ app: &AppHandle,
+ generation: u64,
+ mode: Mode,
+ outcome: RunOutcome,
+ detail: Option<&str>,
+) {
let Some(supervisor) = app.try_state::() else {
+ crate::recovery::finish_run(app, generation, false);
return;
};
let ready = outcome == RunOutcome::Ready;
@@ -468,6 +485,7 @@ pub fn run_finished(app: &AppHandle, mode: Mode, outcome: RunOutcome, detail: Op
(owed, state.attempts)
};
if ready {
+ crate::recovery::finish_run(app, generation, true);
if mode == Mode::Recover {
supervisor.note("recovery run ready");
}
@@ -498,6 +516,7 @@ pub fn run_finished(app: &AppHandle, mode: Mode, outcome: RunOutcome, detail: Op
}
}
}
+ crate::recovery::finish_run(app, generation, false);
}
/// The watchdog's one question: who answers the endpoint, if anything.
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index 2d1844dbdaf..a2dad4dcdde 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCodex",
- "version": "2.93.0",
+ "version": "2.94.0",
"identifier": "com.opencodex.desktop",
"build": {
"frontendDist": "../ui",
diff --git a/devlog/_plan/261028_oauth_provider_parity/000_plan.md b/devlog/_plan/261028_oauth_provider_parity/000_plan.md
new file mode 100644
index 00000000000..cedb3bc7c23
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/000_plan.md
@@ -0,0 +1,77 @@
+# 000 — Multi-Provider OAuth Protocol, Client Profile & Pool Isolation Plan
+
+## Reader Summary (`READER-DOC-02`)
+
+OpenCodex currently leaks cross-account client identifiers (`device_id` on Claude native passthrough, `x-codex-installation-id` and `x-oai-attestation` on Codex pools, un-scoped `request.sessionId` and `thoughtSignature` replay caches on Google Antigravity, and shared HTTP/2 sessions on Cursor), emits mismatched or outdated client fingerprints (`@anthropic-ai/sdk/0.74.0` vs `claude-cli/2.1.280`, static per-token `X-Claude-Code-Session-Id`, form-urlencoded Codex token refresh), and leaves gaps in 401/403/429 multi-account pool failover across Google Antigravity, Cursor, Claude, and Codex. This four-phase unit (`wp1` through `wp4`) aligns every provider's wire headers, OAuth refresh contracts, per-conversation session IDs, per-account device/installation IDs, and 401/403/429 pool protection with official client profiles (`claude-code-deobf`, `codex-rs`, `gemini-cli`) and reference proxies (`CLIProxyAPI`, `cliproxy-rs`, `cc-switch`), delivering each phase as an independently verified PR to `dev`. Full comparative evidence lives in [001_provider_parity_matrix.md](./001_provider_parity_matrix.md).
+
+---
+
+## 1. Loop-Spec Header
+
+| Field | Value |
+|---|---|
+| **Loop archetype** | `satisfy-spec` (C4 docs-first multi-cycle HOTL PABCD loop with topic-split PR delivery and squash merge to `dev`) |
+| **Trigger** | User requested a full comparative audit (`wp0`) and topic-split implementation (`wp1`..`wp4`) to resolve OpenCodex parity, header/fingerprint, session-id, device-id, installation-id, token-refresh, and multi-account pool isolation/cooldown gaps across Claude, Codex (ChatGPT), Google OAuth (Gemini CLI / Antigravity / Cloud Code), and Cursor, through independent review, green exact-head CI, and squash merge to `dev`. |
+| **Goal** | Close all identified parity and isolation gaps across Claude (`wp1`), Codex (`wp2`), Google OAuth & Cursor (`wp3`), and Multi-Account Pool Protection (`wp4`), landing each implementation slice as a green, reviewed PR into `dev`. |
+| **Non-goals** | Leaving third-party reference clones inside the worktree; raising or weakening `file-size-baseline.json` caps, `privacy:scan`, or `structure:check`; pushing directly to `main` or `preview`; adding brittle `cch` cryptographic billing-header signing. |
+| **Verifier** | Focused `bun test` suites per phase (verified below under `PLAN-VERIFIER-REAL-01`), `bun run typecheck`, `bun run privacy:scan`, `bun run structure:check`, `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts`, exact-head GitHub PR CI checks, and inherited-model (`gemini-4-argon`) independent review tasks (`role: "review"` via `goat_task_create`). |
+| **Stop condition** | `wp0` roadmap locked and audited; `wp1`..`wp4` each implemented red-to-green, verified locally under `lockf -k /tmp/goat-mbp-heavy.lock`, opened as PRs to `dev`, approved by inherited-model review (`VERDICT: PASS`), verified green on exact-head CI and local `dev` merge simulation, squash-merged to `dev`, and `origin.report(completed)` sent. |
+| **Memory artifact** | `devlog/_plan/261028_oauth_provider_parity/` (`000_plan.md`, `001_provider_parity_matrix.md`, `010_phase1_claude_parity.md`, `020_phase2_codex_parity.md`, `030_phase3_google_cursor_parity.md`, `040_phase4_pool_protection.md`) and `.codexclaw/goalplans/loop-archetype-satisfy-spec-multi-cycle-c4-docs/goalplan.json`. |
+| **Expected terminal outcomes** | `DONE` when all criteria (`c-1`..`c-5`) and work-phases (`wp0`..`wp4`) are met with fresh proof and squash-merged PRs; `BLOCKED` / `NEEDS_HUMAN` / `UNSAFE` / `BUDGET_EXHAUSTED` only on genuine external blockers or resource bounds. |
+| **Escalation condition** | Destructive actions outside scope, unresolvable upstream ambiguity, or persistent hosted CI / permission blocks. |
+| **HOTL resource bounds** | Token/cost and wall-clock bounds: `none stated; host limits apply`. Unattended tool/credential & write scope: full local workspace write, `git push`, `gh pr create` / `gh pr merge --squash` targeting `dev` (explicitly authorized by user prompt), and `lockf -k /tmp/goat-mbp-heavy.lock` serialization for heavy local runs. |
+
+---
+
+## 2. Dependency-Ordered Work-Phase Map (`PHASE-SPLIT-01` & `DIFFLEVEL-ROADMAP-01`)
+
+| Work-Phase | Decade Document | Scope & Architectural Role | Prerequisite |
+|---|---|---|---|
+| **`wp0`** | [000_plan.md](./000_plan.md), [001_provider_parity_matrix.md](./001_provider_parity_matrix.md) | Docs-first comparative survey and diff-level multi-phase roadmap lock. | None |
+| **`wp1`** | [010_phase1_claude_parity.md](./010_phase1_claude_parity.md) | **Claude OAuth Translated & Native Path Parity & Isolation**: unify `claude-cli/2.1.280` + `@anthropic-ai/sdk/0.112.1` + `v26.3.0` + normalized `X-Stainless-OS`/`Arch` + `anthropic-dangerous-direct-browser-access`, `1h` cache TTL beta pairing (`extended-cache-ttl-2025-04-11`), first-party `message_stop` validation, per-conversation `X-Claude-Code-Session-Id`, and per-account isolated `device_id` (`account-metadata.ts`). | `wp0` |
+| **`wp2`** | [020_phase2_codex_parity.md](./020_phase2_codex_parity.md) | **Codex (ChatGPT) OAuth Installation ID Isolation & Official JSON Refresh**: isolate `x-codex-installation-id`, `x-codex-turn-metadata.installation_id`, and `body.client_metadata["x-codex-installation-id"]` per pooled account via idempotent HMAC-tagged UUIDv4; strip `x-oai-attestation` on pool overrides; scrub `x-codex-turn-state` on account switch; align `refreshChatGPTToken`, `account-store.ts`, and `main-account.ts` with official JSON refresh and `ORIGINATOR = "codex_cli_rs"`. | `wp1` |
+| **`wp3`** | [030_phase3_google_cursor_parity.md](./030_phase3_google_cursor_parity.md) | **Google OAuth (Antigravity / Cloud Code) & Cursor Parity & Isolation**: isolate `antigravitySessionId`, thoughtSignature replay cache, and `provider-models.ts` `catalogAuthority` per account/project; preserve `thinkingBudget`; filter `context_engineering_is_the_way_to_go`; extract Google `id_token` `sub` and `allowedTiers` default; isolate `cursorH2Pool` per credential; anchor `_cursorIdentityScope` to JWT `sub`; add `x-cursor-streaming: "true"` on HTTP/1.1; remove `opencodex-quota` UA giveaway. | `wp2` |
+| **`wp4`** | [040_phase4_pool_protection.md](./040_phase4_pool_protection.md) | **Multi-Account Pool Protection (401/403/429 Cooldowns, Session Pinning, Suspended Exclusion)**: preserve Google `invalid_grant` refresh error codes so `needsReauth` and 401 failover fire; check bare account-wide cooldowns in `isCooled` and record bare + family cooldowns on Antigravity auth refusal; extract Google 429 `RetryInfo` into `Retry-After` and enable `exhaustedCooldownMs`; wire `cursor` into `FORCE_REFRESH_PROVIDERS`, `isOAuth401ReplayProvider`, terminal refresh detection, and preflight 401/403 rotation; evict Anthropic session affinity on auto-pause; classify Codex `deactivated_workspace`/`account_deactivated` as terminal `revoked`. | `wp3` |
+
+---
+
+## 3. Source-of-Truth (`SOT-SYNC-01`) Synchronization Target
+
+Per `SOT-SYNC-01` and `structure/AGENTS.md`:
+- All modified files live inside already-claimed directories (`src/oauth/`, `src/adapters/`, `src/providers/`, `src/codex/`, `src/server/`).
+- Specifically, [structure/providers/anthropic-account-pool.md](../../structure/providers/anthropic-account-pool.md) (490/600 lines — 110 lines headroom), [structure/providers/generic-account-pool.md](../../structure/providers/generic-account-pool.md) (33/600 lines — 567 lines headroom), and [structure/providers/cursor.md](../../structure/providers/cursor.md) (440/600 lines — 160 lines headroom) are the general source-of-truth documents owning these contracts and will be synced during `C` phase when their documented behavior changes, verified by `bun run structure:check`.
+
+---
+
+## 4. Verifier Grounding (`PLAN-VERIFIER-REAL-01`)
+
+Every verifier command below was executed on the current checkout (`e25be531f0`) during `wp0` `P` phase:
+
+1. `bun run typecheck` — **Exit code: `0`**. Reads all `src/**/*.ts` and `tests/**/*.ts` via `tsconfig.json` (`"include": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"]`).
+2. `bun run privacy:scan` — **Exit code: `0`**. Executes `scripts/privacy-scan.ts`, which scans tracked and staged files across `src/`, `tests/`, `devlog/`, `structure/`, and `docs-site/`.
+3. `bun run structure:check` — **Exit code: `0`**. Executes `scripts/structure-ssot.ts`, which verifies `structure/INDEX.md`, `structure/manifest.json` ownership of `src/` paths, doc line budgets (`<= 600`), and invariant test bindings.
+4. `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/ci-workflows/file-size-ratchet.test.ts` — **Exit code: `0`** (36 pass, 0 fail). Directly reads `scripts/test-layout/layout.json`, `scripts/test-layout/layout-*.json`, `tests/fixtures/test-layout-expected.json`, and `tests/fixtures/file-size-baseline.json`.
+5. `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/clients/client-fingerprint.test.ts tests/adapters/anthropic/anthropic-account-metadata.test.ts tests/adapters/anthropic/anthropic-client-identity.test.ts` — **Exit code: `0`** (40 pass, 0 fail). Directly imports `src/adapters/client-fingerprint.ts`, `src/adapters/anthropic/account-metadata.ts`, `src/adapters/anthropic/client-identity.ts`, `src/adapters/anthropic/passthrough.ts`, and `src/adapters/anthropic.ts`.
+6. `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/oauth/chatgpt-oauth.test.ts tests/oauth/chatgpt-token-expiry.test.ts tests/oauth/chatgpt-token-boundaries.test.ts` — **Exit code: `0`** (65 pass, 0 fail). Directly imports `src/oauth/chatgpt.ts` and reads `src/codex/account-store.ts`.
+7. `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/adapters/google/google-antigravity-wire.test.ts tests/adapters/google/google-antigravity-replay.test.ts tests/adapters/google/google-antigravity-oauth.test.ts tests/adapters/google/google-wire-compiler.test.ts tests/providers/cursor/cursor-adapter.test.ts tests/providers/cursor/cursor-discovery.test.ts` — **Exit code: `0`** (206 pass, 0 fail). Directly imports `src/adapters/google-antigravity-wire.ts`, `src/adapters/google-antigravity-replay.ts`, `src/oauth/google-antigravity.ts`, `src/adapters/google-wire-compiler.ts`, `src/adapters/cursor.ts`, and `src/adapters/cursor/discovery.ts`.
+8. `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/oauth/generic-oauth-failover.test.ts tests/oauth/oauth-account-auto-pause.test.ts tests/adapters/anthropic/anthropic-revoked-token.test.ts tests/claude-integration/messages-revoked-token.test.ts` — **Exit code: `0`** (183 pass, 0 fail). Directly imports `src/oauth/generic-account-failover.ts`, `src/oauth/account-auto-pause.ts`, `src/oauth/anthropic-account-refusal.ts`, and `src/server/claude-messages.ts`.
+
+---
+
+## 5. Architect Consultation & Reflection Record (`phase-plan.md`)
+
+- **Architect Handle**: `taskId: node:delegated-task:command%3Amcp%3A57d8ceeb-1d54-4394-9a7e-1ea8500927ee%3Adelegate-task%3Awp0-arch-proposal-261028` (`childThreadId: thread:delegated-task:command%3Amcp%3A57d8ceeb-1d54-4394-9a7e-1ea8500927ee%3Adelegate-task%3Awp0-arch-proposal-261028`, inherited model `google-antigravity/gemini-4-argon`).
+- **Architect Proposal Decisions & Main Dispositions**:
+ - `DEC-01` (Claude OAuth Profile & Header Unification): **Accepted** — `CLAUDE_CLI_VERSION = "2.1.280"`, `CLAUDE_CLI_SDK_PACKAGE_VERSION = "0.112.1"`, `CLAUDE_CLI_RUNTIME_VERSION = "v26.3.0"` in `src/providers/claude-cli-identity.ts`; normalized `X-Stainless-OS`/`Arch` and `anthropic-dangerous-direct-browser-access: "true"` in `src/adapters/client-fingerprint.ts`; `CLAUDE_CLI_USER_AGENT` applied in `applyAnthropicOAuthAuth` and OAuth sidecars while preserving `@anthropic-ai/sdk/0.112.1` on API-key mode; `extended-cache-ttl-2025-04-11` paired with `1h` cache breakpoints.
+ - `DEC-02` (Claude Per-Conversation Session ID & Per-Account Device ID Isolation): **Accepted** — `claudeCodeSessionId(token, sessionKey)` derives a per-conversation UUID when `sessionKey` is present and falls back to the per-credential UUID when omitted; `bindAnthropicAccountMetadata` derives a deterministic 64-hex `device_id` per `providerAccountUuid`.
+ - `DEC-03` (Codex OAuth Installation ID & Turn-State Isolation): **Accepted** — self-verifying idempotent `deriveCodexAccountInstallationId` in `src/codex/forward-transport-headers.ts` rewrites `x-codex-installation-id`, `x-codex-turn-metadata.installation_id`, and `body.client_metadata["x-codex-installation-id"]` per `chatgptAccountId`, strips `x-oai-attestation` on pool overrides, and scrubs `x-codex-turn-state` on account switches.
+ - `DEC-04` (Codex OAuth JSON Token Refresh & Originator Alignment): **Accepted** — `refreshChatGPTToken`, `account-store.ts`, and `main-account.ts` use `Content-Type: application/json` + `Accept: application/json` with 30s timeout and bounded 64KB response reads while `exchangeToken` retains `application/x-www-form-urlencoded`; `ORIGINATOR = "codex_cli_rs"`.
+ - `DEC-05` (Google OAuth / Antigravity Session, Replay, and Discovery Isolation): **Accepted** — `antigravitySessionId(parsed, project)` and `antigravityModel = `${project}:${wireModelId}`` isolate CCA sessions and `thoughtSignature` replay caches per project while preserving un-scoped `antigravitySessionId(parsed)` when `accountScope` is omitted; `provider-models.ts` scopes `catalogAuthority` per account/project; `google-wire-compiler.ts` preserves `thinkingBudget`; `google-antigravity.ts` extracts `id_token` `sub`, selects `allowedTiers` default, and preserves OAuth error codes.
+ - `DEC-06` (Cursor OAuth Refresh, Connection Pool, and Session Isolation): **Accepted** — `cursor` added to `FORCE_REFRESH_PROVIDERS` and `isOAuth401ReplayProvider`; `cursorH2Pool` keyed by `${origin}\0${credentialScope}`; `_cursorIdentityScope` fallback anchored to JWT `sub`; `cursorContextUsageTracker` keyed by `${identityScope}\0${conversationId}`; `"x-cursor-streaming": "true"` added on HTTP/1.1 `RunSSE`; `opencodex-quota` UA removed.
+ - `DEC-07` (Multi-Account Pool Protection Across All 4 Families): **Accepted** — bare account-wide cooldown checked inside `isCooled(provider, id, now, family)` and recorded on Antigravity auth refusal; session affinity evicted on Anthropic auto-pause; `exhaustedCooldownMs` and `RetryInfo` `Retry-After` extraction enabled for `google-antigravity`; Cursor preflight 401/403 status surfaced and rotated; Codex `deactivated_workspace`/`account_deactivated` classified as terminal `revoked` (preserving `#1789` workspace-403 affinity invariant in `src/codex/routing.ts`).
+ - `DEC-08` (File-Size Ratchet & Boundary Safety): **Accepted** — all near-cap files stay strictly under cap (`src/codex/routing.ts` <= 1626, `src/oauth/index.ts` < 2000, `scripts/test-layout/layout.json` untouched at 1999). New test files if any are registered in `scripts/test-layout/layout-providers.json` + `tests/fixtures/test-layout-expected.json`.
+- **Plan Revision Sent to Architect**: `devlog/_plan/261028_oauth_provider_parity/` (`000_plan.md`, `001_provider_parity_matrix.md`, `010_phase1_claude_parity.md`, `020_phase2_codex_parity.md`, `030_phase3_google_cursor_parity.md`, `040_phase4_pool_protection.md`).
+- **Architect Reflection Handle & Verdict**: `taskId: node:delegated-task:command%3Amcp%3A57d8ceeb-1d54-4394-9a7e-1ea8500927ee%3Adelegate-task%3Awp0-arch-reflection-261028` (`REFLECTION VERDICT: ALIGNED` across `DEC-01`..`DEC-08`).
+- **Reflection Refinements & Main Dispositions**:
+ 1. `wp3` (`DEC-06`): Export `decodeCursorJwtPayload` from `src/oauth/cursor.ts:41` so `src/adapters/cursor.ts` imports it one-way (`adapters -> oauth`, zero import cycle). **Disposition**: Accepted and folded into `030_phase3_google_cursor_parity.md`.
+ 2. `wp4` (`DEC-07`): Because `src/oauth/generic-session-affinity.ts:6` already imports from `./generic-account-failover`, do not add a reverse static import from `./generic-session-affinity` into `src/oauth/generic-account-failover.ts` (`INV-LAYER-01` / `tests/ci-workflows/import-cycle-ratchet.test.ts`); instead, once `isCooled` checks bare account-wide cooldowns, `generic-session-affinity.ts:60` (`readBinding` -> `genericSessionAccountEligible`) automatically evicts the cooled account on read, and `clearGenericSessionAffinityForAccount` can be called from `src/server/` without creating an import cycle. **Disposition**: Accepted and folded into `040_phase4_pool_protection.md`.
diff --git a/devlog/_plan/261028_oauth_provider_parity/001_provider_parity_matrix.md b/devlog/_plan/261028_oauth_provider_parity/001_provider_parity_matrix.md
new file mode 100644
index 00000000000..43023170e9d
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/001_provider_parity_matrix.md
@@ -0,0 +1,71 @@
+# 001 — Multi-Provider OAuth Protocol, Client Profile & Pool Isolation Comparative Matrix
+
+## 1. Purpose and Provenance
+
+This document records the comparative survey across five sources for OpenCodex's multi-provider OAuth, client-fingerprint, session/device isolation, and pool-protection surfaces:
+
+1. **OpenCodex (`HEAD e25be531f0`)**: `src/adapters/`, `src/oauth/`, `src/providers/`, `src/codex/`, and `src/server/`.
+2. **Official Client Corpus (`/Users/jun/Developer/codex`)**:
+ - Claude Code CLI `2.1.280` / `2.1.311` and Claude Desktop `3.6556.2` (`/Users/jun/Developer/codex/150_claude_code`, `/Users/jun/Developer/codex/claude-app-deobf`, `/Users/jun/Developer/codex/046_claude-latest-analysis`, `/Users/jun/Developer/codex/041_provider-oauth-streaming`).
+ - OpenAI Codex CLI (`codex-rs`) (`/Users/jun/Developer/codex/120_codex-cli`, `/Users/jun/Developer/codex/121_openai-codex`).
+ - Google Gemini CLI & Antigravity / Cloud Code Assist (`/Users/jun/Developer/codex/130_gemini-cli`, `/Users/jun/Developer/codex/041_provider-oauth-streaming`).
+3. **CLIProxyAPI (`/tmp/ocx-parity-ref-261028/CLIProxyAPI`)**: Go reference proxy (`internal/runtime/executor/`, `internal/auth/`, `sdk/cliproxy/auth/`).
+4. **cliproxy-rs (`/tmp/ocx-parity-ref-261028/cliproxy-rs`)**: Rust reference proxy (`crates/cpa-exec/src/`, `crates/cpa-common/src/`, `crates/cpa-server/src/`).
+5. **cc-switch (`/tmp/ocx-parity-ref-261028/cc-switch`)**: Cross-platform client/account manager and relay (`src-tauri/src/`).
+
+No implementation diffs appear in this survey document (`LEXICO-SPLIT-01`). All concrete file-level diffs live in `010_phase1_claude_parity.md` through `040_phase4_pool_protection.md`.
+
+---
+
+## 2. Area 1: Claude OAuth Translated & Native Path Comparison
+
+| Surface / Contract | OpenCodex Current (`HEAD`) | Official Claude Code (`2.1.280` / `2.1.311`) | CLIProxyAPI (`Go`) | cliproxy-rs (`Rust`) | cc-switch (`Rust`) |
+|---|---|---|---|---|---|
+| **CLI Version Constant** | `CLAUDE_CLI_VERSION = "2.1.280"` (`src/providers/claude-cli-identity.ts:11`), but comment in `src/adapters/client-fingerprint.ts:16` still says `2.1.63` | `2.1.280` baseline / `2.1.311` current (`150_claude_code/compilable-cli/src/env-and-flags.mjs:2`) | `"2.1.280"` (`internal/runtime/executor/helps/claude_device_profile.go:20`) | `"2.1.280"` (`crates/cpa-exec/src/claude/profile.rs:21`) | Invokes installed `claude` CLI (`src-tauri/src/services/claude_account/login.rs:1`) |
+| **Inference `User-Agent`** | `"@anthropic-ai/sdk/0.74.0"` on translated OAuth (`src/adapters/anthropic.ts:559,1246`), vision OAuth (`src/vision/anthropic-describe.ts:182`), web-search OAuth (`src/web-search/anthropic-executor.ts:191`), and unconfirmed native OAuth (`src/adapters/anthropic/passthrough.ts:302`) | `"claude-cli/2.1.280 (external, cli)"` | `"claude-cli/2.1.280 (external, cli)"` (`claude_device_profile.go:20`) | `"claude-cli/2.1.280 (external, cli)"` (`crates/cpa-exec/src/claude/profile.rs:21`, `headers.rs:448`) | Preserves caller `User-Agent` |
+| **Profile / Usage `User-Agent`** | `"claude-cli/2.1.280 (external, cli)"` (`src/oauth/anthropic-identity.ts:39`, `src/providers/claude-cli-usage.ts:61`) — **contradicts inference `User-Agent`** | `"claude-cli/2.1.280 (external, cli)"` on usage; `"axios/1.15.2"` on OAuth token/profile | `"axios/1.15.2"` on token/profile (`internal/auth/claude/anthropic_auth.go:221,265`) | `"axios/1.15.2"` on token/profile (`crates/cpa-exec/src/oauth.rs:143,148`) | Reads local `.claude.json` |
+| **`X-Stainless-Package-Version`** | `"0.74.0"` (`src/adapters/client-fingerprint.ts:25`, `src/adapters/anthropic.ts:559`) | `"0.112.1"` (in `2.1.280`) / `"0.137.0"` (in Desktop `3.6556.2` `index.chunk-CaMfgPde.js:155`) | `"0.112.1"` (`claude_device_profile.go:21`) | `"0.112.1"` (`crates/cpa-exec/src/claude/profile.rs:22`) | Caller passthrough |
+| **`X-Stainless-Runtime-Version`** | `process.version.slice(1)` — strips leading `"v"` (`src/adapters/client-fingerprint.ts:26`) | `process.version` with leading `"v"` (`"v26.3.0"`, `index.chunk-CaMfgPde.js:191`) | `"v26.3.0"` (`claude_device_profile.go:22`) | `"v26.3.0"` (`crates/cpa-exec/src/claude/profile.rs:23`) | Caller passthrough |
+| **`X-Stainless-OS` & `X-Stainless-Arch`** | Raw `process.platform` (`"darwin"`, `"linux"`, `"win32"`) & `process.arch` (`src/adapters/client-fingerprint.ts:23-24`) | Normalized `"MacOS"`, `"Linux"`, `"Windows"`, `"FreeBSD"`, `"OpenBSD"` & `"arm64"`, `"x64"`, `"x32"` (`index.chunk-CaMfgPde.js:247-248`) | `"MacOS"`, `"arm64"` (`claude_device_profile.go:23-24`) | `"MacOS"`, `"arm64"` (`crates/cpa-exec/src/claude/profile.rs:24-25`) | Caller passthrough |
+| **`anthropic-dangerous-direct-browser-access`** | Omitted in `CLAUDE_CODE_HEADERS` (`src/adapters/client-fingerprint.ts:17-27`) | `"true"` | `"true"` (`headers.rs:134,377`) | `"true"` (`crates/cpa-exec/src/claude/headers.rs:134,377`) | Caller passthrough |
+| **`anthropic-beta` Header (Translated & Native)** | Translated sends `claude-code-20250219,oauth-2025-04-20` (`src/oauth/anthropic.ts:30`) and **omits** `extended-cache-ttl-2025-04-11` even when `ttl: "1h"` is emitted (`src/adapters/anthropic.ts:91,1249`). Native `beta-allowlist.ts:51-73` **drops** `extended-cache-ttl-2025-04-11` and `effort-2025-11-24`, and `passthrough.ts:298-319` never adds `extended-cache-ttl-2025-04-11` when upgrading breakpoints to `1h` | Sends `claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,...` and pairs `ttl: "1h"` with `extended-cache-ttl-2025-04-11` and effort with `effort-2025-11-24` | Pairs `1h` TTL with `extended-cache-ttl-2025-04-11` and effort with `effort-2025-11-24` (`claude_executor_request.go:130`) | Same as Go (`crates/cpa-exec/src/claude/betas.rs:40,198,276`, `headers.rs:309`) | Caller passthrough |
+| **`X-Claude-Code-Session-Id`** | Static UUID derived solely from `accessToken` (`claudeCodeSessionId(token)` in `src/adapters/client-fingerprint.ts:35-41`) — shared across every conversation on that token | Per-conversation / CLI session UUIDv4 | Per-conversation UUID from session/metadata/thread (`ClaudeAgentSessionUUIDForRequest`), fallback to 1h cached UUID per key (`claude_credential_identity.go:28`) | Same as Go (`crates/cpa-exec/src/claude/session.rs:55,108`) | Extracted per conversation (`src-tauri/src/proxy/session.rs:73`) |
+| **`metadata.user_id` & `device_id` Isolation** | `bindAnthropicAccountMetadata` (`src/adapters/anthropic/account-metadata.ts:7-17`) only swaps `account_uuid` when `metadata.user_id` already has a valid `account_uuid` UUID, **preserving the caller's `device_id` across every account in the pool** | `JSON.stringify({ device_id: "<64-hex>", account_uuid: "", session_id: "" })` | Rewrites `device_id` (per-account 64-hex), `account_uuid`, and `session_id` while preserving extra fields (`internal/auth/claude/identity.go:26`) | Same as Go (`crates/cpa-exec/src/claude/identity.rs:38,79`) | Per-account isolated config dir |
+| **Streaming `message_stop` Check** | `src/adapters/anthropic.ts:1548` treats EOF after `message_delta` (`pendingStopReason !== undefined`) as `done` even on first-party Anthropic when `message_stop` was never received or a content block was left open | Requires `message_stop` | Requires `message_stop` (`claude_executor_stream.go:377`) | Requires `message_stop` (`crates/cpa-exec/src/claude/stream.rs:41`) | Allows fallback on non-Anthropic bridges |
+
+---
+
+## 3. Area 2: Codex (ChatGPT) OAuth Path Comparison
+
+| Surface / Contract | OpenCodex Current (`HEAD`) | Official Codex CLI (`120_codex-cli` / `121_openai-codex`) | CLIProxyAPI (`Go`) | cliproxy-rs (`Rust`) | cc-switch (`Rust`) |
+|---|---|---|---|---|---|
+| **Client Installation ID (`x-codex-installation-id` & `installation_id`)** | `FORWARD_HEADERS` (`src/adapters/openai-responses/transport.ts:12`) and `materializeCodexUpstreamAuth` (`src/codex/auth-context.ts:1803`) forward the caller's raw `x-codex-installation-id`, `x-codex-turn-metadata.installation_id`, and `body.client_metadata["x-codex-installation-id"]` verbatim to **all pooled ChatGPT accounts** | Persists one UUIDv4 per `CODEX_HOME/installation_id` (`codex-rs/core/src/installation_id.rs:19`) and sends it in `x-codex-installation-id`, `client_metadata`, and `x-codex-turn-metadata` (`responses_metadata.rs:338,429`) | **Does not forward** `x-codex-installation-id` (`codex_executor_request.go:437`, `codex_websockets_request.go:112`) | **Does not forward** `x-codex-installation-id` (`crates/cpa-exec/src/codex_request.rs:1495,1664`) | **Does not forward** `x-codex-installation-id` (`src-tauri/src/proxy/providers/claude.rs:922`) |
+| **Device Attestation (`x-oai-attestation`)** | Withholds `x-oai-agent-assertion` on pool override, **but forwards `x-oai-attestation` (`transport.ts:14`) verbatim to pooled accounts** | Bound to local client session (`codex-rs/core/src/attestation.rs:7`, `client.rs:980`) | **Does not forward** `x-oai-attestation` | **Does not forward** `x-oai-attestation` | **Does not forward** `x-oai-attestation` |
+| **Account-Switch Sticky State (`x-codex-turn-state`)** | `account-change-state.ts:190-277` scrubs `previous_response_id` and `conversation`, **but leaves `x-codex-turn-state` in headers and `body.client_metadata`**, and exits early at line 262 if only `encryptedReasoning` or `x-codex-turn-state` is present | Clears `turn_state` whenever `auth_owner_generation` changes (`codex-rs/core/src/client.rs:193,1612`) | Forwards per turn | Forwards per turn | Does not forward |
+| **OAuth Token Refresh Format & Headers** | 3 separate implementations (`src/oauth/chatgpt.ts:44,274`, `src/codex/account-store.ts:1378`, `src/codex/main-account.ts:114`), all sending `application/x-www-form-urlencoded` without `Accept: application/json`; `account-store.ts` and `main-account.ts` use unbounded `res.json()` on 200 OK | Uses **`TokenEncoding::Json`** (`Content-Type: application/json`) for refresh (`codex-rs/login/src/auth/manager.rs:1712`, `oauth/client.rs:15,101-135`) and `TokenEncoding::Form` for authorization_code exchange (`server.rs:804`) | Form-encoded with `Accept: application/json` and 30s timeout (`internal/auth/codex/openai_auth.go:220`) | Form-encoded with `Accept: application/json` and 30s timeout (`crates/cpa-exec/src/codex_oauth.rs:308`) | Form-encoded with 30s timeout (`codex_oauth_auth.rs:748`) |
+| **Authorize URL `originator`** | `ORIGINATOR = "opencodex"` (`src/oauth/chatgpt.ts:118,229`) | `DEFAULT_ORIGINATOR = "codex_cli_rs"` (`codex-rs/login/src/auth/default_client.rs:42`, `server.rs:584`) | Omitted | Omitted | `"codex_cli_rs"` (`codex_oauth_auth.rs:74`) |
+| **Header Allowlist (`version`) & Control-Plane `originator`** | `FORWARD_HEADERS` (`transport.ts:10-15`) omits `"version"`; `model-entitlements.ts:723,876` and `warmup.ts:252` omit `originator` | Sends `originator: "codex_cli_rs"` and `version` (`models_endpoint.rs:220`, `default_client.rs:456`) | Forwards `Version` and sets `Originator` (`codex_executor_request.go:440,552`) | Forwards `version` and sets `originator` (`codex_request.rs:1497,1561`) | Sends `originator: "codex_cli_rs"` and `version` (`codex_oauth_models.rs:50`) |
+
+---
+
+## 4. Area 3: Google OAuth (Gemini CLI / Antigravity / Cloud Code Assist) & Cursor Comparison
+
+| Surface / Contract | OpenCodex Current (`HEAD`) | Official Corpus (`130_gemini-cli`, `041_provider-oauth-streaming`) | CLIProxyAPI (`Go`) | cliproxy-rs (`Rust`) | cc-switch (`Rust`) |
+|---|---|---|---|---|---|
+| **Antigravity `request.sessionId` & Replay Cache Isolation** | `antigravitySessionId(parsed)` (`src/adapters/google-antigravity-wire.ts:83-109`) and `replayKey(model, sessionId)` (`src/adapters/google-antigravity-replay.ts:344`) have **no account/project scope**, so pooled Google accounts share the exact same `request.sessionId` and cross-inject thoughtSignatures after failover | Per-session `session_id` + `user_prompt_id` (`130_gemini-cli/packages/core/src/code_assist/converter.ts:117-163`); multi-account harnesses scope by `accountKey` (`projectId` / `accountId`) | Per-credential transport & capability isolation (`antigravity_executor.go:76-83,206-226`, `antigravity_models.go:101-108`) | Same as Go (`crates/cpa-exec/src/antigravity.rs:133,316`) | Per-account session files |
+| **Antigravity Model Discovery Cache Isolation** | `src/codex/catalog/provider-models.ts:652-654` computes `catalogAuthority` only for `workbuddy-ai`, leaving `google-antigravity` un-scoped across pooled accounts | Per-account discovery scope | Per-credential capability key (`antigravity_models.go:101-108`) | Per-credential probe table (`crates/cpa-exec/src/antigravity.rs:141`) | N/A |
+| **Google Wire Compiler `thinkingConfig` & Signature Sentinel** | `compileGenerationConfig` (`src/adapters/google-wire-compiler.ts:166-180`) keeps `thinkingLevel` and `includeThoughts` but **drops `thinkingBudget`**; `isLikelyRealThoughtSignature` (`google-antigravity-wire.ts:51`) and `extractSignature` (`google-antigravity-replay.ts:500`) do not filter `"context_engineering_is_the_way_to_go"` | Supports both `thinkingLevel` and `thinkingBudget` (`130_gemini-cli/packages/core/src/code_assist/converter.ts:74`) | Supports both `thinkingLevel` and `thinkingBudget` (`internal/thinking/provider/antigravity/apply.go:125-145`) | Filters both `skip_thought_signature_validator` and `context_engineering_is_the_way_to_go` (`crates/cpa-common/src/signature/gemini.rs:12-13`) | N/A |
+| **Antigravity `loadCodeAssist`, `onboardUser`, and Refresh** | `loadCodeAssistProject` (`src/oauth/google-antigravity.ts:99`) hits `PROD_API`, `onboardProject` (line 114) hardcodes `"free-tier"` instead of reading `allowedTiers` default, `credentialsFromPayload` (line 161) omits JWT `sub` as `accountId`, and `postToken` (line 79) discards OAuth error codes | `loadCodeAssist` -> `getOnboardTier(allowedTiers)` (`130_gemini-cli/packages/core/src/code_assist/setup.ts:161-271`) | `defaultAntigravityTierID(loadResp)` + preserves `token_error_code` (`internal/auth/antigravity/auth.go:114-139`, `antigravity_auth.rs:221,540`) | Same as Go (`crates/cpa-exec/src/antigravity_auth.rs:221-244,515-550`) | N/A |
+| **Cursor HTTP/2 Pool & Identity Scope Isolation** | `cursorH2Pool` (`src/adapters/cursor/h2-pool.ts:38-55`) keys HTTP/2 sessions by `origin` only, multiplexing all Cursor accounts over one TLS connection; `src/adapters/cursor.ts:179` hashes raw `accessToken` when `_cursorIdentityScope` is unset (breaking checkpoints on refresh); `cursorContextUsageTracker` (`live-transport.ts:141,735`) keys by `conversationId` without `identityScope` | `credentialsFromCursorTokens` (`src/oauth/cursor.ts:54`) extracts stable `sub` from JWT | N/A | N/A | N/A |
+| **Cursor Headers & Quota Probe UA** | `http1-bidi.ts:217-228` omits `"x-cursor-streaming": "true"`; `live-models.ts:29` uses old `"cli-2026.02.13-41ac335"` vs `"cli-2026.07.08-0c04a8a"` in `live-transport.ts:97`; `vendor-probes-oauth.ts:440` sends `"User-Agent": "opencodex-quota"` | Unified `CURSOR_CLIENT_VERSION`, `"x-cursor-streaming": "true"` on HTTP/1.1 `RunSSE`, no giveaway `User-Agent` | N/A | N/A | N/A |
+
+---
+
+## 5. Area 4: Multi-Account Pool Protection (401 / 403 / 429 & Suspended Exclusion) Comparison
+
+| Provider Family | 401 (Token Refresh & Revoked Grant `#6832`) | 403 (Entitlement / Workspace / Verify Account / Suspended) | 429 (Per-Account / Per-Model Cooldown & Reset Parsing) | Session Pinning Failover & Eviction |
+|---|---|---|---|---|
+| **Claude (`anthropic`, `anthropic2`)** | Exact `"OAuth access token has been revoked."` (`src/oauth/anthropic-account-refusal.ts:51-65`) marks `needsReauth`, clears affinity, and rotates (`#6832`). **Gap A-1**: non-revoked 401 `authentication_error` (e.g. clock-skew expired token) is not force-refreshed before failing. | `isAccountRefusal` (`anthropic-account-refusal.ts:67-90`) cools for `10m`, clears affinity, and rotates; consecutive permanent 401/403 triggers `accountAutoPause`. **Gap A-2**: `wasAccountAutoPaused` branch (`line 121`) skips `clearAnthropicSessionAffinityForAccount`. | 4-way classification (`shared-quota`, `family-quota`, `transient-rate`, `request-scoped-unknown`) with `Retry-After` and unified reset header parsing (`src/oauth/anthropic-rate-limit-policy.ts`). | Evicts affinity on `needsReauth`, `paused`, `shared-quota` 429, 403 refusal, and 401 revoked (`src/oauth/anthropic-routing.ts:1033,1216,1268,1301`). |
+| **Codex (`openai` pool / `chatgpt`)** | Pre-output 401 triggers `refreshPoolForwardAuth` (`src/server/responses/core-auth.ts:458-551`). **Gap X-1**: when refresh fails terminally (`quarantine: true`), `passthrough-dispatch.ts:1239` and `compact.ts:1318` quarantine the account but immediately return 401 instead of failing over to a healthy sibling pool account. **Gap X-3**: `chatgpt-refresh-failure.ts:34-44` omits `account_deactivated` / `deactivated_workspace`. | Distinguishes `workspace`, `entitlement`, and `credential` 403 (`src/codex/quota-rejection.ts:81-111`). **Gap X-2**: `workspace` 403 (`src/codex/routing.ts:1389-1400`) does not clear thread affinity or cool the account, and `passthrough-dispatch.ts:1641-1668` does not fail over on pre-output 403. | Separates `shared` vs `reserve` quota scopes (`src/codex/routing/cooldown-math.ts:83-216`, `health-store.ts:34-91`) with probe leases and transient burst holds. | Evicts thread affinity on `needsReauth`, `quarantine`, 429 cooldown, and consecutive failure threshold (`src/codex/routing.ts:436,505,1441,1487`). |
+| **Google (`google-antigravity`)** | **Gap G-1**: `postToken` (`src/oauth/google-antigravity.ts:71-83`) discards `invalid_grant` from 400 error bodies, so `terminal(error)` (`src/oauth/index.ts:787`) is `false`, `needsReauth` is never set, and `adapter-dispatch.ts:982-989` never rotates to a sibling account! | `VALIDATION_REQUIRED` -> 60s `"auth"` cooldown + rotate; `verify_account` -> `needsReauth` + rotate (`adapter-dispatch.ts:1673-1763`). **Gap G-2**: missing in `adapter-continuation.ts` and incomplete in `sidecar-execution.ts` / `images/loop.ts`. **Gap G-4**: `isCooled` (`generic-account-failover.ts:120-142`) misses bare account-wide `"auth"` cooldowns when called with `family` (`"gem"` / `"cla"`). | **Gap G-3**: `google-http.ts:128-137` drops Google JSON `RetryInfo.retryDelay`, and `exhaustedCooldownMs` (`account-quota-rank.ts:295-301`) is gated to `provider === "kiro"`, so exhausted Antigravity accounts only cool for 60s. | **Gap G-2**: `rotateAntigravityAccountOnAuthRefusal` (`generic-account-failover.ts:675-714`) does not clear generic session affinity (`clearGenericSessionAffinityForAccount`). |
+| **Cursor (`cursor`)** | **Gap C-1**: `cursor` is omitted from `FORCE_REFRESH_PROVIDERS` (`src/oauth/index.ts:728`) and `isOAuth401ReplayProvider` (`request-transport.ts:186`). **Gap C-2**: `cursor.ts:722`, `live-transport.ts:1508`, and `http1-bidi.ts:108` drop HTTP 401/403/429 status metadata. **Gap C-3**: `refreshCursorToken` (`src/oauth/cursor.ts:237`) throws `"Cursor token refresh failed: 401"`, which `isTerminalRefreshError` (`src/oauth/index.ts:778`) fails to recognize as terminal. | **Gap C-4**: `classifyCursorError` (`src/adapters/cursor/cursor-errors.ts:303-313`) maps `permission_denied` / `forbidden` to `"Cursor authentication failed"` (401), and `run-turn-execution.ts:588` has no preflight 403 account rotation. | `rotateRunTurnAdapterOnPreflight429` (`run-turn-execution.ts:588-687`) rotates on Connect `resource_exhausted` (429), but HTTP-level 429s on HTTP/2 or HTTP/1.1 lose their 429 status (`Gap C-2`). | Because 401/403/revoked errors never mark `needsReauth` or cool the account (`Gaps C-1..C-4`), sessions stay pinned to dead Cursor accounts in `generic-session-affinity.ts`. |
diff --git a/devlog/_plan/261028_oauth_provider_parity/010_phase1_claude_parity.md b/devlog/_plan/261028_oauth_provider_parity/010_phase1_claude_parity.md
new file mode 100644
index 00000000000..90bb43cd4dc
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/010_phase1_claude_parity.md
@@ -0,0 +1,222 @@
+# 010 — Phase 1 (`wp1`): Claude OAuth Translated & Native Path Parity, Session ID, and `device_id` Isolation
+
+- **Work-Phase ID**: `wp1`
+- **Depends On**: `wp0` (`000_plan.md`, `001_provider_parity_matrix.md`)
+- **Shared Types / Interfaces Used**: `AnthropicMessagesPassthroughOptions` (`src/adapters/anthropic/passthrough.ts:87`), `OcxParsedRequest` (`src/types.ts`), `IncomingMeta` (`src/adapters/base.ts`)
+- **Design Decisions Implemented**: `DEC-01`, `DEC-02`, `DEC-08`
+
+## 1. File Change Map
+
+| Action | File Path | Current Lines / Cap | Responsibility & Change Summary |
+|---|---|---:|---|
+| `MODIFY` | `src/providers/claude-cli-identity.ts` | 12 / 1999 | Export `CLAUDE_CLI_SDK_PACKAGE_VERSION = "0.112.1"` and `CLAUDE_CLI_RUNTIME_VERSION = "v26.3.0"` alongside `CLAUDE_CLI_VERSION = "2.1.280"` and `CLAUDE_CLI_USER_AGENT`. |
+| `MODIFY` | `src/adapters/client-fingerprint.ts` | 86 / 1999 | Update `CLAUDE_CODE_HEADERS` (`X-Stainless-Package-Version: "0.112.1"`, `X-Stainless-Runtime-Version: "v26.3.0"`, normalized `X-Stainless-OS` via `stainlessOsName()`, normalized `X-Stainless-Arch` via `stainlessArchName()`, `anthropic-dangerous-direct-browser-access: "true"`); extend `claudeCodeSessionId(token, sessionKey?)` to derive a per-conversation UUID when `sessionKey` is present and fall back to per-credential UUID when absent. |
+| `MODIFY` | `src/adapters/anthropic/account-metadata.ts` | 17 / 1999 | Add `deriveAnthropicAccountDeviceId(accountUuid)` and `extractAnthropicCallerSessionId(body)`; update `bindAnthropicAccountMetadata(body, providerAccountUuid)` so `metadata.user_id` always binds an isolated per-account 64-hex `device_id` alongside `account_uuid` while preserving `session_id` and extra JSON keys. |
+| `MODIFY` | `src/adapters/anthropic/beta-allowlist.ts` | 159 / 1999 | Move `extended-cache-ttl-2025-04-11`, `effort-2025-11-24`, `thinking-token-count-2026-05-13`, `mid-conversation-tool-changes-2026-07-01`, and `advanced-tool-use-2025-11-20` from `KNOWN_UNFORWARDED_BETAS` into `NATIVE_CLIENT_BETAS` so observed native Claude Code clients keep their 2.1.280 feature betas. |
+| `MODIFY` | `src/adapters/anthropic/passthrough.ts` | 334 / 1999 | Accept `sessionKey?: string | null` in `AnthropicMessagesPassthroughOptions`; resolve conversation session ID and pass to `applyAnthropicOAuthAuth` and `bindAnthropicAccountMetadata`; auto-append `extended-cache-ttl-2025-04-11` when first-party wire body contains `ttl: "1h"`. |
+| `MODIFY` | `src/server/messages-native.ts` | 1133 / 1999 | Pass `sessionKey: oauthBinding?.sessionKey ?? options.sessionKey` into `buildAnthropicMessagesPassthroughRequest` at line 483. |
+| `MODIFY` | `src/adapters/anthropic.ts` | 1719 / 1999 | Use `CLAUDE_CLI_SDK_PACKAGE_VERSION` in `anthropicBaseRequestHeaders`; set `headers["User-Agent"] = CLAUDE_CLI_USER_AGENT` and per-conversation `X-Claude-Code-Session-Id` in `applyAnthropicOAuthAuth(headers, accessToken, sessionKey)`; add `extended-cache-ttl-2025-04-11` when `1h` cache breakpoints are present; require `message_stop` on first-party Anthropic streams when a block is open or `message_stop` is absent. |
+| `MODIFY` | `src/vision/anthropic-describe.ts` | 248 / 1999 | Use `CLAUDE_CLI_USER_AGENT` on OAuth vision requests instead of `@anthropic-ai/sdk/0.74.0`. |
+| `MODIFY` | `src/web-search/anthropic-executor.ts` | 260 / 1999 | Use `CLAUDE_CLI_USER_AGENT` on OAuth web-search requests instead of `@anthropic-ai/sdk/0.74.0`. |
+| `MODIFY` | `tests/clients/client-fingerprint.test.ts` | 136 / 1999 | Add red-to-green tests for `CLAUDE_CODE_HEADERS` (`0.112.1`, `v26.3.0`, `MacOS`/`Linux`/`Windows`, `anthropic-dangerous-direct-browser-access`), `CLAUDE_CLI_USER_AGENT` on OAuth vs `@anthropic-ai/sdk/0.112.1` on API-key, per-conversation `claudeCodeSessionId(token, sessionKey)` isolation, and isolate `GOOGLE_ANTIGRAVITY_USER_AGENT` env var. |
+| `MODIFY` | `tests/adapters/anthropic/anthropic-account-metadata.test.ts` | 73 / 1999 | Update and expand tests for per-account deterministic `device_id` isolation (`deriveAnthropicAccountDeviceId(A) !== deriveAnthropicAccountDeviceId(B)`), `account_uuid` binding, `session_id` preservation, and extra-key retention. |
+| `MODIFY` | `tests/adapters/anthropic/anthropic-client-identity.test.ts` | 225 / 1999 | Update SDK package version (`0.112.1`) and OAuth `User-Agent` (`claude-cli/2.1.280 (external, cli)`) assertions, plus per-conversation `X-Claude-Code-Session-Id` tests. |
+| `MODIFY` | `tests/claude-integration/messages-native-oauth.test.ts` | 530 / 1999 | Update native OAuth `metadata.user_id` assertions to verify deterministic per-account `device_id` (`deriveAnthropicAccountDeviceId(uuid)`) across account switches. |
+| `MODIFY` | `tests/vision/vision-anthropic.test.ts` | 240 / 1999 | Update OAuth vision `User-Agent` assertion to `claude-cli/2.1.280 (external, cli)`. |
+
+## 2. Field and Option Chains (`PLAN-FIELD-CHAIN-01`)
+
+| Field / Option | 1. Creation | 2. Serialization | 3. Deserialization | 4. Consumers |
+|---|---|---|---|---|
+| `AnthropicMessagesPassthroughOptions.sessionKey?: string \| null` | Created in `src/server/messages-native.ts:483` (`sessionKey: oauthBinding?.sessionKey ?? options.sessionKey`) and `src/adapters/anthropic.ts:535` (`resolveTranslatedAnthropicSessionKey(parsed, incoming)`). | Serialized into HTTP header `X-Claude-Code-Session-Id` via `claudeCodeSessionId(accessToken, sessionKey)` (`src/adapters/anthropic.ts:581`) and `body.metadata.user_id` JSON `session_id` (`src/adapters/anthropic/account-metadata.ts`). | N/A — outbound upstream wire header and JSON field only; OpenCodex does not deserialize its own outbound `X-Claude-Code-Session-Id`. | `buildAnthropicMessagesPassthroughRequest` (`src/adapters/anthropic/passthrough.ts:300-316`), `applyAnthropicOAuthAuth` (`src/adapters/anthropic.ts:574-583`), `bindAnthropicAccountMetadata` (`src/adapters/anthropic/account-metadata.ts:45-95`). |
+| `metadata.user_id.device_id` (`deriveAnthropicAccountDeviceId(accountUuid)`) | Created in `bindAnthropicAccountMetadata(body, providerAccountUuid)` (`src/adapters/anthropic/account-metadata.ts:28`) from `providerAccountUuid` (`src/adapters/anthropic/passthrough.ts:300`). | Serialized into `body.metadata.user_id` as JSON `{ ...identity, device_id: isolatedDeviceId, account_uuid: providerAccountUuid }` (`src/adapters/anthropic/account-metadata.ts:35-42`). | Parsed in unit/integration tests (`tests/adapters/anthropic/anthropic-account-metadata.test.ts:24`, `tests/claude-integration/messages-native-oauth.test.ts:356,376`). | `buildAnthropicMessagesPassthroughRequest` (`src/adapters/anthropic/passthrough.ts:300`) and upstream Anthropic `/v1/messages`. |
+
+## 3. Bypass & Enforcement Records (`PLAN-BYPASS-NAMED-01`)
+
+| Mechanism | Tier | Executing Surface | Known Bypass Path | Residual Risk | Wording Downgraded? | Final Layer |
+|---|---|---|---|---|---|---|
+| Per-account `device_id` & per-conversation `X-Claude-Code-Session-Id` isolation | E4 (Runtime request builder) | `bindAnthropicAccountMetadata` (`src/adapters/anthropic/account-metadata.ts`) and `applyAnthropicOAuthAuth` (`src/adapters/anthropic.ts`) | Caller-forward subscription passthrough (`src/server/claude-messages.ts:305` `anthropicNativePassthrough`) forwards the caller's own single-account bearer and body verbatim when not using the OpenCodex OAuth pool, or operator `provider.headers` explicitly overriding `X-Claude-Code-Session-Id`. | Single-account direct passthrough uses only the caller's own credential (no pool to link); operator header overrides are explicit local config. | Yes — described as "per-account device_id isolation for proxy-managed OAuth pools", not unbypassable enforcement. | `src/adapters/anthropic/passthrough.ts` & `src/adapters/anthropic.ts` |
+
+## 4. Before / After Diffs (`DIFFLEVEL-ROADMAP-01`)
+
+### 4.1 `src/providers/claude-cli-identity.ts`
+```diff
+ export const CLAUDE_CLI_VERSION = "2.1.280";
+ export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;
++export const CLAUDE_CLI_SDK_PACKAGE_VERSION = "0.112.1";
++export const CLAUDE_CLI_RUNTIME_VERSION = "v26.3.0";
+```
+
+### 4.2 `src/adapters/client-fingerprint.ts`
+```diff
+ import { createHash } from "node:crypto";
++import {
++ CLAUDE_CLI_RUNTIME_VERSION,
++ CLAUDE_CLI_SDK_PACKAGE_VERSION,
++} from "../providers/claude-cli-identity";
++
++export function stainlessOsName(platform: string = process.platform): string {
++ switch (platform.toLowerCase()) {
++ case "darwin": return "MacOS";
++ case "linux": return "Linux";
++ case "win32": return "Windows";
++ case "freebsd": return "FreeBSD";
++ case "openbsd": return "OpenBSD";
++ default: return platform ? `Other:${platform}` : "Unknown";
++ }
++}
++
++export function stainlessArchName(arch: string = process.arch): string {
++ if (arch === "x64" || arch === "x86_64") return "x64";
++ if (arch === "arm64" || arch === "aarch64") return "arm64";
++ if (arch === "arm") return "arm";
++ if (arch === "ia32" || arch === "x32") return "x32";
++ return arch ? `other:${arch}` : "unknown";
++}
+
+-// ── Claude Code CLI (matches Claude Code 2.1.63 / @anthropic-ai/sdk 0.74.0) ──
++// ── Claude Code CLI (matches Claude Code 2.1.280 / @anthropic-ai/sdk 0.112.1) ──
+ export const CLAUDE_CODE_HEADERS: Record = {
+ "X-App": "cli",
++ "anthropic-dangerous-direct-browser-access": "true",
+ "X-Stainless-Retry-Count": "0",
+ "X-Stainless-Runtime": "node",
+ "X-Stainless-Lang": "js",
+ "X-Stainless-Timeout": "600",
+- "X-Stainless-Arch": process.arch,
+- "X-Stainless-OS": process.platform,
+- "X-Stainless-Package-Version": "0.74.0",
+- "X-Stainless-Runtime-Version": process.version.slice(1),
++ "X-Stainless-Arch": stainlessArchName(),
++ "X-Stainless-OS": stainlessOsName(),
++ "X-Stainless-Package-Version": CLAUDE_CLI_SDK_PACKAGE_VERSION,
++ "X-Stainless-Runtime-Version": CLAUDE_CLI_RUNTIME_VERSION,
+ };
+
+-export function claudeCodeSessionId(token: string | undefined): string {
++export function claudeCodeSessionId(token: string | undefined, sessionKey?: string | null): string {
+ const seed = token && token.length > 0 ? token : "opencodex-anon";
+- const h = createHash("sha256").update(`claude-code-session:${seed}`, "utf8").digest("hex");
++ const conv = typeof sessionKey === "string" ? sessionKey.trim() : "";
++ const input = conv ? `claude-code-session:${seed}:conv:${conv}` : `claude-code-session:${seed}`;
++ const h = createHash("sha256").update(input, "utf8").digest("hex");
+ // Shape the hash into a v4-looking UUID (version nibble 4, variant nibble 8-b).
+ const variant = ((parseInt(h[16], 16) & 0x3) | 0x8).toString(16);
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
+ }
+```
+
+### 4.3 `src/adapters/anthropic/account-metadata.ts`
+```diff
++import { createHash } from "node:crypto";
+ const PROVIDER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ const MAX_USER_ID_BYTES = 4096;
+ type Rec = Record;
+ const isRec = (value: unknown): value is Rec => value !== null && typeof value === "object" && !Array.isArray(value);
+
++/** Deterministic per-account 64-hex device_id so pooled accounts never share one client device_id. */
++export function deriveAnthropicAccountDeviceId(accountUuid: string): string {
++ return createHash("sha256").update(`ocx-claude-code-device-v1:${accountUuid.trim().toLowerCase()}`, "utf8").digest("hex");
++}
++
++export function extractAnthropicCallerSessionId(body: Readonly): string | undefined {
++ if (!isRec(body.metadata) || typeof body.metadata.user_id !== "string") return undefined;
++ const userId = body.metadata.user_id;
++ if (Buffer.byteLength(userId) > MAX_USER_ID_BYTES) return undefined;
++ try {
++ const identity: unknown = JSON.parse(userId);
++ if (isRec(identity) && typeof identity.session_id === "string" && identity.session_id.trim()) {
++ return identity.session_id.trim();
++ }
++ } catch { /* ignore */ }
++ return undefined;
++}
++
+ /** Unknown formats stay untouched; the argument is the provider UUID, never the local pool id. */
+ export function bindAnthropicAccountMetadata(body: Rec, providerAccountUuid: string | undefined): Rec {
+ if (!providerAccountUuid || !PROVIDER_UUID.test(providerAccountUuid) || !isRec(body.metadata)) return body;
+ const userId = body.metadata.user_id;
+ if (typeof userId !== "string" || Buffer.byteLength(userId) > MAX_USER_ID_BYTES) return body;
+ let identity: unknown;
+ try { identity = JSON.parse(userId); } catch { return body; }
+- if (!isRec(identity) || typeof identity.account_uuid !== "string" || !PROVIDER_UUID.test(identity.account_uuid)
+- || identity.account_uuid === providerAccountUuid) return body;
+- return { ...body, metadata: { ...body.metadata, user_id: JSON.stringify({ ...identity, account_uuid: providerAccountUuid }) } };
++ if (!isRec(identity) || typeof identity.account_uuid !== "string" || !PROVIDER_UUID.test(identity.account_uuid)) return body;
++ const isolatedDeviceId = deriveAnthropicAccountDeviceId(providerAccountUuid);
++ const nextDevice = typeof identity.device_id === "string" ? isolatedDeviceId : undefined;
++ if (identity.account_uuid === providerAccountUuid && (nextDevice === undefined || identity.device_id === nextDevice)) return body;
++ return {
++ ...body,
++ metadata: {
++ ...body.metadata,
++ user_id: JSON.stringify({
++ ...identity,
++ ...(nextDevice !== undefined ? { device_id: nextDevice } : {}),
++ account_uuid: providerAccountUuid,
++ }),
++ },
++ };
+ }
+```
+
+### 4.4 `src/adapters/anthropic.ts`, `src/adapters/anthropic/passthrough.ts`, `src/adapters/anthropic/beta-allowlist.ts`, `src/vision/anthropic-describe.ts`, `src/web-search/anthropic-executor.ts`
+```diff
+--- a/src/adapters/anthropic.ts
++++ b/src/adapters/anthropic.ts
+@@ -30,6 +30,7 @@
+ import { redactSecretString } from "../lib/redact";
+ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
++import { CLAUDE_CLI_SDK_PACKAGE_VERSION, CLAUDE_CLI_USER_AGENT } from "../providers/claude-cli-identity";
++import { collectAnthropicCacheTtls } from "./anthropic/cache-retention";
+@@ -554,10 +555,10 @@
+ export function anthropicBaseRequestHeaders(stream: boolean | undefined): Record {
+ return {
+ "Content-Type": "application/json",
+ "anthropic-version": ANTHROPIC_API_VERSION,
+ "Accept": stream ? "text/event-stream" : "application/json",
+- "User-Agent": "@anthropic-ai/sdk/0.74.0",
++ "User-Agent": `@anthropic-ai/sdk/${CLAUDE_CLI_SDK_PACKAGE_VERSION}`,
+ };
+ }
+@@ -573,11 +574,23 @@
+-export function applyAnthropicOAuthAuth(headers: Record, accessToken: string): void {
++export function resolveTranslatedAnthropicSessionKey(parsed: OcxParsedRequest, incoming?: IncomingMeta): string | undefined {
++ return parsed._codexOwnThreadId?.trim()
++ || parsed._clientThreadId?.trim()
++ || (parsed.options.promptCacheKey?.trim() || undefined)
++ || incoming?.headers?.get("x-claude-code-session-id")?.trim()
++ || incoming?.headers?.get("session_id")?.trim()
++ || incoming?.headers?.get("session-id")?.trim()
++ || incoming?.headers?.get("thread-id")?.trim()
++ || undefined;
++}
++
++export function applyAnthropicOAuthAuth(headers: Record, accessToken: string, sessionKey?: string | null): void {
+ headers["Authorization"] = `Bearer ${accessToken}`;
+ headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA;
++ headers["User-Agent"] = CLAUDE_CLI_USER_AGENT;
+ Object.assign(headers, CLAUDE_CODE_HEADERS);
+- headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(accessToken);
++ headers["X-Claude-Code-Session-Id"] = claudeCodeSessionId(accessToken, sessionKey);
+ headers["x-client-request-id"] = crypto.randomUUID();
+ }
+```
+And in `src/adapters/anthropic.ts:1245-1262`, pass `resolveTranslatedAnthropicSessionKey(parsed, incoming)` to `applyAnthropicOAuthAuth`, and after `normalizeTtlOrdering(body)`, if `usesNativeAnthropicEndpoint(provider) && collectAnthropicCacheTtls(body).includes("1h")`, call `mergeAnthropicBetaHeader(headers, ["extended-cache-ttl-2025-04-11"])` (and `"effort-2025-11-24"` when `body.output_config` carries `effort`). In `parseStream` (`src/adapters/anthropic.ts:1548`), require `currentBlockType === "" && currentToolCallId === ""` and `!usesNativeAnthropicEndpoint(provider)` before allowing EOF without `message_stop` to emit `done`.
+
+## 5. Activation Scenarios (`C-ACTIVATION-GROUNDING-01`) & Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+1. **Conditional Path 1 — Per-conversation `X-Claude-Code-Session-Id` vs per-credential fallback**:
+ - *Trigger*: Build two translated OAuth requests with the same `accessToken` and different `_clientThreadId` (`thread-A` vs `thread-B`), two turns with the same `_clientThreadId` (`thread-A`), and one request with no thread/session key.
+ - *Observable Effect*: `thread-A` and `thread-B` produce distinct `X-Claude-Code-Session-Id` UUIDs; both turns of `thread-A` produce the identical UUID; the keyless request falls back to `claudeCodeSessionId(accessToken)`.
+2. **Conditional Path 2 — Per-account `device_id` isolation in `bindAnthropicAccountMetadata`**:
+ - *Trigger*: Pass the same native Messages `body` (`metadata.user_id` carrying `{ device_id: "caller-device", account_uuid: A, session_id: S }`) through `buildAnthropicMessagesPassthroughRequest` for pool account `A` and pool account `B`.
+ - *Observable Effect*: Account `A` receives `device_id: deriveAnthropicAccountDeviceId(A)` and account `B` receives `device_id: deriveAnthropicAccountDeviceId(B)` (`device_id` values are 64-hex strings and `!==`), while `source.metadata.user_id` remains unmutated.
+3. **Conditional Path 3 — `1h` cache TTL paired with `extended-cache-ttl-2025-04-11` and `message_stop` truncation check**:
+ - *Trigger*: Build a first-party Anthropic request with `cacheRetention: "long"` (or default 1h on `api.anthropic.com`) and feed a truncated SSE stream (`message_start` -> `content_block_start` -> `message_delta` -> EOF without `message_stop`) to `parseStream`.
+ - *Observable Effect*: `headers["anthropic-beta"]` includes `extended-cache-ttl-2025-04-11`, and the truncated first-party stream yields an `error` event instead of a false `done`.
+
+### Verifier Commands (Run & Verified in P Phase per `PLAN-VERIFIER-REAL-01`)
+- `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/clients/client-fingerprint.test.ts tests/adapters/anthropic/anthropic-account-metadata.test.ts tests/adapters/anthropic/anthropic-client-identity.test.ts tests/claude-integration/messages-native-oauth.test.ts tests/vision/vision-anthropic.test.ts` — baseline exit code `0`; reads targets directly via imports at `tests/clients/client-fingerprint.test.ts:2-8`, `tests/adapters/anthropic/anthropic-account-metadata.test.ts:2-3`, `tests/adapters/anthropic/anthropic-client-identity.test.ts:1-8`, `tests/claude-integration/messages-native-oauth.test.ts:1-20`, `tests/vision/vision-anthropic.test.ts:1-10`.
+- `bun run typecheck` — baseline exit code `0`; reads all `src/**/*.ts` and `tests/**/*.ts` via `tsconfig.json` `include`.
+- `bun run privacy:scan && bun run structure:check && bun test tests/ci-workflows/file-size-ratchet.test.ts` — baseline exit code `0`; scans repository files for secrets, structure ownership (`structure/manifest.json`), and file-size caps (`tests/fixtures/file-size-baseline.json`).
diff --git a/devlog/_plan/261028_oauth_provider_parity/020_phase2_codex_parity.md b/devlog/_plan/261028_oauth_provider_parity/020_phase2_codex_parity.md
new file mode 100644
index 00000000000..bcb7aa37ca7
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/020_phase2_codex_parity.md
@@ -0,0 +1,189 @@
+# 020 — Phase 2 (`wp2`): Codex (ChatGPT) OAuth Installation ID Isolation & Official JSON Token Refresh Parity
+
+- **Work-Phase ID**: `wp2`
+- **Depends On**: `wp1` (`010_phase1_claude_parity.md`)
+- **Shared Types / Interfaces Used**: `OAuthCredentials` (`src/oauth/types.ts`), `CodexAuthContext` (`src/codex/auth-context.ts`), `ApplyAccountChangeConversationStateScrubArgs` (`src/server/responses/account-change-state.ts:231`)
+- **Design Decisions Implemented**: `DEC-03`, `DEC-04`, `DEC-08`
+
+## 1. File Change Map
+
+| Action | File Path | Current Lines / Cap | Responsibility & Change Summary |
+|---|---|---:|---|
+| `MODIFY` | `src/codex/forward-transport-headers.ts` | 25 / 1999 | Add self-verifying idempotent `deriveCodexAccountInstallationId(chatgptAccountId, callerInstallationId)`, `rewriteTurnMetadataInstallationId(rawMetadata, chatgptAccountId)`, `isolateCodexPoolAccountHeaders(headers, chatgptAccountId)`, and `isolateCodexPoolClientMetadata(body, chatgptAccountId)`. |
+| `MODIFY` | `src/adapters/openai-responses/transport.ts` | 99 / 1999 | Add `"version"` to `FORWARD_HEADERS`; call `isolateCodexPoolAccountHeaders` (rewriting `x-codex-installation-id` & `x-codex-turn-metadata` and deleting `x-oai-attestation`) when `_codexAccountOverride` is active. |
+| `MODIFY` | `src/codex/auth-context.ts` | 1958 / 1999 | Call `isolateCodexPoolAccountHeaders(selected, ...)` in `materializeCodexUpstreamAuth` (lines 1811, 1841) and `materializeCodexUpstreamAuthAsync` (line 1925) whenever a pool or substituted main credential is materialized. |
+| `MODIFY` | `src/adapters/openai-responses/passthrough.ts` | 720 / 1999 | When `isCanonicalOpenAiForwardProvider(provider)` and a pooled/substituted account is active (`(provider as { _codexAccountOverride?: { chatgptAccountId: string } })._codexAccountOverride?.chatgptAccountId`), apply `isolateCodexPoolClientMetadata(finalBody, chatgptAccountId)` so `body.client_metadata["x-codex-installation-id"]` and `body.client_metadata["x-codex-turn-metadata"].installation_id` match the isolated header UUID. |
+| `MODIFY` | `src/server/responses/account-change-state.ts` | 313 / 1999 | Delete `x-codex-turn-state` from `body.client_metadata` in `scrubUnportableConversationStateInPlace` and from `args.headers` in `applyAccountChangeConversationStateScrub`; ensure `_stripNativeCompactionEncryptedContent` and `_stripReasoningEncryptedContent` still arm when `encryptedReasoning` is the sole unportable carrier. |
+| `MODIFY` | `src/server/responses/core-codex-account.ts` | 1203 / 1999 | Pass `headers: retryHeaders` into `applyAccountChangeConversationStateScrub` at line 919 so in-request pool failover strips `x-codex-turn-state` from the retry headers. |
+| `MODIFY` | `src/server/responses/request-prepare.ts` | 1660 / 1999 | Pass `headers: selectedForwardHeaders` into `applyAccountChangeConversationStateScrub` at line 1603. |
+| `MODIFY` | `src/oauth/chatgpt.ts` | 284 / 1999 | Update `ORIGINATOR = "codex_cli_rs"` (line 118); update `chatGptTokenRequest` (line 30) to support JSON body (`Content-Type: application/json`, `Accept: application/json`) for `refreshChatGPTToken` while preserving `application/x-www-form-urlencoded` (+ `Accept: application/json`) for `exchangeToken`. |
+| `MODIFY` | `src/codex/account-store.ts` | 1520 / 1999 | Switch pool token refresh (line 1378) to `Content-Type: application/json` + `Accept: application/json` with `JSON.stringify({ client_id, grant_type: "refresh_token", refresh_token })`, compose a 30s timeout with `signal`, and use `readBoundedResponseBody` (`maxBytes: 64 * 1024`). |
+| `MODIFY` | `src/codex/main-account.ts` | 485 / 1999 | Switch `refreshNativeMainGrant` (line 114) to `Content-Type: application/json` + `Accept: application/json` with JSON body, 30s timeout, and bounded 64KB response read. |
+| `MODIFY` | `tests/oauth/chatgpt-oauth.test.ts` | 120 / 1999 | Update and add regression tests for JSON `refreshChatGPTToken`, `ORIGINATOR = "codex_cli_rs"`, and per-account `deriveCodexAccountInstallationId` / `isolateCodexPoolAccountHeaders` / `isolateCodexPoolClientMetadata`. |
+| `MODIFY` | `tests/oauth/chatgpt-token-expiry.test.ts` | 235 / 1999 | Update refresh body expectation to JSON `{ client_id, grant_type: "refresh_token", refresh_token }` while keeping form-urlencoded for browser code exchange. |
+| `MODIFY` | `tests/oauth/chatgpt-token-boundaries.test.ts` | 190 / 1999 | Verify JSON refresh headers (`Content-Type: application/json`, `Accept: application/json`) and bounded response handling. |
+
+## 2. Field and Option Chains (`PLAN-FIELD-CHAIN-01`)
+
+| Field / Option | 1. Creation | 2. Serialization | 3. Deserialization | 4. Consumers |
+|---|---|---|---|---|
+| `ApplyAccountChangeConversationStateScrubArgs.headers?: Headers` | Created in `src/server/responses/request-prepare.ts:1603` (`headers: selectedForwardHeaders`) and `src/server/responses/core-codex-account.ts:919` (`headers: retryHeaders`). | Mutates outbound `Headers` by deleting `x-codex-turn-state` when `accountChanged` is true (`src/server/responses/account-change-state.ts:258`). | N/A — in-memory `Headers` instance passed to `buildRequest`. | `applyAccountChangeConversationStateScrub` (`src/server/responses/account-change-state.ts:248-277`) and downstream `buildResponsesTransport` (`src/adapters/openai-responses/transport.ts:40`). |
+| `chatGptTokenRequest` `body: URLSearchParams \| Record` | Created in `ChatGPTOAuthFlow.exchangeToken` (`URLSearchParams`, `src/oauth/chatgpt.ts:245`) and `refreshChatGPTToken` (`{ client_id, grant_type, refresh_token }`, `src/oauth/chatgpt.ts:278`). | Serialized as `body.toString()` with `Content-Type: application/x-www-form-urlencoded` when `body instanceof URLSearchParams`, or `JSON.stringify(body)` with `Content-Type: application/json` otherwise (`src/oauth/chatgpt.ts:45-52`). | Deserialized by `auth.openai.com/oauth/token` and mock servers in `tests/oauth/chatgpt-token-expiry.test.ts` & `tests/oauth/chatgpt-token-boundaries.test.ts`. | `chatGptTokenRequest` (`src/oauth/chatgpt.ts:30-70`). |
+
+## 3. Bypass & Enforcement Records (`PLAN-BYPASS-NAMED-01`)
+
+| Mechanism | Tier | Executing Surface | Known Bypass Path | Residual Risk | Wording Downgraded? | Final Layer |
+|---|---|---|---|---|---|---|
+| Per-account `x-codex-installation-id` & `x-codex-turn-metadata.installation_id` isolation | E4 (Runtime transport & auth materializer) | `isolateCodexPoolAccountHeaders` & `isolateCodexPoolClientMetadata` (`src/codex/forward-transport-headers.ts`) | Direct caller-owned mode (`authCtx.kind === "main"` with `substituteMainCredential === false`) intentionally forwards the caller's own bearer and installation ID to that single caller's account. | None for pooled accounts: every `pool`, `main-pool`, and `substituteMainCredential` path runs through `materializeCodexUpstreamAuth` and `buildResponsesTransport`. | Yes — described as "per-account installation ID isolation for pooled and substituted ChatGPT credentials". | `src/codex/auth-context.ts` & `src/adapters/openai-responses/transport.ts` |
+
+## 4. Before / After Diffs (`DIFFLEVEL-ROADMAP-01`)
+
+### 4.1 `src/codex/forward-transport-headers.ts`
+```diff
++import { createHmac } from "node:crypto";
++
+ /** Native request metadata shared by the HTTP adapter and WS preparation. */
+ export const CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
+ export const CODEX_RESPONSES_LITE_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
+ export const CODEX_ROUTING_HINT_HEADER = "x-codex-routing-hint";
++export const CODEX_INSTALLATION_ID_HEADER = "x-codex-installation-id";
++export const CODEX_TURN_METADATA_HEADER = "x-codex-turn-metadata";
++export const CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
++export const CODEX_ATTESTATION_HEADER = "x-oai-attestation";
++
++const INSTALL_BODY_KEY = "opencodex-codex-pool-install-body-v1";
++const INSTALL_TAG_KEY = "opencodex-codex-pool-install-tag-v1";
++const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
++const MAX_TURN_METADATA_BYTES = 32 * 1024;
++
++function accountTagForUuidPrefix(account: string, prefix20Hex: string): string {
++ return createHmac("sha256", INSTALL_TAG_KEY).update(`${account}\0${prefix20Hex.toLowerCase()}`).digest("hex").slice(0, 12);
++}
++
++/**
++ * Deterministic, idempotent RFC 4122 v4 UUID isolated per (chatgptAccountId, callerInstallationId).
++ * Calling it twice with the same chatgptAccountId returns the identical UUID without double-hashing.
++ */
++export function deriveCodexAccountInstallationId(chatgptAccountId: string, callerInstallationId: string): string {
++ const account = chatgptAccountId.trim();
++ const raw = callerInstallationId.trim();
++ if (!account || !raw) return callerInstallationId;
++ if (UUID_V4_RE.test(raw)) {
++ const hex = raw.replace(/-/g, "").toLowerCase();
++ if (hex.slice(20, 32) === accountTagForUuidPrefix(account, hex.slice(0, 20))) {
++ return raw.toLowerCase();
++ }
++ }
++ const h = createHmac("sha256", INSTALL_BODY_KEY).update(`${account}\0${raw}`).digest("hex");
++ const variant = ((Number.parseInt(h[16]!, 16) & 0x3) | 0x8).toString(16);
++ const prefix20 = `${h.slice(0, 8)}${h.slice(8, 12)}4${h.slice(13, 16)}${variant}${h.slice(17, 20)}`;
++ const node12 = accountTagForUuidPrefix(account, prefix20);
++ return `${prefix20.slice(0, 8)}-${prefix20.slice(8, 12)}-${prefix20.slice(12, 16)}-${prefix20.slice(16, 20)}-${node12}`;
++}
++
++export function rewriteTurnMetadataInstallationId(rawMetadata: string, chatgptAccountId: string): string {
++ if (!chatgptAccountId.trim() || Buffer.byteLength(rawMetadata, "utf8") > MAX_TURN_METADATA_BYTES) return rawMetadata;
++ let parsed: unknown;
++ try { parsed = JSON.parse(rawMetadata); } catch { return rawMetadata; }
++ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return rawMetadata;
++ const record = parsed as Record;
++ if (typeof record.installation_id !== "string" || !record.installation_id.trim()) return rawMetadata;
++ const nextId = deriveCodexAccountInstallationId(chatgptAccountId, record.installation_id);
++ if (nextId === record.installation_id) return rawMetadata;
++ return JSON.stringify({ ...record, installation_id: nextId });
++}
++
++export function isolateCodexPoolAccountHeaders(headers: Headers, chatgptAccountId: string): void {
++ headers.delete(CODEX_ATTESTATION_HEADER);
++ if (!chatgptAccountId.trim()) return;
++ const installId = headers.get(CODEX_INSTALLATION_ID_HEADER);
++ if (installId && installId.trim()) {
++ headers.set(CODEX_INSTALLATION_ID_HEADER, deriveCodexAccountInstallationId(chatgptAccountId, installId));
++ }
++ const turnMeta = headers.get(CODEX_TURN_METADATA_HEADER);
++ if (turnMeta) {
++ headers.set(CODEX_TURN_METADATA_HEADER, rewriteTurnMetadataInstallationId(turnMeta, chatgptAccountId));
++ }
++}
++
++export function isolateCodexPoolClientMetadata(body: unknown, chatgptAccountId: string | undefined): unknown {
++ if (!chatgptAccountId?.trim() || !body || typeof body !== "object" || Array.isArray(body)) return body;
++ const record = body as Record;
++ const cm = record.client_metadata;
++ if (!cm || typeof cm !== "object" || Array.isArray(cm)) return body;
++ const meta = cm as Record;
++ let nextMeta: Record | undefined;
++ const rawInstallId = meta[CODEX_INSTALLATION_ID_HEADER];
++ if (typeof rawInstallId === "string" && rawInstallId.trim()) {
++ const nextId = deriveCodexAccountInstallationId(chatgptAccountId, rawInstallId);
++ if (nextId !== rawInstallId) nextMeta = { ...meta, [CODEX_INSTALLATION_ID_HEADER]: nextId };
++ }
++ const rawTurnMeta = (nextMeta ?? meta)[CODEX_TURN_METADATA_HEADER];
++ if (typeof rawTurnMeta === "string" && rawTurnMeta.trim()) {
++ const rewritten = rewriteTurnMetadataInstallationId(rawTurnMeta, chatgptAccountId);
++ if (rewritten !== rawTurnMeta) nextMeta = { ...(nextMeta ?? meta), [CODEX_TURN_METADATA_HEADER]: rewritten };
++ }
++ return nextMeta ? { ...record, client_metadata: nextMeta } : body;
++}
+```
+
+### 4.2 `src/oauth/chatgpt.ts`, `src/codex/account-store.ts`, `src/codex/main-account.ts`
+```diff
+--- a/src/oauth/chatgpt.ts
++++ b/src/oauth/chatgpt.ts
+@@ -30,7 +30,7 @@
+ async function chatGptTokenRequest(
+- body: URLSearchParams,
++ body: URLSearchParams | Record,
+ label: "ChatGPT refresh" | "ChatGPT token exchange",
+ options: { signal?: AbortSignal; timeoutMs?: number },
+ ): Promise {
+@@ -41,9 +41,13 @@
+ try {
+ controller.signal.throwIfAborted();
++ const isForm = body instanceof URLSearchParams;
+ const response = await fetch(TOKEN_URL, {
+ method: "POST",
+- headers: { "Content-Type": "application/x-www-form-urlencoded" },
+- body: body.toString(),
++ headers: {
++ "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
++ Accept: "application/json",
++ },
++ body: isForm ? body.toString() : JSON.stringify(body),
+ signal: controller.signal,
+ });
+@@ -115,1 +119,1 @@
+-const ORIGINATOR = "opencodex";
++const ORIGINATOR = "codex_cli_rs";
+@@ -274,9 +278,9 @@
+ export async function refreshChatGPTToken(
+ refreshToken: string,
+ options: { signal?: AbortSignal; timeoutMs?: number } = {},
+ ): Promise {
+- return chatGptTokenRequest(new URLSearchParams({
++ return chatGptTokenRequest({
++ client_id: CLIENT_ID,
+ grant_type: "refresh_token",
+- client_id: CLIENT_ID,
+ refresh_token: refreshToken,
+- }), "ChatGPT refresh", options);
++ }, "ChatGPT refresh", options);
+ }
+```
+
+## 5. Activation Scenarios (`C-ACTIVATION-GROUNDING-01`) & Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+1. **Conditional Path 1 — Per-account installation ID isolation and idempotency**:
+ - *Trigger*: Materialize headers via `materializeCodexUpstreamAuth` for pool account `acct-1` and `acct-2` from a request carrying `x-codex-installation-id: 11111111-1111-4111-8111-111111111111`, `x-codex-turn-metadata: {"installation_id":"11111111-1111-4111-8111-111111111111"}`, `x-oai-attestation: att-1`, and `body.client_metadata`, then pass through `createResponsesPassthroughAdapter.buildRequest`.
+ - *Observable Effect*: `acct-1` and `acct-2` receive distinct valid v4 UUIDs in `x-codex-installation-id`, `x-codex-turn-metadata.installation_id`, and `body.client_metadata["x-codex-installation-id"]`; within `acct-1`, the HTTP header and `body.client_metadata` UUIDs are byte-identical (no double-hashing); `x-oai-attestation` is removed on pool overrides and preserved on direct caller-owned sends.
+2. **Conditional Path 2 — `x-codex-turn-state` scrub on account change**:
+ - *Trigger*: Serve turn 1 on `acct-1`, then switch to `acct-2` on turn 2 with `x-codex-turn-state: ts-acct-1` in headers and `body.client_metadata`.
+ - *Observable Effect*: `applyAccountChangeConversationStateScrub` removes `x-codex-turn-state` from both outbound headers and `body.client_metadata`.
+3. **Conditional Path 3 — Official JSON token refresh contract**:
+ - *Trigger*: Call `refreshChatGPTToken`, `getValidCodexToken` (pool refresh), and `refreshNativeMainGrant` against a mock token server.
+ - *Observable Effect*: Refresh requests send `Content-Type: application/json` and `Accept: application/json` with JSON body `{"client_id":"app_EMoamEEZ73f0CkXaXp7hrann","grant_type":"refresh_token","refresh_token":"..."}`, while `exchangeToken` sends `application/x-www-form-urlencoded`.
+
+### Verifier Commands (Run & Verified in P Phase per `PLAN-VERIFIER-REAL-01`)
+- `bun test tests/oauth/chatgpt-oauth.test.ts tests/oauth/chatgpt-token-expiry.test.ts tests/oauth/chatgpt-token-boundaries.test.ts` — baseline exit code `0`; reads `src/oauth/chatgpt.ts` and `src/codex/account-store.ts` directly.
+- `bun run typecheck && bun run privacy:scan && bun run structure:check && bun test tests/ci-workflows/file-size-ratchet.test.ts` — baseline exit code `0`.
diff --git a/devlog/_plan/261028_oauth_provider_parity/030_phase3_google_cursor_parity.md b/devlog/_plan/261028_oauth_provider_parity/030_phase3_google_cursor_parity.md
new file mode 100644
index 00000000000..e3ce96cf11c
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/030_phase3_google_cursor_parity.md
@@ -0,0 +1,148 @@
+# 030 — Phase 3 (`wp3`): Google OAuth (Gemini CLI / Antigravity / Cloud Code) & Cursor Parity and Account Isolation
+
+- **Work-Phase ID**: `wp3`
+- **Depends On**: `wp2` (`020_phase2_codex_parity.md`)
+- **Shared Types / Interfaces Used**: `OcxParsedRequest` (`src/types.ts`), `OAuthCredentials` (`src/oauth/types.ts`), `CursorTransportFactoryInput` (`src/adapters/cursor/transport.ts`)
+- **Design Decisions Implemented**: `DEC-05`, `DEC-06`, `DEC-08`
+
+## 1. File Change Map
+
+| Action | File Path | Current Lines / Cap | Responsibility & Change Summary |
+|---|---|---:|---|
+| `MODIFY` | `src/adapters/google-antigravity-wire.ts` | 221 / 1999 | Reject `"context_engineering_is_the_way_to_go"` in `isLikelyRealThoughtSignature`; accept `accountScope?: string` in `antigravitySessionId(parsed, accountScope)` so pooled Google Antigravity accounts emit isolated per-account/project `request.sessionId` values. |
+| `MODIFY` | `src/adapters/google-antigravity-replay.ts` | 910 / 1999 | Export `THOUGHT_SIGNATURE_CONTEXT_BYPASS = "context_engineering_is_the_way_to_go"` and reject it in `extractSignature` and `loadReplaySnapshotEntry`. |
+| `MODIFY` | `src/adapters/google-wire-compiler.ts` | 215 / 1999 | Preserve `thinkingBudget` (or `thinking_budget`) in `compileGenerationConfig` when `thinkingLevel` is not set. |
+| `MODIFY` | `src/adapters/google.ts` | 1805 / 1999 | Pass `provider.project` into `antigravitySessionId(parsed, provider.project)` at line 1129 and scope `antigravityModel` by project (`${project}:${wireModelId}`) at line 1136 so the thoughtSignature replay cache never leaks across pooled Google accounts. |
+| `MODIFY` | `src/oauth/google-antigravity.ts` | 310 / 1999 | Extract `sub` from `id_token` as `accountId` in `credentialsFromPayload`; select default tier from `allowedTiers` (`isDefault === true`) in `loadCodeAssistProject` and pass to `onboardProject`; preserve OAuth error code (`error` / `error_description`) in `postToken` on non-2xx responses. |
+| `MODIFY` | `src/codex/catalog/provider-models.ts` | 910 / 1999 | Compute `catalogAuthority` for `cloudCodeAssist` (`google-antigravity`) from `(auth.oauthAccountId ?? apiKey, project)` at line 652 so per-account model discovery caches do not bleed across pooled accounts. |
+| `MODIFY` | `src/providers/quota/antigravity.ts` | 355 / 1999 | Pass `ANTIGRAVITY_DISCOVERY_UA` (`antigravity/cli/1.3.4 ...`) when falling back to `:fetchAvailableModels` at line 333 so Gemini 4 Argon quota rows are preserved. |
+| `MODIFY` | `src/adapters/cursor/h2-pool.ts` | 115 / 1999 | Accept `credentialScope?: string` in `CursorH2SessionPool.request(url, headers, credentialScope)` and key pool entries by `${origin}\0${credentialScope ?? ""}` so pooled Cursor accounts never share an HTTP/2 TLS connection. |
+| `MODIFY` | `src/adapters/cursor/live-models.ts` | 265 / 1999 | Unify `CURSOR_DISCOVERY_CLIENT_VERSION` with `CURSOR_CLIENT_VERSION` (`"cli-2026.07.08-0c04a8a"`), derive a deterministic per-account `x-session-id` in `cursorDiscoveryHeaders`, and pass a hashed credential scope to `cursorH2Pool.request`. |
+| `MODIFY` | `src/oauth/cursor.ts` | 260 / 1999 | Export `decodeCursorJwtPayload` (line 41) so `src/adapters/cursor.ts` can read the stable JWT `sub` claim one-way (`adapters -> oauth`, no import cycle). |
+| `MODIFY` | `src/adapters/cursor.ts` | 740 / 1999 | Derive fallback `_parsed._cursorIdentityScope` from the JWT `sub` claim (via `decodeCursorJwtPayload` from `../oauth/cursor`) before falling back to `sha256(token)` so token refresh preserves checkpoints and thread continuity; pass `status` on preflight error events. |
+| `MODIFY` | `src/adapters/cursor/live-transport.ts` | 1786 / 1999 | Scope `cursorContextUsageTracker` keys by `${request._cursorIdentityScope ?? "local"}\0${request.conversationId}`; surface HTTP `:status` 401/403/429 before Connect frames. |
+| `MODIFY` | `src/adapters/cursor/http1-bidi.ts` | 260 / 1999 | Add `"x-cursor-streaming": "true"` in `commonHeaders` for HTTP/1.1 `RunSSE` requests. |
+| `MODIFY` | `src/providers/quota/vendor-probes-oauth.ts` | 593 / 1999 | Remove the giveaway `"User-Agent": "opencodex-quota"` header on `fetchCursorQuota` (line 440). |
+| `MODIFY` | `tests/adapters/google/google-antigravity-wire.test.ts` | 240 / 1999 | Add red-to-green tests for per-account/project `antigravitySessionId` isolation and `context_engineering_is_the_way_to_go` rejection. |
+| `MODIFY` | `tests/adapters/google/google-wire-compiler.test.ts` | 120 / 1999 | Add red-to-green tests for `thinkingBudget` preservation. |
+| `MODIFY` | `tests/adapters/google/google-antigravity-oauth.test.ts` | 180 / 1999 | Add red-to-green tests for `allowedTiers` default tier onboarding, `id_token` `sub` extraction, and `invalid_grant` error code preservation. |
+| `MODIFY` | `tests/providers/cursor/cursor-adapter.test.ts` | 420 / 1999 | Add red-to-green tests for JWT `sub`-based `_cursorIdentityScope` stability across token refresh, per-credential `cursorH2Pool` isolation, and HTTP/1.1 `x-cursor-streaming: "true"`. |
+
+## 2. Field and Option Chains (`PLAN-FIELD-CHAIN-01`)
+
+| Field / Option | 1. Creation | 2. Serialization | 3. Deserialization | 4. Consumers |
+|---|---|---|---|---|
+| `antigravitySessionId(parsed, accountScope?)` | Created in `src/adapters/google.ts:1129` (`antigravitySessionId(parsed, provider.project)`). | Serialized into `envelope.request.sessionId` (`src/adapters/google.ts:1148`). | N/A — outbound wire field to Google Cloud Code Assist. | `createGoogleAdapter.buildRequest` (`src/adapters/google.ts:1129-1180`) and `tests/adapters/google/google-antigravity-wire.test.ts`. |
+| `CursorH2SessionPool.request(url, headers, credentialScope?)` | Created in `src/adapters/cursor/live-models.ts:212` (`createHash("sha256").update(opts.apiKey).digest("hex").slice(0, 16)`). | N/A — in-memory pool map key `${origin}\0${credentialScope ?? ""}`. | N/A — in-memory map lookup (`src/adapters/cursor/h2-pool.ts:44,77`). | `CursorH2SessionPool` (`src/adapters/cursor/h2-pool.ts`) and `fetchCursorUsableModelsHttp2Once` (`src/adapters/cursor/live-models.ts:212`). |
+
+## 3. Bypass & Enforcement Records (`PLAN-BYPASS-NAMED-01`)
+
+| Mechanism | Tier | Executing Surface | Known Bypass Path | Residual Risk | Wording Downgraded? | Final Layer |
+|---|---|---|---|---|---|---|
+| Per-account/project `antigravitySessionId` & replay cache isolation and per-credential `cursorH2Pool` isolation | E4 (Runtime adapter & transport pool) | `src/adapters/google.ts`, `src/adapters/google-antigravity-wire.ts`, `src/adapters/cursor/h2-pool.ts`, `src/adapters/cursor.ts` | Direct caller call to `antigravitySessionId(parsed)` without `accountScope` or `_reasoningReplayScope` (e.g. unit tests calling the pure helper with one argument) keeps the un-scoped hash for backward compatibility. | Production `google.ts:1129` always supplies `provider.project` (which is required on line 1128). | Yes — described as "per-project session and replay isolation in the Google CCA adapter". | `src/adapters/google.ts:1129-1180` & `src/adapters/cursor/h2-pool.ts:38-55` |
+
+## 4. Before / After Diffs (`DIFFLEVEL-ROADMAP-01`)
+
+### 4.1 `src/adapters/google-antigravity-wire.ts` & `src/adapters/google.ts`
+```diff
+--- a/src/adapters/google-antigravity-wire.ts
++++ b/src/adapters/google-antigravity-wire.ts
+@@ -54,5 +54,5 @@
+ // evidence that a turn was signed. It is never a real signature.
+- if (sig === "skip_thought_signature_validator") return false;
++ if (sig === "skip_thought_signature_validator" || sig === "context_engineering_is_the_way_to_go") return false;
+ // Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
+@@ -83,5 +83,8 @@
+-export function antigravitySessionId(parsed: OcxParsedRequest): string {
++export function antigravitySessionId(parsed: OcxParsedRequest, accountScope?: string): string {
+ const text = codexThreadAnchor(parsed).preimage ?? firstUserText(parsed);
+ if (!text) return `-${Math.floor(Math.random() * 9e18).toString()}`;
+- const digest = createHash("sha256").update(text, "utf8").digest();
++ const scope = parsed._reasoningReplayScope?.identity?.credentialDurableIdentity?.trim()
++ || accountScope?.trim()
++ || "";
++ const preimage = scope ? `acct:${scope}\0${text}` : text;
++ const digest = createHash("sha256").update(preimage, "utf8").digest();
+ const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
+ return `-${masked.toString()}`;
+ }
+```
+```diff
+--- a/src/adapters/google.ts
++++ b/src/adapters/google.ts
+@@ -1127,10 +1127,10 @@
+ const project = provider.project;
+ if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`).");
+- const sessionId = antigravitySessionId(parsed);
++ const sessionId = antigravitySessionId(parsed, project);
+ const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
+ const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
+ parsed.modelId,
+ mappedEffort,
+ provider.baseUrl,
+ );
+- antigravityModel = wireModelId;
++ antigravityModel = `${project}:${wireModelId}`;
+ antigravitySession = sessionId;
+@@ -1179,3 +1179,3 @@
+ if (antigravityUsesReplayCache(wireModelId)) {
+- applyAntigravityReplay(wireModelId, sessionId, contents);
++ applyAntigravityReplay(antigravityModel, sessionId, contents);
+ } else {
+```
+
+### 4.2 `src/adapters/google-wire-compiler.ts`
+```diff
+--- a/src/adapters/google-wire-compiler.ts
++++ b/src/adapters/google-wire-compiler.ts
+@@ -173,5 +173,9 @@
+ if (thinkingLevel) thinking.thinkingLevel = thinkingLevel;
+ }
++ const rawBudget = finiteNumber(value.thinkingConfig.thinkingBudget ?? value.thinkingConfig.thinking_budget);
++ if (!thinking.thinkingLevel && rawBudget !== undefined && (rawBudget === -1 || rawBudget >= 0)) {
++ thinking.thinkingBudget = Math.floor(rawBudget);
++ }
+ if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true;
+```
+
+### 4.3 `src/adapters/cursor/h2-pool.ts`, `src/adapters/cursor.ts`, `src/adapters/cursor/http1-bidi.ts`
+```diff
+--- a/src/adapters/cursor/h2-pool.ts
++++ b/src/adapters/cursor/h2-pool.ts
+@@ -38,8 +38,9 @@
+ request(
+ url: string,
+ headers: http2.OutgoingHttpHeaders,
++ credentialScope?: string,
+ ): Promise<{ session: http2.ClientHttp2Session; stream: http2.ClientHttp2Stream }> {
+- const origin = new URL(url).origin;
++ const origin = new URL(url).origin;
++ const poolKey = credentialScope ? `${origin}\0${credentialScope}` : origin;
+```
+```diff
+--- a/src/adapters/cursor/http1-bidi.ts
++++ b/src/adapters/cursor/http1-bidi.ts
+@@ -220,6 +220,7 @@
+ "connect-protocol-version": "1",
+ authorization: `Bearer ${this.options.token}`,
+ "x-ghost-mode": "true",
++ "x-cursor-streaming": "true",
+ "x-cursor-client-version": this.options.clientVersion,
+ "x-cursor-client-type": "cli",
+```
+
+## 5. Activation Scenarios (`C-ACTIVATION-GROUNDING-01`) & Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+1. **Conditional Path 1 — Per-project `antigravitySessionId` and replay cache isolation**:
+ - *Trigger*: Observe a Gemini `thoughtSignature` on `project-A` for thread `T`, then build a request for `project-B` on the same thread `T`.
+ - *Observable Effect*: `project-A` and `project-B` receive different `request.sessionId` values, and `project-A`'s cached signature is not injected into `project-B`'s `contents`, while a second turn on `project-A` does receive the cached signature.
+2. **Conditional Path 2 — `thinkingBudget` preservation and `context_engineering_is_the_way_to_go` sentinel filtering**:
+ - *Trigger*: Compile a Gemini body with `generationConfig: { thinkingConfig: { thinkingBudget: 8192, includeThoughts: true } }`, and test `isLikelyRealThoughtSignature("context_engineering_is_the_way_to_go")`.
+ - *Observable Effect*: `compiled.body.generationConfig.thinkingConfig` retains `{ thinkingBudget: 8192, includeThoughts: true }`, and the bypass sentinel returns `false`.
+3. **Conditional Path 3 — Cursor JWT `sub` identity scope stability & per-credential HTTP/2 pool isolation**:
+ - *Trigger*: Run two turns of a Cursor conversation where the JWT `accessToken` rotates (`exp` changes) while `sub` (`"github|12345"`) stays the same; then run `fetchCursorUsableModelsOnce` for two different Cursor tokens.
+ - *Observable Effect*: `_parsed._cursorIdentityScope` remains identical across the token refresh (preserving the committed checkpoint), and `cursorH2Pool` opens separate HTTP/2 sessions per token digest.
+
+### Verifier Commands (Run & Verified in P Phase per `PLAN-VERIFIER-REAL-01`)
+- `env -u GOOGLE_ANTIGRAVITY_USER_AGENT bun test tests/adapters/google/google-antigravity-wire.test.ts tests/adapters/google/google-antigravity-replay.test.ts tests/adapters/google/google-antigravity-oauth.test.ts tests/adapters/google/google-wire-compiler.test.ts tests/providers/cursor/cursor-adapter.test.ts tests/providers/cursor/cursor-discovery.test.ts` — baseline exit code `0`.
+- `bun run typecheck && bun run privacy:scan && bun run structure:check && bun test tests/ci-workflows/file-size-ratchet.test.ts` — baseline exit code `0`.
diff --git a/devlog/_plan/261028_oauth_provider_parity/040_phase4_pool_protection.md b/devlog/_plan/261028_oauth_provider_parity/040_phase4_pool_protection.md
new file mode 100644
index 00000000000..bce6d1160eb
--- /dev/null
+++ b/devlog/_plan/261028_oauth_provider_parity/040_phase4_pool_protection.md
@@ -0,0 +1,139 @@
+# 040 — Phase 4 (`wp4`): Multi-Account Pool Protection (Cooldowns, Session Pinning & Suspended Account Auto-Exclusion)
+
+- **Work-Phase ID**: `wp4`
+- **Depends On**: `wp3` (`030_phase3_google_cursor_parity.md`)
+- **Shared Types / Interfaces Used**: `QuotaModelFamily` (`src/oauth/account-quota-rank.ts`), `ChatgptRefreshDiagnosticCode` (`src/codex/chatgpt-refresh-failure.ts`), `AdapterEvent` (`src/types.ts`)
+- **Design Decisions Implemented**: `DEC-07`, `DEC-08`
+
+## 1. File Change Map
+
+| Action | File Path | Current Lines / Cap | Responsibility & Change Summary |
+|---|---|---:|---|
+| `MODIFY` | `src/oauth/generic-account-failover.ts` | 896 / 1999 | In `isCooled(provider, accountId, now, family)` (line 120), also check bare account-wide cooldown `isCooled(provider, accountId, now, undefined)` when `family !== undefined`; in `rotateAntigravityAccountOnAuthRefusal` (line 418), record both family and bare cooldowns and invalidate `presence.delete("google-antigravity")` (without importing `./generic-session-affinity`, preserving `INV-LAYER-01` cycle-free invariant); add `rotateGenericAccountOnAuthRefusal` for generic 403/auth refusals. |
+| `MODIFY` | `src/oauth/generic-session-affinity.ts` | 125 / 1999 | Export `clearGenericSessionAffinityForAccount(provider: string, accountId: string)` so server-layer auth/403/429 rotations and `readBinding` evict sessions pinned to a cooled or suspended account without introducing an import cycle. |
+| `MODIFY` | `src/oauth/account-quota-rank.ts` | 310 / 1999 | Extend `exhaustedCooldownMs` (line 295) to support `google-antigravity` using `accountResetTimestamp` when headroom is 0. |
+| `MODIFY` | `src/adapters/google-errors.ts` | 175 / 1999 | Export `extractGoogleRetryAfterSeconds(payloadText: string): string \| undefined` to parse `RetryInfo.retryDelay` or `ErrorInfo.metadata.quotaResetDelay` from 429 JSON error bodies. |
+| `MODIFY` | `src/adapters/google-http.ts` | 240 / 1999 | When an Antigravity or Vertex 429 response has no `Retry-After` header, populate `Retry-After` from `extractGoogleRetryAfterSeconds(payloadText)` so `rotateGenericOAuthAccountOn429` cools for the server-stated reset window instead of 60s. |
+| `MODIFY` | `src/oauth/cursor.ts` | 260 / 1999 | In `refreshCursorToken` (line 237), classify HTTP 400/401/403 and `data.shouldLogout === true` with `(invalid_grant)` in the error message so `isTerminalRefreshError` (`src/oauth/index.ts:778`) marks revoked/logged-out Cursor accounts `needsReauth`. |
+| `MODIFY` | `src/oauth/index.ts` | 1984 / 1999 | Add `"cursor"` to `FORCE_REFRESH_PROVIDERS` (line 728). (1 line change; file stays at 1985 / 1999 lines.) |
+| `MODIFY` | `src/server/responses/request-transport.ts` | 1425 / 1999 | Add `route.providerName === "cursor"` to `isOAuth401ReplayProvider` (line 191); generalize line 1073 terminal-refresh fallback from `kiro`-only to `tryAlternateAfterTerminalRefresh(config, route.providerName, failedId, failedGeneration)`. |
+| `MODIFY` | `src/adapters/cursor/cursor-errors.ts` | 350 / 1999 | Distinguish `permission_denied` / `forbidden` / `access denied` as `"Cursor access denied (HTTP 403)"` (403) instead of `"Cursor authentication failed"` (401), and match `"failed with http 401|403|429"` from HTTP/1.1 & HTTP/2 transports. |
+| `MODIFY` | `src/server/responses/run-turn-execution.ts` | 1120 / 1999 | In `recoverRunTurnAdapterOnPreflight401` (line 495), resolve `const status = error.status ?? adapterFailureFromMessage(error.message).httpStatus`; add `rotateRunTurnAdapterOnPreflight403` to cool and rotate on preflight 403 refusals. |
+| `MODIFY` | `src/oauth/anthropic-account-refusal.ts` | 232 / 1999 | In `rotateAnthropicAccountOnResponseForInstance` (line 121), call `routing.clearAnthropicSessionAffinityForAccount(sent.accountId)` when `wasAccountAutoPaused(response)` is true. |
+| `MODIFY` | `src/codex/chatgpt-refresh-failure.ts` | 60 / 1999 | Add `"account_deactivated"` and `"deactivated_workspace"` to `REFRESH_DIAGNOSTIC_CODES` and terminal `revoked` classification (lines 2-6, 35). |
+| `MODIFY` | `tests/oauth/generic-oauth-failover.test.ts` | 842 / 1999 | Add red-to-green tests for bare account-wide cooldown enforcement under model-family queries (`isCooled`), session affinity eviction on Antigravity auth refusal, Google `RetryInfo` 429 cooldown extraction, Cursor 401/403 preflight rotation & `needsReauth` exclusion, Anthropic auto-pause affinity eviction, and Codex `deactivated_workspace` / `account_deactivated` terminal classification. |
+
+## 2. Field and Option Chains (`PLAN-FIELD-CHAIN-01`)
+
+| Field / Option | 1. Creation | 2. Serialization | 3. Deserialization | 4. Consumers |
+|---|---|---|---|---|
+| `ChatgptRefreshDiagnosticCode` (`"account_deactivated"`, `"deactivated_workspace"`) | Created in `classifyChatgptRefreshFailure` (`src/codex/chatgpt-refresh-failure.ts:31-36`). | Logged via `noteChatgptRefreshFailure` (`src/codex/chatgpt-refresh-failure.ts:58`) and attached to `ChatGptTokenError.oauthError` (`src/oauth/chatgpt.ts:80`) / `TokenRefreshError` (`src/codex/account-store.ts:1393`). | Parsed from OAuth token endpoint JSON `error` / `error.code` (`src/codex/chatgpt-refresh-failure.ts:22-28`). | `chatGptTokenError` (`src/oauth/chatgpt.ts:89`), `refreshNativeMainGrant` (`src/codex/main-account.ts:127`), `getValidCodexToken` (`src/codex/account-store.ts:1391`), `shouldMarkAccountNeedsReauthForCodexAuthFailure` (`src/codex/auth-context.ts`). |
+
+## 3. Bypass & Enforcement Records (`PLAN-BYPASS-NAMED-01`)
+
+| Mechanism | Tier | Executing Surface | Known Bypass Path | Residual Risk | Wording Downgraded? | Final Layer |
+|---|---|---|---|---|---|---|
+| Multi-account 401/403/429 cooldown, session affinity eviction, and `needsReauth` exclusion | E4 (Runtime pool router & pre-output recovery loops) | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-account-refusal.ts`, `src/server/responses/run-turn-execution.ts`, `src/codex/routing.ts` | A mid-stream error occurring *after* assistant output bytes have already been streamed to the client cannot replay the turn on another account; a singleton 1-account pool has no alternate to rotate to. | Post-output mid-stream errors still record cooldowns/health where supported, and singleton pools surface the upstream error directly to the caller. | Yes — described as "pre-output multi-account failover and cooldown hygiene". | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-account-refusal.ts`, `src/codex/routing.ts` |
+
+## 4. Before / After Diffs (`DIFFLEVEL-ROADMAP-01`)
+
+### 4.1 `src/oauth/generic-account-failover.ts` & `src/oauth/generic-session-affinity.ts`
+```diff
+--- a/src/oauth/generic-account-failover.ts
++++ b/src/oauth/generic-account-failover.ts
+@@ -119,6 +119,7 @@
+ function isCooled(provider: string, accountId: string, now: number, family?: QuotaModelFamily): boolean {
++ if (family !== undefined && isCooled(provider, accountId, now, undefined)) return true;
+ const entry = health.get(healthKey(provider, accountId, family));
+ if (!entry) return false;
+@@ -418,6 +419,12 @@
+ const failed = set.accounts.find(row => row.id === failedAccountId);
+ if (failed && credentialGeneration(failed.credential) === sentGeneration) {
+ health.set(healthKey("google-antigravity", failedAccountId), {
+ cooldownUntil: now + DEFAULT_COOLDOWN_MS,
+ cooldownSource: "auth",
+ identity: sentGeneration,
+ });
+ health.set(healthKey("google-antigravity", failedAccountId, family), {
+ cooldownUntil: now + DEFAULT_COOLDOWN_MS,
+ cooldownSource: "auth",
+ identity: sentGeneration,
+ });
++ presence.delete("google-antigravity");
+ sweepExpiredOnWrite(now);
+ }
+```
+
+### 4.2 `src/oauth/cursor.ts`, `src/oauth/index.ts`, `src/server/responses/request-transport.ts`, `src/server/responses/run-turn-execution.ts`
+```diff
+--- a/src/oauth/index.ts
++++ b/src/oauth/index.ts
+@@ -728,9 +728,10 @@
+ const FORCE_REFRESH_PROVIDERS = new Set([
+ "xai",
+ "github-copilot",
+ "kiro",
+ "google-antigravity",
++ "cursor",
+ "orcarouter-oauth",
+ "devin",
+ "workbuddy-ai",
+ "dsh-account",
+ ]);
+```
+```diff
+--- a/src/server/responses/request-transport.ts
++++ b/src/server/responses/request-transport.ts
+@@ -184,9 +184,10 @@
+ const isOAuth401ReplayProvider = (
+ route.providerName === "xai"
+ || route.providerName === "github-copilot"
+ || route.providerName === "kiro"
+ || route.providerName === "google-antigravity"
++ || route.providerName === "cursor"
+ || route.providerName === "orcarouter-oauth"
+ // runTurn transport: the replay runs on the first-event preflight in run-turn-execution.
+ || route.providerName === "devin"
+```
+
+### 4.3 `src/oauth/anthropic-account-refusal.ts` & `src/codex/chatgpt-refresh-failure.ts`
+```diff
+--- a/src/oauth/anthropic-account-refusal.ts
++++ b/src/oauth/anthropic-account-refusal.ts
+@@ -121,5 +121,6 @@
+ if (wasAccountAutoPaused(response)) {
++ routing.clearAnthropicSessionAffinityForAccount(sent.accountId);
+ if (!options.canRetry || options.signal?.aborted) return null;
+ return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(),
+```
+```diff
+--- a/src/codex/chatgpt-refresh-failure.ts
++++ b/src/codex/chatgpt-refresh-failure.ts
+@@ -2,5 +2,5 @@
+ const REFRESH_DIAGNOSTIC_CODES = [
+ "invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused",
+- "refresh_token_expired", "invalid_client", "invalid_request", "server_error",
++ "refresh_token_expired", "account_deactivated", "deactivated_workspace", "invalid_client", "invalid_request", "server_error",
+ "temporarily_unavailable", "rate_limit_exceeded", "unsupported_grant_type", "invalid_scope",
+ ] as const;
+@@ -35,3 +35,3 @@
+- if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused"].includes(exactCode ?? "")) {
++ if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused", "account_deactivated", "deactivated_workspace"].includes(exactCode ?? "")) {
+ reason = "revoked";
+```
+
+## 5. Activation Scenarios (`C-ACTIVATION-GROUNDING-01`) & Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+1. **Conditional Path 1 — Google Antigravity `invalid_grant` refresh -> `needsReauth` + bare cooldown check under model family**:
+ - *Trigger*: Seed 2 Antigravity accounts (`acc-1`, `acc-2`); have `acc-1` return 401 on inference and 400 `{"error":"invalid_grant"}` on refresh; also test recording a bare `"auth"` cooldown on `acc-1` and querying `eligibleFailoverAccounts("google-antigravity", now, "gem")`.
+ - *Observable Effect*: `acc-1` is marked `needsReauth: true`, `acc-2` serves the replayed turn, and a bare `"auth"` cooldown excludes `acc-1` even when queried with `family = "gem"` or `"cla"`.
+2. **Conditional Path 2 — Cursor preflight 401/403/revoked refresh failover**:
+ - *Trigger*: Seed 2 Cursor accounts (`cur-1`, `cur-2`); have `cur-1` fail preflight with 401 (and refresh fail with 401 `invalid_grant` or `shouldLogout: true`) or preflight 403 `permission_denied`.
+ - *Observable Effect*: `cur-1` is marked `needsReauth` (on dead refresh) or cooled (on 403), session affinity is unbound from `cur-1`, and `runTurn` replays cleanly on `cur-2`.
+3. **Conditional Path 3 — Anthropic auto-pause affinity eviction & Codex `deactivated_workspace` / `account_deactivated` terminal classification**:
+ - *Trigger*: Trigger `wasAccountAutoPaused` on an Anthropic account with an active `sessionAffinity` binding, and trigger `deactivated_workspace` / `account_deactivated` refresh failure on a Codex pool account.
+ - *Observable Effect*: Anthropic auto-pause immediately evicts the session affinity binding for the paused account, and `classifyChatgptRefreshFailure` classifies `deactivated_workspace` / `account_deactivated` as terminal `revoked`.
+
+### Verifier Commands (Run & Verified in P Phase per `PLAN-VERIFIER-REAL-01`)
+- `bun test tests/oauth/generic-oauth-failover.test.ts tests/oauth/oauth-account-auto-pause.test.ts tests/adapters/anthropic/anthropic-revoked-token.test.ts tests/claude-integration/messages-revoked-token.test.ts tests/oauth/chatgpt-token-expiry.test.ts` — baseline exit code `0`.
+- `bun run typecheck && bun run privacy:scan && bun run structure:check && bun test tests/ci-workflows/file-size-ratchet.test.ts` — baseline exit code `0`.
diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs
index 89ccd0728d6..d4fb8509e9f 100644
--- a/docs-site/astro.config.mjs
+++ b/docs-site/astro.config.mjs
@@ -160,6 +160,7 @@ export default defineConfig({
{ label: "Server & Runtime", translations: { fr: "Serveur et environnement d’exécution", ko: "서버 & 런타임", "zh-CN": "服务器与运行时", "zh-TW": "伺服器與執行階段", ru: "Сервер и рантайм", ja: "サーバー & ランタイム", tr: "Sunucu ve Çalışma Zamanı" }, slug: "reference/configuration/server" },
],
},
+ { label: "Client Support", translations: { fr: "Clients pris en charge", ko: "클라이언트 지원", "zh-CN": "客户端支持", "zh-TW": "用戶端支援", ru: "Поддержка клиентов", ja: "クライアント対応", tr: "İstemci desteği" }, slug: "reference/client-support" },
{ label: "Adapters", translations: { fr: "Adaptateurs", ko: "어댑터", "zh-CN": "适配器", "zh-TW": "適配器", ru: "Адаптеры", ja: "アダプター", tr: "Adaptörler" }, slug: "reference/adapters" },
{ label: "Architecture", translations: { fr: "Architecture", ko: "아키텍처", "zh-CN": "架构", "zh-TW": "架構", ru: "Архитектура", ja: "アーキテクチャ", tr: "Mimari" }, slug: "reference/architecture" },
{ label: "Platform Support", translations: { fr: "Prise en charge des plateformes", ko: "플랫폼 지원", "zh-CN": "平台支持", "zh-TW": "平台支援", ru: "Поддержка платформ", ja: "プラットフォーム対応", tr: "Platform Desteği" }, link: `${SITE_URL}/reference/platform-support` },
diff --git a/docs-site/src/components/Landing.astro b/docs-site/src/components/Landing.astro
index 8944272605e..99a3f92abb8 100644
--- a/docs-site/src/components/Landing.astro
+++ b/docs-site/src/components/Landing.astro
@@ -34,8 +34,10 @@ const prefix = locale ? `${base}${locale}/` : base;
const fr = {
'Install': 'Installer',
'Start the proxy': 'Démarrer le proxy',
- 'Inject a provider into Codex': 'Ajouter un fournisseur à Codex',
+ 'Add a provider and connect Codex': 'Ajouter un fournisseur et connecter Codex',
+ 'In another terminal: launch Claude Code through ocx': 'Dans un autre terminal : lancer Claude Code via ocx',
'Codex now routes through ocx': 'Codex passe maintenant par ocx',
+ 'See supported tools': 'Voir les outils pris en charge',
'Getting Started': 'Démarrage',
'Installation': 'Installation',
'Quickstart': 'Démarrage rapide',
@@ -61,16 +63,16 @@ const fr = {
'Project': 'Projet',
'Contributing': 'Contribuer',
'Run ': 'Exécutez ',
- ' on any LLM.': ' sur n’importe quel LLM.',
- 'opencodex is a local proxy that serves Codex and Claude Code from one port — translating each wire format to any provider, with streaming, tool calls, reasoning, and images in both directions. Zero client patches.': 'opencodex est un proxy local qui fournit Codex et Claude Code sur un même port. Il traduit chaque format de protocole vers n’importe quel fournisseur, dans les deux sens, avec la diffusion en continu, les appels d’outils, le raisonnement et les images. Aucune modification des clients.',
+ ' on the models you choose.': ' sur les modèles de votre choix.',
+ 'opencodex is one local gateway for your AI coding tools and models. It serves Codex, Claude Code, Claude Desktop and Grok Build from one port and connects more tools through managed configs, translating each wire format to the provider you choose, with streaming, tool calls, reasoning and images where the model supports them. Zero client patches.': 'opencodex est une passerelle locale unique pour vos outils de code IA et vos modèles. Il sert Codex, Claude Code, Claude Desktop et Grok Build sur un même port et connecte d’autres outils via des configurations gérées, en traduisant chaque format de protocole vers le fournisseur de votre choix, avec la diffusion en continu, les appels d’outils, le raisonnement et les images lorsque le modèle les prend en charge. Aucune modification des clients.',
'Get Started': 'Commencer',
'Supported providers': 'Fournisseurs pris en charge',
'40+ providers': 'Plus de 40 fournisseurs',
'The opencodex dashboard — live proxy status, routed providers, and available models': 'Le tableau de bord opencodex : état du proxy en direct, fournisseurs routés et modèles disponibles',
- 'Install. Start. Codex, rerouted.': 'Installez. Démarrez. Codex est redirigé.',
+ 'Install. Start. Connect your tool.': 'Installez. Démarrez. Connectez votre outil.',
'Install.': 'Installez.',
'Start.': 'Démarrez.',
- 'Codex, rerouted.': 'Codex est redirigé.',
+ 'Connect your tool.': 'Connectez votre outil.',
'Full quickstart': 'Démarrage rapide complet',
'Watch Codex run on a routed model.': 'Regardez Codex exécuter une tâche sur un modèle routé.',
'Codex App running a task on a routed non-OpenAI model': 'Codex App exécutant une tâche sur un modèle routé autre qu’OpenAI',
@@ -164,12 +166,14 @@ const providers: { label: string; path?: string }[] = [
const quickstartMini = [
{ cmd: 'npm install -g @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
+ { cmd: 'ocx init', note: t('Add a provider and connect Codex', '프로바이더 추가 후 Codex 연결', '添加提供商并连接 Codex', 'Добавьте провайдера и подключите Codex', 'プロバイダーを追加して Codex を接続', '新增供應商並連接 Codex', 'Sağlayıcı ekleyip Codex’i bağlayın') },
{ cmd: 'ocx start', note: t('Start the proxy', '프록시 시작', '启动代理', 'Запуск прокси', 'プロキシを起動', '啟動代理', "Proxy'yi başlatın") },
+ { cmd: 'ocx claude', note: t('In another terminal: launch Claude Code through ocx', '다른 터미널에서: ocx를 거쳐 Claude Code 실행', '在另一个终端中:通过 ocx 启动 Claude Code', 'В другом терминале: запуск Claude Code через ocx', '別のターミナルで: ocx 経由で Claude Code を起動', '在另一個終端機中:透過 ocx 啟動 Claude Code', 'Başka bir terminalde: Claude Code’u ocx üzerinden başlatın') },
];
const quickstart = [
{ cmd: 'npm install -g @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
- { cmd: 'ocx init', note: t('Inject a provider into Codex', 'Codex에 프로바이더 주입', '向 Codex 注入 provider', 'Внедрение провайдера в Codex', 'プロバイダーを Codex に注入', '向 Codex 注入供應商', "Codex'e bir sağlayıcı ekleyin") },
+ { cmd: 'ocx init', note: t('Add a provider and connect Codex', '프로바이더 추가 후 Codex 연결', '添加提供商并连接 Codex', 'Добавьте провайдера и подключите Codex', 'プロバイダーを追加して Codex を接続', '新增供應商並連接 Codex', 'Sağlayıcı ekleyip Codex’i bağlayın') },
{ cmd: 'ocx start', note: t('Start the proxy', '프록시 시작', '启动代理', 'Запуск прокси', 'プロキシを起動', '啟動代理', "Proxy'yi başlatın") },
{ cmd: 'codex "Write a hello world in Rust"', note: t('Codex now routes through ocx', 'Codex가 ocx를 경유', 'Codex 现在经由 ocx 路由', 'Codex теперь работает через ocx', 'Codex は ocx 経由に', 'Codex 現在經由 ocx 路由', 'Codex artık ocx üzerinden yönlendirilir') },
];
@@ -261,10 +265,10 @@ const docsMap = [
ClaudeCodex · Claude
- {t(' on any LLM.', '를 어떤 LLM 위에서든.', ' 跑在任意 LLM 上。', ' на любой LLM.', ' を任意の LLM 上で。', ' 跑在任意 LLM 上。', ' dilediğiniz LLM ile çalıştırın.')}
+ {t(' on the models you choose.', '를 원하는 모델로.', ' 跑在你选择的模型上。', ' на выбранных вами моделях.', ' を好きなモデルで。', ' 跑在你選擇的模型上。', ' seçtiğiniz modellerle çalıştırın.')}
- {t('opencodex is a local proxy that serves Codex and Claude Code from one port — translating each wire format to any provider, with streaming, tool calls, reasoning, and images in both directions. Zero client patches.', 'opencodex는 포트 하나로 Codex와 Claude Code를 함께 서빙하는 로컬 프록시입니다 — 각 wire 포맷을 어떤 프로바이더로든 양방향 변환하고, 스트리밍·tool call·reasoning·이미지까지 그대로 통과시킵니다. 클라이언트 패치는 없습니다.', 'opencodex 是一个本地代理,用同一个端口同时服务 Codex 与 Claude Code —— 将各自的线路格式双向翻译为任意 provider,支持流式、工具调用、推理与图像。无需修补客户端。', 'opencodex — локальный прокси, который обслуживает Codex и Claude Code через один порт: транслирует каждый wire-формат под любого провайдера, пропуская стриминг, вызовы инструментов, рассуждения и изображения в обе стороны. Никаких патчей на стороне клиента.', 'opencodex は 1 つのポートで Codex と Claude Code をまとめて提供するローカルプロキシです — 各 wire フォーマットを任意のプロバイダーへ双方向に変換し、ストリーミング・ツール呼び出し・推論・画像までそのまま通します。クライアントのパッチは不要です。', 'opencodex 是一個本機代理,用同一個連接埠同時服務 Codex 與 Claude Code —— 將各自的線路格式雙向翻譯為任意供應商,支援流式、工具呼叫、推理與圖像。無需修補用戶端。', 'opencodex, Codex ve Claude Code’u tek bir port üzerinden sunan yerel bir proxy’dir — her bir hat formatını dilediğiniz sağlayıcıya dönüştürür; akış (streaming), araç çağrıları (tool calls), akıl yürütme (reasoning) ve görseller her iki yönde de sorunsuz iletilir. İstemci tarafında sıfır yama.')}
+ {t('opencodex is one local gateway for your AI coding tools and models. It serves Codex, Claude Code, Claude Desktop and Grok Build from one port and connects more tools through managed configs, translating each wire format to the provider you choose, with streaming, tool calls, reasoning and images where the model supports them. Zero client patches.', 'opencodex는 AI 코딩 도구와 모델을 잇는 하나의 로컬 게이트웨이입니다. 포트 하나로 Codex, Claude Code, Claude Desktop, Grok Build를 서빙하고 관리형 설정으로 더 많은 도구를 연결하며, 각 wire 포맷을 원하는 프로바이더로 변환합니다. 모델이 지원하면 스트리밍·tool call·reasoning·이미지도 그대로 통과합니다. 클라이언트 패치는 없습니다.', 'opencodex 是一个本地网关,连接你的 AI 编程工具与模型。它用同一个端口服务 Codex、Claude Code、Claude Desktop 和 Grok Build,并通过托管配置连接更多工具,把各自的线路格式翻译为你选择的提供商;在模型支持时,流式、工具调用、推理与图像都会保留。无需修补客户端。', 'opencodex — единый локальный шлюз для ваших ИИ-инструментов для кода и моделей. Он обслуживает Codex, Claude Code, Claude Desktop и Grok Build через один порт и подключает другие инструменты через управляемые конфигурации, транслируя каждый wire-формат для выбранного провайдера; стриминг, вызовы инструментов, рассуждения и изображения проходят, если их поддерживает модель. Никаких патчей на стороне клиента.', 'opencodex は、AI コーディングツールとモデルをつなぐ 1 つのローカルゲートウェイです。1 つのポートで Codex、Claude Code、Claude Desktop、Grok Build を提供し、管理された設定でほかのツールも接続します。各 wire フォーマットを選んだプロバイダーへ変換し、モデルが対応していればストリーミング・ツール呼び出し・推論・画像もそのまま通します。クライアントのパッチは不要です。', 'opencodex 是一個本機閘道,連接你的 AI 程式開發工具與模型。它用同一個連接埠服務 Codex、Claude Code、Claude Desktop 和 Grok Build,並透過代管設定連接更多工具,把各自的線路格式翻譯為你選擇的供應商;在模型支援時,串流、工具呼叫、推理與圖像都會保留。無需修補用戶端。', 'opencodex, yapay zekâ kodlama araçlarınız ve modelleriniz için tek bir yerel ağ geçididir. Codex, Claude Code, Claude Desktop ve Grok Build’i tek bir port üzerinden sunar, diğer araçları yönetilen yapılandırmalarla bağlar ve her hat formatını seçtiğiniz sağlayıcıya dönüştürür; model destekliyorsa akış, araç çağrıları, akıl yürütme ve görseller korunur. İstemci tarafında sıfır yama.')}
diff --git a/docs-site/src/components/SiteJsonLd.astro b/docs-site/src/components/SiteJsonLd.astro
index 18e2403d9bd..d84049e2f13 100644
--- a/docs-site/src/components/SiteJsonLd.astro
+++ b/docs-site/src/components/SiteJsonLd.astro
@@ -22,14 +22,14 @@ const homeUrl = locale ? `${SITE_URL}/${locale}/` : `${SITE_URL}/`;
const inLanguage = locale ? langs[locale] : 'en';
const descriptions = {
- en: 'Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI, App, SDK, and Claude Code.',
- fr: 'Proxy universel de fournisseurs pour OpenAI Codex et Claude Code — utilisez n’importe quel LLM avec Codex CLI, App, SDK et Claude Code.',
- ko: 'OpenAI Codex & Claude Code를 위한 범용 프로바이더 프록시 — Codex CLI, App, SDK와 Claude Code에서 어떤 LLM이든 사용하세요.',
- 'zh-CN': '面向 OpenAI Codex 与 Claude Code 的通用 provider 代理 —— 在 Codex CLI、App、SDK 和 Claude Code 中使用任意 LLM。',
- 'zh-TW': '適用於 OpenAI Codex 與 Claude Code 的通用 provider 代理 —— 在 Codex CLI、App、SDK 和 Claude Code 中使用任意 LLM。',
- ru: 'Универсальный прокси провайдеров для OpenAI Codex и Claude Code — используйте любую LLM с Codex CLI, App, SDK и Claude Code.',
- ja: 'OpenAI Codex & Claude Code 向けの汎用プロバイダープロキシ — Codex CLI、App、SDK と Claude Code で任意の LLM を使えます。',
- tr: 'OpenAI Codex ve Claude Code için evrensel sağlayıcı proxy’si — Codex CLI, App, SDK ve Claude Code ile dilediğiniz LLM’i kullanın.',
+ en: 'One local gateway for your AI coding tools and models: use the providers you choose with Codex, Claude Code, Claude Desktop and Grok Build, and connect more tools through managed configs.',
+ fr: 'Une passerelle locale unique pour vos outils de code IA et vos modèles : utilisez les fournisseurs de votre choix avec Codex, Claude Code, Claude Desktop et Grok Build, et connectez d’autres outils via des configurations gérées.',
+ ko: 'AI 코딩 도구와 모델을 잇는 하나의 로컬 게이트웨이: Codex, Claude Code, Claude Desktop, Grok Build에서 원하는 프로바이더를 쓰고, 관리형 설정으로 더 많은 도구를 연결하세요.',
+ 'zh-CN': '一个本地网关,连接你的 AI 编程工具与模型:在 Codex、Claude Code、Claude Desktop 和 Grok Build 中使用你选择的提供商,并通过托管配置连接更多工具。',
+ 'zh-TW': '一個本機閘道,連接你的 AI 程式開發工具與模型:在 Codex、Claude Code、Claude Desktop 和 Grok Build 中使用你選擇的供應商,並透過代管設定連接更多工具。',
+ ru: 'Единый локальный шлюз для ваших ИИ-инструментов для кода и моделей: используйте выбранных провайдеров в Codex, Claude Code, Claude Desktop и Grok Build и подключайте другие инструменты через управляемые конфигурации.',
+ ja: 'AI コーディングツールとモデルをつなぐ 1 つのローカルゲートウェイ。Codex、Claude Code、Claude Desktop、Grok Build で好きなプロバイダーを使い、管理された設定でほかのツールも接続できます。',
+ tr: 'Yapay zekâ kodlama araçlarınız ve modelleriniz için tek bir yerel ağ geçidi: Codex, Claude Code, Claude Desktop ve Grok Build ile seçtiğiniz sağlayıcıları kullanın, diğer araçları yönetilen yapılandırmalarla bağlayın.',
} as const;
type GraphNode = Record;
@@ -59,14 +59,15 @@ if (!locale) {
name: 'opencodex',
alternateName: 'ocx',
description:
- 'Local LLM proxy that lets OpenAI Codex (CLI, App, SDK) and Claude Code run on any model — Claude, Gemini, Grok, DeepSeek, Kimi, Qwen, Ollama, OpenRouter, and more — with streaming, tool calls, reasoning tokens, and images working in both directions.',
+ 'Local gateway that connects AI coding tools — OpenAI Codex (CLI, App, SDK), Claude Code, Claude Desktop, Grok Build and tools with managed configs — to the model providers you choose: Claude, Gemini, Grok, DeepSeek, Kimi, Qwen, Ollama, OpenRouter, and more, with streaming, tool calls, reasoning tokens, and images where the client, model and provider support them.',
keywords:
- 'codex, claude code, openai codex proxy, claude code proxy, llm proxy, ai gateway, anthropic, gemini, grok, deepseek, ollama, openrouter, responses api, codex cli',
+ 'codex, claude code, claude desktop, grok build, ai coding tools gateway, openai codex proxy, claude code proxy, llm proxy, ai gateway, anthropic, gemini, grok, deepseek, ollama, openrouter, responses api, codex cli',
featureList: [
- 'Run Codex CLI/App/SDK on any LLM provider',
- 'Run Claude Code on any LLM via the Anthropic Messages API',
+ 'Connect Codex CLI/App/SDK, Claude Code, Claude Desktop and Grok Build to the providers you choose',
+ 'Managed config integrations for OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code and more',
+ 'Accepts the Responses, Chat Completions and Anthropic Messages APIs',
'ChatGPT account pool with quota-aware routing',
- 'Streaming, tool calls, reasoning tokens, and vision in both directions',
+ 'Streaming, tool calls, reasoning tokens, and vision where the client, model and provider support them',
'Web dashboard on localhost:10100',
],
applicationCategory: 'DeveloperApplication',
diff --git a/docs-site/src/content/docs/contributing.md b/docs-site/src/content/docs/contributing.md
index d0021b7f12a..6c1929381e6 100644
--- a/docs-site/src/content/docs/contributing.md
+++ b/docs-site/src/content/docs/contributing.md
@@ -15,7 +15,7 @@ bun install
bun run setup:hooks # retire the managed pre-push and post-merge hooks
bun run dev:proxy # proxy API in dev mode
bun run dev:gui # dashboard dev server (another terminal)
-bun run typecheck # bun x tsc --noEmit
+bun run typecheck # bun check --no-script --noEmit
bun run test # full suite (default)
```
@@ -33,10 +33,11 @@ yourself — the dev server is unaffected because it rebuilds on demand.
## Build and test commands
The root package is Bun-native TypeScript; there is no separate server compile step. Use the checked-in
-scripts so local commands match CI:
+scripts for local verification:
```bash
-bun run typecheck # strict TypeScript check
+bun run typecheck # strict Bun-native type check
+bun run typecheck:tsc # reference check with the installed TypeScript compiler
bun run test:changed # import-graph tests against the resolved dev merge base
bun run test # full suite (default)
bun test tests/routing/router.test.ts # focused test file
@@ -45,6 +46,14 @@ bun run privacy:scan # credential/privacy scan used by CI
bun run prepare:package # refresh package launchers/assets
```
+`typecheck` uses Bun's native checker with the root `tsconfig.json`, preserving its `src/` scope
+and strict compiler options without emitting files. `--no-script` keeps it on the type checker
+if a `check` package script is added later. The installed Bun dependency supplies the checker;
+`typecheck:tsc` retains the TypeScript compiler for comparisons and fallback. CI and release preflight
+continue to invoke `tsc` directly, and the GUI retains its own TypeScript build check.
+The `prepublishOnly` hook also uses `typecheck:tsc`, between the audit and GUI build steps;
+an audit or compiler failure stops the remaining steps.
+
The suite uses Bun 1.4.4 (`package.json`'s `testRunnerBun`) while the shipped runtime is 1.4.4.
`scripts/test.ts` finds a 1.4.4 binary on PATH or in `~/.bun/bin`, or uses `OCX_TEST_RUNNER_BUN`.
Install that version locally if it is missing; the resolver never downloads it.
@@ -181,7 +190,7 @@ does not change `main`/`preview` review rules or allow direct pushes, force-push
## Conventions
-- **ES Modules only** (`import`/`export`), TypeScript, `strict` mode. Keep `bun x tsc --noEmit` clean.
+- **ES Modules only** (`import`/`export`), TypeScript, `strict` mode. Keep `bun run typecheck` clean.
- **~500 lines per file max** — split by responsibility (the `web-search/` and `vision/` sidecars are
good examples of small, focused modules behind a single `index.ts`).
- **Handle async errors at boundaries** — sidecars never throw into the request path; they degrade to
diff --git a/docs-site/src/content/docs/fr/contributing.md b/docs-site/src/content/docs/fr/contributing.md
index 279b582c754..78c5ba4ae7f 100644
--- a/docs-site/src/content/docs/fr/contributing.md
+++ b/docs-site/src/content/docs/fr/contributing.md
@@ -15,7 +15,7 @@ bun install
bun run setup:hooks # retirer les anciens hooks gérés pre-push et post-merge
bun run dev:proxy # proxy API in dev mode
bun run dev:gui # dashboard dev server (another terminal)
-bun run typecheck # bun x tsc --noEmit
+bun run typecheck # bun check --no-script --noEmit
bun run test # suite complète (par défaut)
```
@@ -163,7 +163,7 @@ du dépôt et des chemins sensibles du point de vue de la sécurité est déclar
## Conventions
-- **Modules ES uniquement** (`import`/`export`), TypeScript, mode `strict`. Gardez `bun x tsc --noEmit` propre.
+- **Modules ES uniquement** (`import`/`export`), TypeScript, mode `strict`. Gardez `bun run typecheck` propre.
- **Environ 500 lignes au maximum par fichier** — répartissez le code par responsabilité. Les services auxiliaires `web-search/` et `vision/` sont
bons exemples de petits modules ciblés derrière un seul `index.ts`).
- **Gérer les erreurs asynchrones aux frontières** — les services auxiliaires ne propagent jamais d’exception dans le
diff --git a/docs-site/src/content/docs/fr/getting-started/quickstart.md b/docs-site/src/content/docs/fr/getting-started/quickstart.md
index fa96c775b52..986a1c11ef9 100644
--- a/docs-site/src/content/docs/fr/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/fr/getting-started/quickstart.md
@@ -1,9 +1,9 @@
---
title: Démarrage rapide
-description: Configurez votre premier fournisseur et acheminez OpenAI Codex via opencodex en trois commandes.
+description: "Installez opencodex, ajoutez votre premier fournisseur et connectez votre outil de programmation : Codex, Claude Code, Claude Desktop, Grok Build ou un autre outil pris en charge."
---
-Ce guide part d’une installation neuve et vous conduit jusqu’à l’exécution de Codex sur un modèle autre qu’OpenAI.
+Ce guide vous mène d’une nouvelle installation à la première requête de votre outil de programmation. Les étapes 1 et 2 sont communes ; l’étape 3 propose une branche par outil. La [page des clients pris en charge](/fr/reference/client-support/) présente les outils et leurs connexions.
## 1. Exécutez l'assistant de configuration
@@ -58,9 +58,13 @@ ocx status
ocx gui # ouvre le tableau de bord sur le port actif
```
-## 3. Utiliser Codex
+## 3. Connectez votre outil
-Codex parle maintenant à opencodex de manière transparente :
+Choisissez la branche correspondant à votre outil. Vous pouvez en connecter plusieurs : ils partagent les mêmes fournisseurs et modèles.
+
+### Codex
+
+Si vous avez accepté l’invite Codex à l’étape 1, Codex communique maintenant avec opencodex de manière transparente. Si vous avez refusé cette invite ou si la configuration s’est arrêtée parce qu’une configuration existait déjà, activez le commutateur Codex sur la page **Connect** du tableau de bord avant de lancer Codex ; [Intégration Codex](/fr/guides/codex-integration/) explique les modifications et leur restauration.
```bash
codex "Refactor this function for readability"
@@ -73,6 +77,42 @@ codex -m "anthropic/claude-opus-5" "Explain this stack trace"
codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
```
+### Claude Code
+
+Avec le CLI `claude` installé :
+
+```bash
+ocx claude
+```
+
+`ocx claude` démarre le proxy s’il ne fonctionne pas et lance Claude Code avec un environnement dirigé vers opencodex. Les arguments supplémentaires sont transmis. Consultez [Claude Code](/fr/guides/claude-code/) pour les valeurs par défaut des niveaux, les autres lanceurs et la désactivation.
+
+### Claude Desktop
+
+```bash
+ocx claude desktop
+```
+
+Lors d’une nouvelle installation, cette commande configure le profil de passerelle pour toute l’application. Une installation utilisant déjà le mode first-party le conserve ; exécutez `ocx claude desktop apply --gateway` pour changer explicitement. Le mode first-party conserve claude.ai pour Desktop et ne route que son onglet Code ; consultez la section sur les modes Claude Desktop dans [Claude Code](/fr/guides/claude-code/).
+
+Quittez complètement puis rouvrez Claude Desktop après un changement de mode.
+
+### Grok Build
+
+Sur une écoute locale, si `~/.grok` existe, `ocx start` enregistre les modèles visibles dans `~/.grok/config.toml`. Choisissez un modèle `ocx-*` dans Grok Build. Les configurations distantes et le bloc géré sont décrits dans [Grok Build](/fr/guides/grok-build/).
+
+Après avoir ajouté des fournisseurs ou des modèles, lancez `ocx ensure` ou redémarrez le proxy pour que Grok Build les voie.
+
+### Autres outils
+
+Les outils avec intégration gérée (OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code et d’autres) se connectent depuis la page **Connect** du tableau de bord ou avec :
+
+```bash
+ocx integration client enable --client
+```
+
+Les fichiers de configuration et les délais de prise d’effet sont décrits dans [Intégrations](/fr/guides/integrations/). Tout autre outil acceptant un point de terminaison personnalisé peut utiliser l’URL de base compatible OpenAI `http://localhost:10100/v1` ou l’URL de base Anthropic `http://localhost:10100`.
+
## Choisissez des modèles de sous-agents (facultatif)
Une nouvelle configuration propose cinq modèles natifs dans le sélecteur de sous-agents de Codex : `gpt-5.5`,
diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md
index 0405b2807d9..0b167f7c9d7 100644
--- a/docs-site/src/content/docs/fr/guides/claude-code.md
+++ b/docs-site/src/content/docs/fr/guides/claude-code.md
@@ -34,8 +34,8 @@ consultez les conditions actuelles d'Anthropic.
Comportement lorsque cette option est activée :
- Un **429** en amont place le compte en temporisation selon `Retry-After` lorsqu'il est présent, ou selon un délai de repli,
- efface ses affinités et peut faire basculer la requête vers un autre compte admissible, dans les limites prévues.
-- L'affinité est **locale au processus** et disparaît au redémarrage du proxy.
+ et peut faire basculer la requête vers un autre compte admissible, dans les limites prévues. Seul un refus de quota partagé efface ses affinités ; une limitation temporaire les conserve.
+- L'affinité survit au redémarrage dans le même répertoire de configuration et la même instance fournisseur (24 heures d'inactivité, 2 000 liaisons au maximum). Le fichier privé ne contient que des empreintes de session et d'identité de connexion, jamais de clé de session brute ni de jeton. Les anciens comptes sans identité persistante restent locaux au processus jusqu'à leur prochaine connexion. Le renouvellement du jeton conserve la liaison ; une nouvelle connexion, la suppression, la pause, une réauthentification requise ou une sélection manuelle pendant l'exécution l'invalident. Un détour temporaire dû à une limitation de débit conserve la liaison initiale.
- Les erreurs de renouvellement du jeton conservent la règle `needsReauth`. Un 403 confirmé lié à un abonnement ou à la facturation du compte peut déclencher un basculement avant la sortie, avec une temporisation selon `Retry-After` ou de dix minutes. Un refus d’autorisation ordinaire reste terminal. Voir la [reprise des comptes](/guides/claude-code/).
- Si chaque compte éligible est en temporisation, le proxy renvoie **429** (et non 401) avec `Retry-After`
lorsqu'il est connu.
@@ -200,6 +200,8 @@ quittez complètement puis rouvrez Claude Desktop.
Le mode picker fait partie de first-party : le [risque pour le compte du mode first-party](#first-party-sur-demande)
s'applique donc aussi à ce mode.
+Avant la première réponse du serveur amont, un téléversement picker sans progression pendant 60 secondes est fermé. Une expiration interrompt le téléversement sans réponse de succès ; en HTTP/2, seul le flux concerné est réinitialisé. Ce délai d’inactivité est suspendu lorsque le serveur amont exerce une contre-pression et supprimé dès la fin du téléversement ou la première réponse amont. Ce mécanisme de surveillance de l’inactivité n’ajoute aucune limite à la durée totale du téléversement ; les limites existantes de l’environnement d’exécution et du protocole restent applicables. Ce délai d’inactivité ne s’applique pas aux flux de réponse.
+
### Utiliser les modèles opencodex depuis l'onglet Code de Desktop (associations first-party)
En mode first-party, le sélecteur de modèles de l'onglet Code appartient à claude.ai : ses lignes
diff --git a/docs-site/src/content/docs/fr/guides/codex-prompt.md b/docs-site/src/content/docs/fr/guides/codex-prompt.md
index 33793afbcc2..2641bb0c050 100644
--- a/docs-site/src/content/docs/fr/guides/codex-prompt.md
+++ b/docs-site/src/content/docs/fr/guides/codex-prompt.md
@@ -36,6 +36,12 @@ temporairement indisponible, ce qui n’est pas le cas.
Cliquez sur le nom d’une couche pour afficher le texte qu’elle envoie. La boîte de dialogue le lit
depuis `codex debug prompt-input` : il s’agit donc du contenu réel, et non d’une description.
+Chaque appel de lecture dispose d’un délai fixe de 15 secondes, qui inclut la lecture des
+métadonnées et l’attente d’une sonde partagée. L’expiration est vérifiée après la fin de la
+lecture synchrone des métadonnées ; si le délai est épuisé, aucune nouvelle sonde ne démarre.
+À l’expiration, la boîte de dialogue indique que le prompt n’a pas pu être lu, mais les
+instructions de base lues localement sont tout de même renvoyées.
+
Il arrive qu’il n’y ait rien à afficher. La boîte de dialogue précise alors la raison :
- **Le fichier existe, mais il est vide.** Votre fichier `~/.codex/AGENTS.md` ne contient aucun
diff --git a/docs-site/src/content/docs/fr/guides/combos.md b/docs-site/src/content/docs/fr/guides/combos.md
index 8a9398988f6..ebd6023df2a 100644
--- a/docs-site/src/content/docs/fr/guides/combos.md
+++ b/docs-site/src/content/docs/fr/guides/combos.md
@@ -374,3 +374,7 @@ gérer un refus de politique ou corriger l’origine de la demande rejetée. Les
Exception aux erreurs 400 terminales : un rejet structuré de `user`, une valeur non prise en charge pour `reasoning.effort`/`reasoning_effort`, un rejet d’entrée d’image propre à un modèle (`param: input`), ou le refus d’un média d’entrée que la route ne peut pas lire (`unsupported_input_modality`) peut faire passer le combo à la cible admissible suivante avant le début de la sortie, sans délai de refroidissement. Le refus de sécurité, l’annulation et une sortie déjà commencée restent non rejouables.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
+
+## Réservation d’une tentative de récupération
+
+Une tentative de récupération activée par `cooldownReprobe: { enabled: true }` appartient uniquement à la requête qui l’a réservée ; les autres requêtes restent soumises aux délais de refroidissement enregistrés. La réservation est conservée pendant l’attente et la sélection JEV. Une cible redevenue utilisable pendant l’attente peut être réessayée si son délai enregistré n’a pas changé. Un nouvel enregistrement annule la réservation de cette cible. Un succès ne supprime que le délai inchangé de la cible ayant répondu ; les cibles non essayées et les nouveaux échecs conservent leurs délais. La désactivation du fournisseur, les quotas, l’admissibilité de la requête et l’annulation restent vérifiés. Voir les [réglages et limites](/guides/combos/#retry-all-cooled-targets-opt-in).
diff --git a/docs-site/src/content/docs/fr/guides/integrations.md b/docs-site/src/content/docs/fr/guides/integrations.md
index a9121f6121d..f909a13a450 100644
--- a/docs-site/src/content/docs/fr/guides/integrations.md
+++ b/docs-site/src/content/docs/fr/guides/integrations.md
@@ -6,6 +6,8 @@ description: Connectez opencodex à OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Co
L'onglet **Connexion** écrit le bloc fournisseur d'opencodex dans le fichier de configuration du client,
puis peut le retirer. Dix-huit clients fonctionnent ainsi, chacun avec son propre commutateur :
+Sous Windows, une surcharge exigeant un chemin absolu doit inclure une lettre de lecteur (`C:\Users\me\client`) ou un partage UNC (`\\server\share\client`). Les formes `\Users\me\client` et `/client/config`, dépendantes du lecteur courant, sont refusées. Les sélecteurs documentés comme acceptant `~` le développent depuis le répertoire personnel effectif ; Gentle Shell ne développe pas `~`.
+
| Client | Fichier de configuration | Format | Prise d'effet de la modification | Identifiant |
|---|---|---|---|---|
| OpenCode | `~/.config/opencode/opencode.json` | JSON | au prochain lancement direct | `OPENCODEX_OPENCODE_API_KEY` |
@@ -328,3 +330,19 @@ ocx integration client enable --client kilo
## Factory Droid
Factory Droid utilise `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` sous Windows). Activez explicitement l’intégration avec `ocx integration client enable --client droid`, puis choisissez un modèle personnalisé dans `/model`. Les entrées gérées n’utilisent pas de clé et fonctionnent uniquement en boucle locale. La désactivation supprime ces entrées ; l’annulation restaure les octets sauvegardés. Si l’ancien `config.json` contient des entrées OpenCodex ou si `settings.local.json` remplace `customModels`, résolvez ce conflit avant l’activation. Consultez la [documentation Factory BYOK](https://docs.factory.ai/model-independence/byok).
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/fr/guides/model-ordering.md b/docs-site/src/content/docs/fr/guides/model-ordering.md
index fab755afdf4..95cdd75584f 100644
--- a/docs-site/src/content/docs/fr/guides/model-ordering.md
+++ b/docs-site/src/content/docs/fr/guides/model-ordering.md
@@ -30,7 +30,7 @@ Sans ordre global du sélecteur, les priorités sans sélecteur pertinentes sont
| `subagentModels[i]` | `i` (`0` à `4`) | La carte de classement présentée dans `src/codex/catalog/sync.ts` |
| Autres modèles acheminés | `5` | Création d'une entrée routé dans `src/codex/catalog/sync.ts` |
| Modèles routés non mis en avant et présents dans `modelPickerOrder` | `1000 + i` | Rang d’affichage du sélecteur dans `src/codex/catalog/sync.ts` |
-| Slugs GPT natifs par défaut | `9` | Création d'entrées natives dans `src/codex/catalog/sync.ts` |
+| Slugs GPT natifs par défaut | `sourcePriority` | Création d'entrées natives dans `src/codex/catalog/sync.ts` |
| Modèles natifs non sélectionnés alors qu'une liste sélectionnée existe | Au moins `featured.length + 100` | Fusion du catalogue natif dans `src/codex/catalog/sync.ts` |
La direction API limite `subagentModels` à cinq entrées avec `slice(0, 5)` en
@@ -73,7 +73,7 @@ Sans sélecteur de compte éligible et sans liste de sélection non vide, l'ordr
3. Modèles natifs non sélectionnés, poussés sous le bloc sélectionné lors de la fusion du catalogue.
Sans `subagentModels`, les modèles routés restent en priorité `5`, les entrées natives GPT utilisent leur
-priorité (normalement `9` pour les entrées construites par opencodex), et le groupe routé reste provider/id
+priorité (sourcePriority, avec `9` en dernier recours), et le groupe routé reste provider/id
alphabétique.
## Exemple
diff --git a/docs-site/src/content/docs/fr/guides/providers.md b/docs-site/src/content/docs/fr/guides/providers.md
index 91cc03aaabe..726ae425a87 100644
--- a/docs-site/src/content/docs/fr/guides/providers.md
+++ b/docs-site/src/content/docs/fr/guides/providers.md
@@ -396,7 +396,7 @@ La relance exige `store: false` explicite, une entrée complète et uniquement d
Sur les routes officielles `https://opencode.ai/zen/v1` et `https://opencode.ai/inference/v1` du fournisseur canonique `opencode-zen` avec authentification par clé API, Gemini utilise automatiquement l’API native Google en mode `googleMode: ai-studio` (par défaut). Les identifiants Zen sont conservés, y compris Flash sans suffixe `-tiered`. Claude utilise automatiquement Anthropic Messages, quel que soit `googleMode`. Au chargement des configurations enregistrées, seules les anciennes surcharges Chat/Responses admissibles de ces modèles natifs sont ignorées, ainsi que les préférences admissibles de l’outil hébergé `image_generation` propres à Responses. Les fournisseurs personnalisés, les routes sans authentification par clé et les autres préférences restent inchangés.
-**La prise en charge de `opencode-free` a été supprimée** : tous les modèles renvoient désormais `403 permission_denied` à cause du contrôle du niveau gratuit en amont chez OpenCode ([#7255](https://github.com/lidge-jun/opencodex/issues/7255)). Le chargement d’une configuration existante désactive en mémoire les entrées concernées et leurs références de modèles, avertit une fois par processus et ne modifie pas le fichier. Si ce fournisseur était le défaut, la route OpenAI OAuth standard devient le défaut. Utilisez `opencode-go`, `opencode-zen` avec clé ou un autre fournisseur. [Conditions OpenCode Zen](https://opencode.ai/docs/zen/).
+**La prise en charge de `opencode-free` a été supprimée** : tous les modèles renvoient désormais `403 permission_denied` à cause du contrôle du niveau gratuit en amont chez OpenCode ([#7255](https://github.com/lidge-jun/opencodex/issues/7255)). Le chargement d’une configuration existante désactive en mémoire les entrées concernées et leurs références de modèles, avertit une fois par processus et ne modifie pas le fichier lors du chargement. Le prochain enregistrement supprime ces entrées et références de config.json. Avant cet enregistrement, le fichier original est automatiquement conservé dans `config.json.pre-opencode-free-removal.bak`, à côté de config.json, ou sous un nom unique si ce nom est occupé. Une copie indépendante, identique octet par octet, est réutilisée uniquement si sa protection réservée au propriétaire peut être vérifiée ; sinon, une nouvelle copie protégée est créée à côté. Aucun fichier existant n’est écrasé. Des premiers chargements simultanés peuvent laisser plusieurs copies identiques et protégées ; l’avertissement indique ce chemin. Pour restaurer la configuration, recopiez ce fichier sur config.json, par exemple après une rétrogradation. Si la copie échoue, l’avertissement demande de copier config.json immédiatement. Si ce fournisseur était le défaut, la route OpenAI OAuth standard devient le défaut. Utilisez `opencode-go`, `opencode-zen` avec clé ou un autre fournisseur. [Conditions OpenCode Zen](https://opencode.ai/docs/zen/). Les lectures passives appliquent uniquement la désactivation en mémoire, sans copie ni avertissement.
Les affectations et valeurs par défaut valides de Claude Desktop sont conservées. Les règles dont la liste de modèles sources autorisés devient vide sont désactivées, ainsi que la synchronisation native des sous-agents si son modèle d’injection est supprimé. Les sauvegardes suivantes ne rétablissent pas les références supprimées et préservent les modifications simultanées valides.
diff --git a/docs-site/src/content/docs/fr/guides/remote-link.md b/docs-site/src/content/docs/fr/guides/remote-link.md
index 82d715db467..abd65a23f7b 100644
--- a/docs-site/src/content/docs/fr/guides/remote-link.md
+++ b/docs-site/src/content/docs/fr/guides/remote-link.md
@@ -10,10 +10,10 @@ Une liaison entre machines connecte un ordinateur OpenCodex **Home** à un ordin
- Home peut se connecter à Child avec une clé OpenSSH.
- Pour une liaison initiée par Child, Child peut se connecter à Home avec une clé OpenSSH (la connexion par mot de passe n’est pas prise en charge).
- OpenCodex 2.66.0 ou ultérieur est installé sur Child (et sur Home pour une liaison initiée par Child).
-- Les deux ordinateurs utilisent macOS ou Linux.
+- Les ordinateurs peuvent utiliser macOS, Linux ou Windows x64. Windows nécessite OpenSSH Server et Windows PowerShell, sans Git Bash ni curl.
- Le tableau de bord qui ajoute un Child depuis Home est ouvert sur Home ou via une session Hub appairée. Transformer l’ordinateur actuel en Child exige une session de tableau de bord appairée par l’opérateur ; une session locale sans identifiant ne peut pas valider ce changement de routage.
-SSH par mot de passe et Windows restent hors du flux actuel. Une liaison peut être lancée des deux côtés : depuis Home, comme décrit ci-dessous, ou depuis Child, comme décrit dans la section « Connecter cet ordinateur comme Child ».
+SSH par mot de passe n'est pas pris en charge. Une liaison peut être lancée depuis Home ou Child. Windows utilise PowerShell via la même connexion OpenSSH vérifiée.
## Ajouter un Child depuis `#remote`
diff --git a/docs-site/src/content/docs/fr/guides/sub-agent-surface.md b/docs-site/src/content/docs/fr/guides/sub-agent-surface.md
index be3e9d21f82..5941150a65e 100644
--- a/docs-site/src/content/docs/fr/guides/sub-agent-surface.md
+++ b/docs-site/src/content/docs/fr/guides/sub-agent-surface.md
@@ -70,7 +70,7 @@ Par exemple, sans sélecteurs de compte et avec cette liste :
| 3 | `deepseek/deepseek-v4-flash` | `5` | Première ligne routée restante dans l'ordre alphabétique |
| 4 | `google/gemini-3-pro` | `5` | Ligne routée suivante |
| 5 | `moonshot/kimi-k3` | `5` | Ligne routée suivante |
-| 6+ | `zai/glm-5.2`, puis les lignes GPT natives | `5`, puis `102` | Hors des cinq |
+| 6+ | `zai/glm-5.2`, puis les lignes GPT natives | `5`, puis `102 + sourcePriority` | Hors des cinq |
Avec une liste vide, opencodex ne fixe aucune priorité et les cinq suivent les priorités normales du catalogue.
diff --git a/docs-site/src/content/docs/fr/guides/web-dashboard.md b/docs-site/src/content/docs/fr/guides/web-dashboard.md
index 1777b9be854..9b7cfe005b3 100644
--- a/docs-site/src/content/docs/fr/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/fr/guides/web-dashboard.md
@@ -13,9 +13,11 @@ catalogue, les services auxiliaires, les réglages des sous-agents et le trafic
ocx gui
```
-Cette commande ouvre `http://localhost:` dans votre navigateur — ou
-`http://127.0.0.1:` lorsque l’ingress de gestion du hub est activé — et démarre d'abord automatiquement le
-proxy si nécessaire. En développement, vous pouvez lancer séparément le serveur de développement de
+Cette commande ouvre l’adresse du proxy actif dans votre navigateur : `http://127.0.0.1:` par défaut,
+ou `http://[::1]:` pour une liaison à l’adresse de bouclage IPv6 littérale `::1`. Ces adresses conservent
+l’origine configurée nécessaire à l’appairage local du navigateur. Lorsque l’ingress de gestion du hub est activé,
+elle ouvre `http://127.0.0.1:`. Le proxy démarre d’abord automatiquement si nécessaire.
+En développement, vous pouvez lancer séparément le serveur de développement de
l'interface contre un proxy déjà actif :
```bash
@@ -37,6 +39,12 @@ remplissage automatique. Le tableau de bord lui-même ne conserve le jeton qu'en
dans `localStorage` ni dans `sessionStorage` ; son enregistrement dépend entièrement du navigateur ou du
gestionnaire de mots de passe.
+**Mémoriser cet appareil** est désactivé par défaut. Sur un tableau de bord HTTPS direct (ou en boucle locale), le serveur échange le jeton administrateur contre un cookie d’appareil HttpOnly, SameSite=Strict et une preuve distincte stockée dans le localStorage de ce site. Le cookie est Secure en HTTPS et limité à l’hôte. Ni le cookie ni la preuve ne sont le jeton administrateur, et aucun ne fonctionne seul. Les cookies sont partagés entre les ports d’un même nom d’hôte ; la preuve, limitée à l’origine, lie l’appareil mémorisé à l’origine de ce tableau de bord. Préférez tout de même un nom d’hôte dédié. Le jeton administrateur n’est jamais écrit dans localStorage ou sessionStorage. Si le stockage du navigateur est indisponible ou ne peut enregistrer la preuve, la mémorisation est signalée comme indisponible.
+
+Le fichier privé dashboard-sessions.json conserve les empreintes du cookie, de la preuve, du jeton administrateur et de l’identité du navigateur, l’origine du serveur et une expiration fixe de 30 jours. La relance de l’application Safari reprend une session courte tant que cet appareil reste autorisé. Les sessions mémorisées ne peuvent pas approuver les actions de consentement : étoile GitHub, consommation de crédits de réinitialisation, modification des couches de prompt, connexion Meta Muse, changements de compatibilité du bureau ou validation des quotas. Utilisez une nouvelle session locale ou appairée pour ces actions. Les tableaux de bord relayés, inter-origines ou HTTP distant ne prennent pas en charge la mémorisation. L’expiration, un changement d’identité du navigateur ou la rotation du jeton suivie d’un redémarrage imposent une nouvelle connexion.
+
+Une action **Oublier cet appareil** réussie, dans la barre latérale ou les actions mobiles, ainsi que la déconnexion réussie, révoque immédiatement l’enregistrement et toutes ses sessions et supprime la preuve du navigateur. Un échec de déconnexion conserve la preuve pour réessayer. Le serveur synchronise le contenu avant de publier le changement ; la persistance de l’entrée de répertoire après un plantage est assurée au mieux. Les anciennes clés de jeton mémorisé sont supprimées sans lecture ni migration. Réservez cette option aux appareils de confiance ; elle ne rétablit pas l’AutoFill de Safari.
+
## Barre de résumé des quotas
Une ligne de résumé en haut de chaque page, sauf la page Sécurité au démarrage, indique
diff --git a/docs-site/src/content/docs/fr/index.mdx b/docs-site/src/content/docs/fr/index.mdx
index f482f1fe671..52182d87459 100644
--- a/docs-site/src/content/docs/fr/index.mdx
+++ b/docs-site/src/content/docs/fr/index.mdx
@@ -1,12 +1,12 @@
---
-title: "opencodex — Exécutez Codex sur n’importe quel LLM"
-description: Proxy universel de fournisseurs pour OpenAI Codex et Claude Code — utilisez n’importe quel LLM avec Codex CLI, App, SDK et Claude Code.
+title: "opencodex — Une passerelle locale pour Codex, Claude Code et plus"
+description: "Une passerelle locale unique pour vos outils de code IA et vos modèles : utilisez les fournisseurs de votre choix avec Codex, Claude Code, Claude Desktop et Grok Build, et connectez d’autres outils via des configurations gérées."
template: splash
head:
# La marque d'abord, pas de suffixe « | opencodex » : Google compare le nom du site avec
# le titre de la page d'accueil, donc le mot-symbole mène et n'est pas dupliqué.
- tag: title
- content: "opencodex — Exécutez Codex sur n’importe quel LLM"
+ content: "opencodex — Une passerelle locale pour Codex, Claude Code et plus"
- tag: meta
attrs:
property: og:locale
diff --git a/docs-site/src/content/docs/fr/reference/cli.md b/docs-site/src/content/docs/fr/reference/cli.md
index 2563cdc0de9..2f16c1c5fce 100644
--- a/docs-site/src/content/docs/fr/reference/cli.md
+++ b/docs-site/src/content/docs/fr/reference/cli.md
@@ -17,7 +17,7 @@ Exécutez `ocx help` (ou `ocx --help` / `ocx -h`) pour afficher l’aide génér
Les commandes de gestion communiquent avec l’API de gestion du proxy actif. Elles s’appuient sur le port d’exécution enregistré et sur des contrôles d’identité, plutôt que sur un second chemin de configuration. Un proxy arrêté ou inaccessible est représenté par une réponse HTTP 503 et entraîne un code de sortie CLI non nul. Les commandes explicitement documentées comme des opérations de configuration hors ligne peuvent, quant à elles, valider et modifier le fichier de configuration sans proxy actif.
-`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
+`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows x64 prend aussi en charge un `codex.cmd` npm standard absolu lié à la preuve, avec une chaîne d’outils Node/npm liée par les handles — locale au préfixe quand elle est présente, sinon la paire PATH capturée ancrée à l’architecture du lanceur — et une plateforme de support vérifiée dans le préfixe, en utilisant des handles maintenus qui refusent la réanalyse. Les autres dispositions Windows restent différées ou non gérées. Un résultat `managed` Windows ne décrit que la disposition d’installation observée : le Node/npm lié est la chaîne d’outils de métadonnées en lecture seule de l’outil de mise à jour, et non une garantie sur l’interpréteur qu’une invocation ultérieure de `codex.cmd` résoudra. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Sur Windows x64, `apply` exige un préfixe npm borné doté d'une autorité d'écriture privée vérifiée et préserve les packages et shims non liés ; les dispositions observées non prises en charge renvoient `applicable: false` et `refusal: windows_apply_disabled` ; la sélection par PATH seul, les wrappers de démarrage automatique OpenCodex et les replis global/vendor restent différés. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
Pour observer une installation Windows x64, consultez [`attest`](/fr/reference/cli/agents/). Sans chemins explicites, la commande observe le candidat sélectionné identifié par l’instantané du lanceur lié à la preuve ; aucune autorité de mise à jour ni sélection du runtime n’est attestée.
diff --git a/docs-site/src/content/docs/fr/reference/cli/agents.md b/docs-site/src/content/docs/fr/reference/cli/agents.md
index 1337a76d0ce..a55deef9004 100644
--- a/docs-site/src/content/docs/fr/reference/cli/agents.md
+++ b/docs-site/src/content/docs/fr/reference/cli/agents.md
@@ -169,6 +169,39 @@ Les familles sont `opus`, `fable`, `sonnet` et `haiku` ; les nouveaux itinérai
seulement quand cette famille est vide. Les anciens indicateurs d'application `--static`, `--hybrid` et `--discovery-only`
restent soutenus. Utilisez `ocx claude config ...` pour les réglages Claude Code.
+### `ocx codex [codex args...]`
+
+La clé est fournie uniquement si la configuration et ses espaces de noms sont protégés contre les modifications par d’autres principaux ; l’utilisateur courant et les autorités privilégiées du système sont de confiance. Les processus du même utilisateur sont de confiance : un acteur disposant de vos droits d’écriture peut déjà lire votre fichier de clé OpenCodex. Le remplacement de configuration par ce même utilisateur entre vérification et démarrage natif est hors de ce modèle de menace ; le lanceur ne fige pas la configuration lors du transfert.
+
+Sur une installation Windows normale, les droits ProgramData par défaut permettent à d’autres utilisateurs de créer des dossiers. Les identifiants automatiques sont donc retenus même si `ProgramData\OpenAI\Codex` est absent. Le dossier de configuration système de Codex sous ProgramData doit exister et être accessible en écriture uniquement à Administrators/SYSTEM ; ses dossiers ancêtres doivent empêcher leur remplacement par d’autres utilisateurs. Demandez à un administrateur de vérifier la politique des dossiers. opencodex ne modifie jamais lui-même les ACL et ne fournit aucune commande de préparation.
+
+Vous pouvez aussi fournir la clé vous-même, par exemple en exportant `OPENCODEX_API_AUTH_TOKEN` ; `ocx codex` conserve sa valeur. Les fichiers de configuration système existants empêchent toujours les identifiants automatiques et nécessitent un examen séparé. Chaque motif de refus produit une courte ligne stderr précise, sans valeur de clé, contenu ni chemin de fichier de clé.
+
+Placez `--` avant une invite d’un seul mot pour éviter la sélection d’une sous-commande Codex inconnue.
+
+L’espace de configuration machine doit empêcher un autre utilisateur de créer les fichiers système absents. Un espace Windows ProgramData/OpenAI/Codex non protégé bloque l’injection sans créer de répertoire. macOS accepte les ACL de refus seules et les autorisations en lecture seule. Toute autorisation d’écriture, ajout, suppression, modification d’attributs, de sécurité ou de propriétaire est refusée, même après un refus. Les tags inconnus et erreurs d’inspection entraînent un refus.
+
+Un nouveau jeton de service est chargé uniquement si argv respecte la liste ci-dessous, si aucune variable non vide de proxy HTTP(S)/ALL ou `CODEX_*`/`OPENAI_*`/`AZURE_OPENAI_*` ne surcharge l’environnement (`CODEX_HOME` canonique est permis), et si les espaces home/config/système passent les contrôles de propriété. La configuration doit contenir uniquement les clés auditées et le fournisseur loopback géré exact, sans autre consommateur de clé ni couche de configuration. Tout argument hors liste, dont `-c`/`--config`, `-p`/`--profile`, les options inconnues et `resume`/`fork`, retient le jeton avec un diagnostic sans valeurs ; les identifiants explicites et les arguments sont préservés.
+
+Le répertoire canonique et `config.toml` doivent appartenir à l’utilisateur courant. POSIX refuse les écritures group/other ; Windows vérifie le SID propriétaire et refuse les droits d’écriture DACL hors utilisateur courant, SYSTEM et Administrators. Les droits de remplacement des ancêtres sont vérifiés. macOS exige aussi l’absence de préférences Codex forcées. Ces contrôles n’isolent pas un processus malveillant du même utilisateur ou d’un administrateur.
+
+| Allowlist | Flags |
+| --- | --- |
+| Switches | `--no-daemon`, `--json`, `--ephemeral`, `--skip-git-repo-check`, `--no-alt-screen`, `--full-auto`, `--dangerously-bypass-approvals-and-sandbox`, `--search` |
+| One value | `--model`/`-m`, `--sandbox`/`-s`, `--ask-for-approval`/`-a`, `--image`/`-i` |
+
+
+
+Le chargement automatique interprète `CODEX_HOME` comme un chemin littéral, y compris `~` et les espaces, le vérifie et transmet le même chemin absolu canonique au processus enfant. Un répertoire illisible empêche le chargement. Toute valeur non vide de `HTTP_PROXY`, `HTTPS_PROXY` ou `ALL_PROXY`, quelle que soit la casse, empêche aussi le chargement indépendamment de `NO_PROXY` ; les lancements refusés et les identifiants exportés par l’utilisateur restent inchangés. Avec un nouveau jeton, `NO_PROXY` et `no_proxy` du processus enfant incluent `127.0.0.1` et `::1` en conservant les exclusions existantes. Activer `features.respect_system_proxy` ou transmettre `--enable`/`--disable` empêche le chargement automatique.
+
+Le lanceur tente de démarrer le proxy puis transmet les arguments d’origine à Codex, sans réécrire sa configuration. Les routes natives et les passerelles tierces sautent le démarrage. Une route inconnue avertit puis tente le démarrage ; un échec avertit et continue. L’aide, la version et les commandes internes comme `login`, `doctor` et `help` sautent le démarrage et la lecture automatique du jeton.
+
+Un `OPENCODEX_API_AUTH_TOKEN` absent est lu dans le `service-api-token` de cette installation uniquement si toute la configuration de transport du provider sélectionné correspond au bloc géré pour son écouteur loopback ou sa connexion client locale enregistrée. Le nom et les commentaires ne suffisent pas. Destinations inconnues ou distantes, profile par défaut, configuration de projet, `OPENAI_BASE_URL` et options transmises `-c`/`--config`, `-p`/`--profile`, `-C`/`--cd`, `--remote` désactivent cette lecture. Les identifiants exportés et les arguments sont conservés ; les remplacements du fichier et les collisions avec les jetons d’administration sont refusés.
+
+Seul un wrapper autostart correspondant (`matched`) lance directement son Codex sous-jacent. Un état inconnu, y compris une exception d’inspection, exige un contournement vérifié ; les wrappers CMD Windows gérés doivent correspondre au modèle généré complet avant leur exécution avec `OCX_SHIM_BYPASS=1`. Les lanceurs npm ordinaires restent pris en charge. Un wrapper invérifiable ou obsolète entraîne le code de sortie 1 sans démarrer Codex ; actualisez-le avec `ocx codex-shim install`. Le lancement ne répare pas les shims ; le contrôle de contenu ne garantit ni la provenance du fichier Windows sous-jacent ni l’absence de remplacement concurrent.
+
+Aide du lanceur : `ocx help codex` ; aide de Codex : `ocx codex --help`. Le comportement ultérieur de `codex` seul est conservé.
+
### `ocx opencode [opencode args...]`
Vérifiez que le proxy est actif, puis lancez opencode avec les blocs `provider.opencodex` et `providers.opencodex` générés dans la couche d’exécution intégrée d’OpenCode (`OPENCODE_CONFIG_CONTENT`). La configuration intégrée existante est préservée et seules ces deux clés sont remplacées pour ce lancement. Les fichiers `opencode.json` globaux ou propres au projet peuvent être lus afin de signaler une substitution existante, mais les fichiers sur disque ne sont jamais modifiés. Les modèles routés apparaissent sous la forme `opencodex//`. Un lancement ultérieur de `opencode` sans intermédiaire se comporte exactement comme auparavant.
@@ -287,7 +320,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows diffère l’inspection sans lire les fichiers du candidat ni la configuration. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
+`check` ne consulte aucun registre et n’exécute pas Codex. Sur POSIX, `selectionAttested` peut être `true` si le résolveur choisit le même chemin canonique que le candidat d’environnement capturé par le lanceur publié. Un manifeste npm-global lié peut alors établir `managed: true` s’il prouve la propriété et, lorsqu’une version candidate est signalée, si elle correspond à celle du manifeste ; les candidats persistants restent non attestés. Windows x64 prend aussi en charge un `codex.cmd` npm standard absolu lié à la preuve, avec une chaîne d’outils Node/npm liée par les handles — locale au préfixe quand elle est présente, sinon la paire PATH capturée ancrée à l’architecture du lanceur — et une plateforme de support vérifiée dans le préfixe, en utilisant des handles maintenus qui refusent la réanalyse. Les autres dispositions Windows restent différées ou non gérées. Un résultat `managed` Windows ne décrit que la disposition d’installation observée : le Node/npm lié est la chaîne d’outils de métadonnées en lecture seule de l’outil de mise à jour, et non une garantie sur l’interpréteur qu’une invocation ultérieure de `codex.cmd` résoudra. Une exécution directe via Bun ou les sources n’a pas de preuve du lanceur. `plan` consulte le registre officiel avec une configuration, un cache et des journaux npm temporaires isolés, et refuse les sessions actives ou une observation incomplète des processus. Il n’installe rien et n’envoie aucun signal. Sur Windows x64, `apply` exige un préfixe npm borné doté d'une autorité d'écriture privée vérifiée et préserve les packages et shims non liés ; les dispositions observées non prises en charge renvoient `applicable: false` et `refusal: windows_apply_disabled` ; la sélection par PATH seul, les wrappers de démarrage automatique OpenCodex et les replis global/vendor restent différés. Voir [les commandes et motifs de refus complets (en anglais)](/reference/cli/agents/#read-only-codex-cli-update-plan).
Sous Windows, une commande simple capturée comme `CODEX_CLI_PATH=codex`, un chemin distant ou un chemin de périphérique produit plutôt `candidate_path_unavailable`. Le candidat a été capturé, mais son chemin ne convient pas à cette inspection.
@@ -322,3 +355,19 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_
Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available.
The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support.
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
index d5e247ccad0..fcd71d1fe5d 100644
--- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md
@@ -34,6 +34,10 @@ ocx start --socks5-off
Arrête le proxy actif à partir de son PID, supprime le fichier de PID et rétablit le fonctionnement natif de Codex. Si un service d’arrière-plan géré est installé, `ocx stop` l’arrête d’abord afin qu’il ne puisse pas relancer le proxy. Le bouton **Stop** du tableau de bord Web exécute la même opération (`POST /api/stop`) sur tous les backends, sauf le Planificateur de tâches Windows : le wrapper peut y relancer le proxy après la fin de la tâche, donc le tableau de bord refuse avec `respawnable_service`, ne modifie rien et vous demande d'exécuter `ocx stop`.
+Sur un client hub connecté, `ocx stop`, y compris via l’API, conserve le routage Codex, le catalogue et la configuration Grok appartenant à la connexion ; `ocx start` les dessert de nouveau. L’arrêt explicite d’un Remote Link Child rétablit au contraire, si la restauration partagée réussit, le routage natif de Codex, le catalogue et les métadonnées de reprise sauvegardées, et supprime la configuration Grok gérée. La connexion Home enregistrée reste conservée ; `ocx disconnect` la supprime.
+
+Pour réutiliser Home depuis Codex après cette restauration, lancez `ocx start` (ou `ocx service start` si un service est installé), puis `ocx sync --restart-codex`. Le démarrage seul ne réapplique pas le routage ; sync ne recrée pas le bloc Grok. Si l’appartenance de la connexion ne peut pas être confirmée avant l’arrêt du service, l’API d’arrêt renvoie HTTP 409 avec `sharedTeardown: "refused"` et laisse le proxy et le gestionnaire de service actifs. Attendez la fin de la connexion ou déconnexion en cours, puis réessayez.
+
### `ocx restart`
Lorsqu’un proxy est actif, demande précisément au PID et au port dont l’identité a été vérifiée de redémarrer sur place, attend la fin du drainage normal, puis vérifie qu’un autre PID d’exécution écoute sur le même port. Le routage géré et la supervision du service restent installés pendant toute l’opération. Une requête dont l’état est incertain est observée, et non rejouée comme lors d’une séquence distincte d’arrêt et de démarrage.
@@ -355,7 +359,7 @@ Lorsqu'une version plus récente du paquet est connue, la zone de notification a
### `ocx gui`
-Ouvre le [tableau de bord Web](/fr/guides/web-dashboard/) à l’adresse `http://localhost:` — ou `http://127.0.0.1:` lorsque l’ingress de gestion du hub est activé — et démarre automatiquement le proxy s’il n’est pas actif.
+Ouvre le [tableau de bord Web](/fr/guides/web-dashboard/) à l’adresse `http://127.0.0.1:` — ou `http://127.0.0.1:` lorsque l’ingress de gestion du hub est activé — et démarre automatiquement le proxy s’il n’est pas actif.
## Mise à jour
diff --git a/docs-site/src/content/docs/fr/reference/client-support.md b/docs-site/src/content/docs/fr/reference/client-support.md
new file mode 100644
index 00000000000..1c9ca908c40
--- /dev/null
+++ b/docs-site/src/content/docs/fr/reference/client-support.md
@@ -0,0 +1,60 @@
+---
+title: "Clients pris en charge"
+description: "Les outils de code auxquels opencodex se connecte, la façon dont chacun est relié et le protocole qu’il utilise."
+---
+
+
+
+
+opencodex est une passerelle locale unique pour vos outils de code IA et vos modèles. Il configure certains outils nativement ; pour les autres, il exporte ou gère un fichier de configuration qui dirige l’outil vers le proxy local. Cette page est générée à partir des registres du code et reflète donc exactement ce que fournit la version actuelle.
+
+## Niveaux de prise en charge
+
+- **implemented**: le client figure dans le registre. Pour les configurations exportées, le protocole indiqué est lu dans la configuration générée par opencodex, et la génération échoue s’il change. Un protocole marqué *revu* désigne un point de terminaison issu d’une revue plutôt que de la configuration elle-même (clients natifs, et clients qui déduisent un point de terminaison précis d’un réglage) ; le générateur vérifie que le code revu est toujours en place. Les lignes « Compatible OpenAI » n’affirment aucun point de terminaison.
+- **test-covered**: implemented, et un cas de test choisi en revue qui exerce ce client est lié. Le générateur vérifie seulement que le test existe toujours et qu’il est actif ; lisez-le pour savoir ce qu’il vérifie.
+- **Vérifié à l’exécution**: une observation datée : une personne a exécuté le produit et la version du client indiqués avec la version d’opencodex indiquée, pour le scénario décrit, à cette date, et a lié la preuve. Cela ne dit rien des autres versions ou produits. La preuve est revue lors de l’ajout du résultat ; le générateur ne la rejoue pas. Un tiret signifie qu’aucun résultat n’est enregistré.
+
+## Clients
+
+| Client | Natif | Export de configuration | Intégration de fichier gérée | Protocole entrant | Niveau | Vérifié à l’exécution | Remarques |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| Codex (CLI, app, SDK) | Oui | — | — | Responses (revu) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/codex-integration/codex-inject-missing-config.test.ts)) | — | Configuré par opencodex lui-même ; aucun fichier de configuration exporté. [guide](/fr/guides/codex-integration/) |
+| Claude Code | Oui | — | — | Anthropic Messages (revu) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/claude-integration/claude-cli.test.ts)) | — | Configuré par opencodex lui-même ; aucun fichier de configuration exporté. [guide](/fr/guides/claude-code/) |
+| Claude Desktop | Oui | — | — | Anthropic Messages (revu) | implemented | — | Configuré par opencodex lui-même ; aucun fichier de configuration exporté. |
+| Grok Build | Oui | — | — | Responses (revu) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/providers/xai/grok-config-inject.test.ts)) | — | Configuré par opencodex lui-même ; aucun fichier de configuration exporté. [guide](/fr/guides/grok-build/) |
+| Qoder | — | JSON | Oui | Compatible OpenAI (point de terminaison choisi par le client) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/qoder-client.test.ts)) | — | Écoute locale uniquement. |
+| Qoder CN | — | JSON | Oui | Compatible OpenAI (point de terminaison choisi par le client) | implemented | — | Écoute locale uniquement. |
+| WorkBuddy | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/agent-clients.test.ts)) | — | Écoute locale uniquement. L’application exécute d’abord une vérification propre au client et peut refuser. |
+| WorkBuddy AI | — | JSON | Oui | Chat Completions | implemented | — | Écoute locale uniquement. L’application exécute d’abord une vérification propre au client et peut refuser. |
+| OpenCode | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | L’export prend aussi en charge une écoute distante (il ajoute l’en-tête de clé API). [guide](/fr/guides/opencode/) |
+| Pi | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Écoute locale uniquement. [guide](/fr/guides/pi/) |
+| Oh My Pi (omp) | — | YAML | Oui | Chat Completions; Responses (modèles OpenAI natifs uniquement) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Écoute locale uniquement. Ne modifie que son propre bloc YAML. |
+| Hermes | — | YAML | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/hermes-catalog-refresh.test.ts)) | — | L’export prend aussi en charge une écoute distante (il ajoute l’en-tête de clé API). Ne modifie que son propre bloc YAML. |
+| OpenClaw | — | JSON5 | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/gui/integrations-invariants.test.ts)) | — | L’export prend aussi en charge une écoute distante (il ajoute l’en-tête de clé API). |
+| Kimi CLI | — | TOML | Oui | Compatible OpenAI (point de terminaison choisi par le client) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export-new-clients.test.ts)) | — | Écoute locale uniquement. |
+| Gajae | — | YAML | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-modality-enum.test.ts)) | — | Écoute locale uniquement. |
+| DSH | — | YAML | Oui | Responses | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Écoute locale uniquement. Écrit dans le magasin de fournisseurs actuel du client. Ne modifie que son propre bloc YAML. Les écritures passent par un fichier de verrou placé à côté de la configuration. |
+| MCode | — | YAML | Oui | Anthropic Messages | implemented | — | Écoute locale uniquement. Les écritures passent par un fichier de verrou placé à côté de la configuration. |
+| ZCode | — | JSON | Oui | Responses (revu) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/integrations-current-store.test.ts)) | — | Écoute locale uniquement. Écrit dans le magasin de fournisseurs actuel du client. Le client choisit le point de terminaison à partir de ce réglage (vérifié en revue d’après le comportement documenté du client). |
+| Command Code | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/command-code-client.test.ts)) | — | Écoute locale uniquement. Les écritures passent par un fichier de verrou placé à côté de la configuration. |
+| Prime | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/prime-client.test.ts)) | — | Écoute locale uniquement. |
+| Aside | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/aside-profiles-catalog-refresh.test.ts)) | — | Écoute locale uniquement. |
+| Raycast | — | YAML | Oui | Chat Completions (revu) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/raycast-client.test.ts)) | — | Écoute locale uniquement. Le client choisit le point de terminaison à partir de ce réglage (vérifié en revue d’après le comportement documenté du client). |
+| omo | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/omo-client.test.ts)) | — | Écoute locale uniquement. |
+| Cline | — | JSON | Oui | Responses | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/cline-client.test.ts)) | — | Écoute locale uniquement. Les écritures passent par un fichier de verrou placé à côté de la configuration. |
+| Kilo Code | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-reasoning-controls.test.ts)) | — | L’export prend aussi en charge une écoute distante (il ajoute l’en-tête de clé API). |
+| Factory Droid | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/droid-client.test.ts)) | — | Écoute locale uniquement. [guide](/fr/guides/factory-droid/) |
+| Gentle Shell | — | JSON | Oui | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/gentle-shell-client.test.ts)) | — | Écoute locale uniquement. |
+| T3 Code | — | JSON | Oui | Anthropic Messages | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/t3code-client.test.ts)) | — | Écoute locale uniquement. L’application exécute d’abord une vérification propre au client et peut refuser. |
+
+## Points de terminaison entrants
+
+Le proxy accepte ces protocoles. Un outil absent de la liste peut quand même se connecter s’il permet de définir un point de terminaison personnalisé pour l’un d’eux.
+
+- `POST /v1/responses`: Responses
+- `POST /v1/chat/completions`: Chat Completions
+- `POST /v1/messages`: Anthropic Messages
+
+## Tenir cette page à jour
+
+Cette page est générée par `scripts/generate-client-support-matrix.ts` à partir des registres d’export, d’intégration gérée et d’intégration native. Pour ajouter un client, ajoutez son entrée dans `scripts/client-support-matrix/metadata.ts` puis lancez `bun run docs:clients` ; `bun run docs:clients:check` échoue tant que la page validée est obsolète.
diff --git a/docs-site/src/content/docs/fr/reference/configuration.md b/docs-site/src/content/docs/fr/reference/configuration.md
index 36ed8b6a383..dbc24b3d8ce 100644
--- a/docs-site/src/content/docs/fr/reference/configuration.md
+++ b/docs-site/src/content/docs/fr/reference/configuration.md
@@ -27,6 +27,12 @@ Si le fichier ne peut pas être analysé, opencodex le sauvegarde en tant que
`config.json.invalid-`, affiche un avertissement dans la console et démarre avec les valeurs par défaut. Si le fichier est absent,
la valeur par défaut d'une nouvelle installation est également utilisée : un fournisseur `openai` en mode transfert.
+Les commandes locales `ocx config set`, `unset`, `import`, la commande hors ligne `ocx effort`,
+les opérations add/remove sur les modèles personnalisés et add/remove/set-default sur les fournisseurs
+refusent un fichier `config.json` existant invalide ou illisible sans en modifier les octets.
+Exécutez `ocx config validate` et réparez le fichier avant de réessayer. Si une configuration existe déjà,
+import exige également que le fichier cible soit lisible et valide.
+
## Priorité et valeurs par défaut
Les valeurs valides de `config.json` remplacent les valeurs par défaut intégrées. Les champs facultatifs absents utilisent les valeurs par défaut
diff --git a/docs-site/src/content/docs/fr/reference/configuration/providers.md b/docs-site/src/content/docs/fr/reference/configuration/providers.md
index a46a4f89add..e63eb4e35a0 100644
--- a/docs-site/src/content/docs/fr/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/fr/reference/configuration/providers.md
@@ -64,6 +64,8 @@ Après une inscription ou une connexion OAuth dans l’interface, une boîte de
| `codexPinnedTransientPolicy?` | `"hold" \| "detour-new-threads"` | `"hold"` | Quand un compte épinglé est dégradé, `hold` continue de l'utiliser et journalise un avertissement ; `detour-new-threads` ne déplace que les nouveaux fils. Un tour déjà envoyé n'est jamais renvoyé. |
| `codexWsReuseAcrossTurns?` | `boolean` | `false` | Réutilisation optionnelle du WebSocket Codex entre les tours d'un même compte et fil. Désactivé par défaut. Activé, un socket occupé est attendu au plus 750 ms et jusqu'à deux sockets restent lors d'un changement de modèle ou de palier. |
| `upstreamHostCircuitThreshold?` | `number` | `0` | Seuil facultatif du coupe-circuit pour les échecs DNS/TCP avérés avant connexion sur les requêtes Responses OpenAI natives en mode transfert et les envois compacts. `0` le désactive ; `1`–`20` ouvre, après ce nombre de requêtes logiques arrivées à leur terme, une temporisation de 30 secondes propre à l'origine du fournisseur. Tant que le circuit est ouvert, les requêtes reçoivent `503` avec `Retry-After` avant la sélection du compte ou l'envoi en amont ; après la temporisation, une requête est admise en état semi-ouvert. Les délais d'attente et les réponses HTTP ne sont jamais comptabilisés, et toute réponse HTTP ferme le circuit. Ce mécanisme s'applique uniquement au routage du pool Codex sans compte épinglé ; il reste inactif pour `codexAccountMode: "direct"` et les sélecteurs qualifiés par compte. |
+| `maxInboundBodyBytes?` | `number` | `0` | Limite en octets de la taille décodée d'un corps JSON entrant par requête. Une valeur absente ou `0` conserve les 256 MiB par défaut ; la valeur configurée est bornée à 1–512 MiB. Redémarrage requis. Un dépassement produit un HTTP 413 local avec le code `inbound_body_too_large`. Une limite effective supérieure à 256 MiB active aussi le contrôle partagé `maxConcurrentInboundBodyBytes` ; relever la limite par requête ne relève pas à lui seul le budget partagé. Routes concernées et détails : [admission des corps entrants](/fr/reference/inbound-body-admission/). |
+| `maxConcurrentInboundBodyBytes?` | `number` | `536870912` (512 MiB) | Budget partagé d'admission facultatif de premier niveau, en octets, actif uniquement si la valeur effective de `maxInboundBodyBytes` dépasse 256 MiB. Une valeur absente, `0` ou invalide utilise les 512 MiB par défaut, inchangés. Les entiers positifs valides sont bornés à 512 MiB–2 GiB (`536870912`–`2147483648`) lors de leur utilisation. Redémarrage requis ; tous les écouteurs d'un processus partagent un seul budget. Il s'agit d'un décompte d'admission, pas d'une préallocation mémoire, d'une limite RSS ou d'un dimensionnement automatique selon la RAM. Choisir 2 GiB exige une décision explicite de l'opérateur. Voir [l'admission des corps entrants](/fr/reference/inbound-body-admission/) pour l'exemple, les compromis de file et de cycle de vie des réponses, et la raison pour laquelle le budget par défaut ne permet qu'un seul envoi complet de 512 MiB avant l'analyse. |
| `modelCacheTtlMs?` | `number` | `300000` | Fenêtre de fraîcheur pour le cache `/models` par fournisseur. |
| `cacheRetention?` | `"none" \| "short" \| "long"` | Anthropic direct : `"long"` ; autres points de terminaison compatibles Anthropic : `"short"` | Politique Anthropic de mise en cache des invites : désactivée, éphémère pendant 5 minutes ou étendue à 1 heure. Sans valeur, les requêtes vers `api.anthropic.com` (clé API ou pools de comptes OAuth) utilisent un TTL d’une heure ; les passerelles et points de terminaison compatibles gardent 5 minutes. Une valeur explicite l’emporte toujours. |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | désactivé | Politique facultative d'actualisation proactive OAuth et de préchauffage des comptes Codex. |
diff --git a/docs-site/src/content/docs/fr/reference/configuration/server.md b/docs-site/src/content/docs/fr/reference/configuration/server.md
index 20e8336a1f8..6808cfe4b49 100644
--- a/docs-site/src/content/docs/fr/reference/configuration/server.md
+++ b/docs-site/src/content/docs/fr/reference/configuration/server.md
@@ -15,7 +15,7 @@ exécute des fonctionnalités d'assistance autour des demandes du fournisseur.
| `proxy?` | `string` | — | URL du proxy HTTP(S) ou SOCKS5 sortant (`socks5://host:port`) ou `${ENV_VAR}`. Les URL HTTP s’appliquent à `HTTP_PROXY` / `HTTPS_PROXY` si elles sont vides. Les URL SOCKS5 utilisent le tunnel SOCKS5 intégré et sont aussi exposées via `ALL_PROXY` (`ocx start --socks5`); `HTTP(S)_PROXY` héritées sont effacées dans ce processus. Le bouclage reste dans `NO_PROXY`. |
| `emptyCompletionRetry?` | `boolean` | `false` | Active une nouvelle tentative Responses identique lorsqu’une réponse ne contient ni texte ni appel d’outil. Cette tentative peut être facturée. `OCX_EMPTY_COMPLETION_RETRY=0` la désactive sans modifier la configuration ; les combinaisons et les tours de compactage routés restent exclus. |
| `stallTimeoutSec?` | `number` | `300` (public) / désactivé (local) | Secondes sans progression utile en amont (Responses et Chat natif) avant la coupure du flux. Sans réglage, un amont **local** (loopback, privé ou nom `.local`/`.lan`) est désactivé par défaut et un amont public vaut 300 s ; une valeur positive s'applique aux deux (minimum 1 s) ; `0` désactive partout le watchdog de silence. Pour Responses qui replie le SSE canonique ChatGPT en JSON non-streaming, un plafond total indépendant de 15 minutes subsiste même lorsque ce watchdog est désactivé. Les lectures de corps en attente de `/v1/responses/compact` partagent ce budget mais valent 300 s par défaut même pour un amont local ; une valeur explicite, y compris `0`, prime. |
-| `connectTimeoutMs?` | `number` | `200000` | Délai maximal par tentative pour DNS/TCP/TLS et les en-têtes finaux ; il prend fin avant la génération du corps. |
+| `connectTimeoutMs?` | `number` | `200000` (public) / désactivé (local) | Délai par tentative entre l'envoi de la requête et le premier en-tête de réponse de l'amont (DNS/TCP/TLS et attente des en-têtes) ; il prend fin avant la génération du corps et est indépendant de `stallTimeoutSec`, qui régit le flux du corps. Sans réglage, un amont **local** (loopback, privé ou nom `.local`/`.lan`) est désactivé par défaut et un amont public vaut 200 s ; une valeur positive s'applique aux deux ; `0` (ou toute valeur non positive) désactive partout ce délai d'en-tête. La désactivation permet à un modèle local de passer plusieurs minutes en prompt prefill sur une requête volumineuse sans que l'horloge du premier en-tête ne coupe une requête saine. |
| `shutdownTimeoutMs?` | `number` | `5000` | Délai de vidange gracieux avant l’annulation des tours actifs. |
| `websockets?` | `boolean` | `false` | Annonce et autorise la route WebSocket Responses destinée aux clients. La valeur false maintient les clients sur HTTP/SSE ; elle ne désactive pas une optimisation WebSocket canonique admissible vers ChatGPT en amont. |
| `corsAllowOrigins?` | `string[]` | `[]` | Origines exactes supplémentaires autorisées par CORS. Les origines de bouclage sont toujours autorisées. Les origines d'extensions de navigateur basées sur l'autorité telles que `chrome-extension://` sont prises en charge ; `*` n'est pas un caractère générique. Firefox et Safari régénèrent l'extension UUID (par installation / par lancement de navigateur), mettez donc à jour l'entrée lorsque l'origine change. |
@@ -322,3 +322,11 @@ Le retour à 2.80.0 ne garantit ni l’agrégation canonique ni le même solde d
Les journaux de versions expérimentales non publiées contenant `pool-current` ou `poolContinuity` sont exclus du contrat C, même après compactage. Leurs soldes d’origine restent un historique inconnu jusqu’à l’expiration normale, sans conversion ni suppression immédiate.
Un enregistrement complet mais invalide, y compris un `null` final, bloque l’admission sous plafond. Le compactage peut conserver les données valides dans un point de contrôle propre, mais le refus reste actif jusqu’au redémarrage avec ce journal propre. Une dernière ligne JSON incomplète suit les règles de récupération existantes. Voir les limites et codes d’erreur dans la [référence anglaise](/reference/configuration/server/#historical-pool-continuity).
+
+## Classification des délais des upstreams locaux
+
+Cette classification règle les délais, pas les autorisations réseau. Sans requête DNS, les destinations HTTP(S) locales sont : `localhost` exact (casse indifférente, point final admis), `0.0.0.0`, loopback IPv4 (127/8), plages privées RFC 1918 et link-local (169.254/16), loopback IPv6, link-local (fe80::/10), unique-local (fc00::/7), équivalents IPv4-mapped et noms `.local`/`.lan`. Les autres noms DNS gardent les délais publics même s'ils résolvent vers loopback. Les destinations invalides, les schémas non pris en charge et `localhost.example` restent publics.
+
+Désactiver le délai d'en-tête préserve l'annulation client et toute échéance globale englobante. Un `stallTimeoutSec` explicitement positif borne le silence du corps local ; sans délais locaux header/stall configurés, aucune borne automatique ne précède les en-têtes. Le JSON local de Chat/Messages natif utilise le budget stall ; le JSON public conserve `max(connectTimeoutMs ?? 200000, 5000)`. Le corps compact conserve 300 secondes par défaut, même en local. Les budgets image/recherche restent inchangés.
+
+Le bloc facultatif `opencodeClientExport` accepte `headerTimeoutMs`, `chunkTimeoutMs` et `timeoutMs` : millisecondes positives ou `false` pour désactiver. Les valeurs absentes préservent les défauts OpenCode ; les champs invalides sont omis. Ces réglages couvrent tout le provider, dans options V1 et settings V2. Voir [modèles locaux lents](/guides/opencode/#slow-local-models-prefill-timeouts).
diff --git a/docs-site/src/content/docs/fr/reference/inbound-body-admission.md b/docs-site/src/content/docs/fr/reference/inbound-body-admission.md
index 6ca406cd940..cc3cc9efd6f 100644
--- a/docs-site/src/content/docs/fr/reference/inbound-body-admission.md
+++ b/docs-site/src/content/docs/fr/reference/inbound-body-admission.md
@@ -9,14 +9,67 @@ reste 256 MiB et les valeurs configurées restent comprises entre 1 MiB et 512 M
proxy après avoir modifié la limite afin que l'écouteur et les lecteurs utilisent ensemble la
valeur voulue.
+Conserver l'ancien budget partagé par défaut de 512 MiB ne supprime pas les compromis
+décrits ci-dessous concernant la file et la rétention pendant le cycle de vie des réponses.
+
+## Choisir explicitement le budget partagé
+
+`maxConcurrentInboundBodyBytes` est un réglage facultatif de **premier niveau** dans
+`config.json`, exprimé en octets. Sa valeur par défaut reste **512 MiB (`536870912`)**,
+comme auparavant. Une valeur absente, `0` ou invalide utilise cette valeur par défaut.
+Un entier positif valide est borné à **512 MiB–2 GiB (`536870912`–`2147483648`)** lors
+de son utilisation. Redémarrez le proxy après modification. Tous les écouteurs d'un même
+processus partagent un seul budget, distinct de `appOwnedMemoryBudgetMb`.
+
+Pour choisir une limite de 512 MiB par requête et activer explicitement un budget partagé
+de 2 GiB, ajoutez ces champs au premier niveau de votre `config.json`, puis redémarrez :
+
+```json
+{
+ "maxInboundBodyBytes": 536870912,
+ "maxConcurrentInboundBodyBytes": 2147483648
+}
+```
+
+Ce choix de l'opérateur augmente le pic potentiel de consommation mémoire. Le proxy ne
+détecte pas automatiquement la RAM disponible et ne choisit pas le budget en fonction de celle-ci.
+
## Augmenter la limite modifie la simultanéité
-Quand la limite résolue dépasse 256 MiB, chaque requête HTTP concernée réserve toute sa marge
-configurée dans un budget d'admission partagé de 512 MiB pour le processus entier. Au plus une
-requête de ce type peut donc s'exécuter à la fois, même si plusieurs écouteurs partagent le
-processus. Les petits corps réservent eux aussi toute la marge ; ni un petit Content-Length ni
-la compression ne contournent l'admission. La réservation demeure jusqu'à la fin du flux de
-réponse, son échec ou la fin de l'annulation, et pas seulement jusqu'à l'analyse du JSON entrant.
+Quand la limite effective dépasse 256 MiB, chaque requête HTTP concernée doit réserver toute
+sa marge par requête **avant toute lecture du corps**. Ces réservations partagent le budget
+`maxConcurrentInboundBodyBytes` du processus, dont la valeur par défaut reste **512 MiB**.
+
+Avant la fin de l'analyse JSON avec contrôle de taille, ni un petit `Content-Length` ni un petit
+corps compressé sur le réseau ne réduisent la réservation. L'admission ne considère pas ces
+valeurs comme une preuve fiable de la taille décodée ou normalisée du JSON.
+
+Après une analyse JSON réussie dans les limites de taille, le lecteur réduit la réservation
+au maximum des octets observés sur le réseau, des octets décodés et de la mesure existante du
+JSON normalisé lorsqu'un traducteur fournit un budget pour cette mesure. La réservation ne
+peut que diminuer et reste plafonnée à la marge initiale par requête ; une mesure du JSON
+normalisé supérieure à cette marge ne l'augmente jamais. Les contrôles UTF-8 et de JSON
+resérialisé sont conservés ; la longueur d'entrée ne remplace pas la taille potentiellement
+plus grande du JSON normalisé.
+
+Avec le **budget partagé par défaut de 512 MiB** et une **limite de 512 MiB par requête**,
+**un seul envoi non encore analysé** peut réserver toute sa marge. Même si l'analyse réduit
+sa réservation à **un octet**, la capacité restante est inférieure à 512 MiB. La prochaine
+marge complète doit donc attendre qu'assez de capacité soit libérée. La valeur par défaut
+**ne résout pas** le problème initial de simultanéité avec une limite de 512 MiB par requête.
+
+Définir explicitement `maxConcurrentInboundBodyBytes` à `2147483648` choisit **2 GiB** et
+permet jusqu'à **4 envois réservant chacun 512 MiB** avant l'analyse et les réductions après
+mesure. Ce budget de 2 GiB ne fixe pas un plafond de 4 requêtes après l'analyse : par exemple,
+**8 très petites requêtes peuvent diffuser leur réponse simultanément** lorsque les réservations
+réduites laissent de la place aux admissions suivantes, sous réserve du budget partagé et
+des autres limites.
+
+La réservation réduite reste détenue pendant le travail asynchrone et l'attente du fournisseur,
+jusqu'à la fin du flux de réponse (EOF), une erreur ou la fin de l'annulation. L'analyse seule
+ne la libère pas. Si le client se déconnecte, elle reste détenue jusqu'à la fin de l'annulation
+côté producteur. Elle est libérée exactement une fois, même si erreur, abandon et annulation
+se produisent en concurrence.
Les points de terminaison POST concernés sont `/v1/responses`, `/v1/responses/compact`,
`/v1/chat/completions`, `/v1/messages`, `/v1/messages/count_tokens`,
@@ -26,33 +79,51 @@ existantes par corps. Les traductions directes internes et les appels combo ne r
une deuxième marge HTTP. Les limites de gestion, d'audio, d'historique de contexte et de
WebSocket restent inchangées.
-Un réglage absent ou égal à zéro, ou une limite résolue inférieure ou égale à 256 MiB,
-n'active pas ce contrôle supplémentaire de simultanéité. Les limites existantes sur le nombre
-de requêtes et les autres ressources continuent de s'appliquer.
+Ce contrôle supplémentaire ne s'applique que si la valeur effective de `maxInboundBodyBytes`
+dépasse 256 MiB. Omettre ce réglage par requête ou le définir à `0` conserve sa valeur par
+défaut de 256 MiB et laisse le contrôle inactif. Omettre ou mettre à zéro
+`maxConcurrentInboundBodyBytes` choisit au contraire le budget partagé par défaut de 512 MiB,
+sans désactiver un contrôle déjà actif. Les limites existantes sur le nombre de requêtes et
+les autres ressources continuent de s'appliquer.
## Refus temporaire ou corps trop volumineux
-Si aucune marge n'est temporairement disponible, le serveur renvoie HTTP **503**,
-`Retry-After: 1` et le code d'erreur `server_busy`, avant l'analyse du protocole ou l'envoi au
-fournisseur. Les clients Messages et de comptage de jetons reçoivent une erreur au format
-Anthropic de type `overloaded_error` ; les clients compatibles OpenAI reçoivent
-`server_error`. Réessayez après la fin de la requête en cours, en respectant l'en-tête de
-nouvelle tentative et la temporisation du client. Augmenter encore la limite du corps ne
-résout pas l'indisponibilité de la marge.
+Si la marge complète est temporairement indisponible, la requête peut rejoindre une **file
+FIFO de 32 requêtes en attente au maximum**, avec une **attente limitée à 120 secondes**.
+Pendant l'attente, l'application ne lit pas les corps des requêtes. Une réduction après mesure ou une libération
+finale permet d'admettre les requêtes dans l'ordre de la file. Une requête abandonnée pendant
+l'attente est retirée de la file et son corps non lu est annulé sans être consommé.
+
+Une file pleine ou une attente expirée entraîne HTTP **503**, `Retry-After: 1` et le code
+d'erreur `server_busy`, avant l'analyse du corps ou l'envoi au fournisseur. Les corps non lus
+sont annulés lors du refus. Les clients Messages et de comptage de jetons reçoivent une erreur
+au format Anthropic de type `overloaded_error` ; les clients compatibles OpenAI reçoivent
+`server_error`. Les contrôles d'authentification et d'origine précèdent l'admission et la mise
+en file.
+
+Respectez l'en-tête de nouvelle tentative et la temporisation du client. L'attente bornée et
+les réservations mesurées réduisent les refus de capacité évitables, sans éliminer toutes les
+réponses 503. Augmenter encore la limite par requête ne résout pas un épuisement durable de
+la capacité.
Un corps dépassant sa limite par requête suit toujours le traitement HTTP **413** existant.
Une taille déclarée excessive emprunte cette voie plutôt que de devenir un refus pour serveur
-occupé. Une déconnexion conserve le comportement d'annulation existant. Les contrôles
-d'authentification et d'origine précèdent ce contrôle de simultanéité.
+occupé.
## Ce que mesure le budget
-512 MiB est la somme des marges admises par requête, **pas une garantie que la mémoire du
-processus restera sous 512 MiB**. Le décodage, les chaînes, les graphes d'objets, les copies
-de requêtes et les autres états de l'application consomment davantage de mémoire. Les mesures
-existantes des octets UTF-8 et du JSON resérialisé sont conservées ; la longueur de l'entrée
-en octets ne remplace pas la taille potentiellement plus grande du JSON normalisé.
+Le budget configuré compte des unités d'admission : les marges complètes avant l'analyse,
+puis les réservations mesurées. Il **ne préalloue ni 2 GiB ni le volume configuré** et
+**ne borne ni la RSS du processus ni le tas des objets analysés**. Le décodage, les chaînes,
+les graphes d'objets, les copies de requêtes et les autres états de l'application consomment
+de la mémoire supplémentaire ; la consommation réelle peut donc dépasser le budget.
+
+Choisir 2 GiB au lieu du budget partagé par défaut de 512 MiB quadruple la réservation totale
+possible. Avec une limite de 512 MiB par requête, ce choix explicite admet jusqu'à quatre envois
+complets avant l'analyse au lieu d'un, ce qui augmente le pic potentiel de consommation mémoire.
+Choisissez le budget selon la marge mémoire de votre déploiement. Une limite par requête
+inférieure ou égale à 256 MiB laisse ce contrôle inactif, quel que soit le budget partagé.
-Pour des petites requêtes parallèles, conservez si possible la limite par défaut. Augmentez-la
-volontairement pour un parcours avec un historique volumineux, en acceptant la restriction de
-simultanéité supplémentaire.
+Conservez si possible la limite par requête par défaut. Augmentez-la volontairement si un
+parcours avec un historique volumineux l'exige, en tenant compte du pic mémoire potentiellement
+plus élevé et de l'attente bornée.
diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md
index ed39e7005e5..910fb0e181e 100644
--- a/docs-site/src/content/docs/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/getting-started/quickstart.md
@@ -1,9 +1,11 @@
---
title: Quickstart
-description: Configure your first provider and route OpenAI Codex through opencodex in three commands.
+description: Install opencodex, add your first provider, and connect the coding tool you already use, whether that is Codex, Claude Code, Claude Desktop, Grok Build or another supported tool.
---
-This guide takes you from a fresh install to running Codex against a non-OpenAI model.
+This guide takes you from a fresh install to the first request from the coding tool you already
+use. Steps 1 and 2 are the same for every tool; step 3 has one short branch per tool. The
+[client support page](/reference/client-support/) lists every tool and how it connects.
## Standalone binary (no npm)
@@ -103,9 +105,17 @@ ocx status
ocx gui # open the dashboard on the live port
```
-## 3. Use Codex
+## 3. Connect your tool
-Codex now talks to opencodex transparently:
+Pick the branch for the tool you use. You can connect more than one; they share the same
+providers and models.
+
+### Codex
+
+If you accepted the Codex prompt in step 1, Codex now talks to opencodex transparently. If you
+declined it, or setup exited because a config already existed, turn on the Codex switch on the
+dashboard's **Connect** page before running Codex. [Codex Integration](/guides/codex-integration/)
+explains what it changes and how to restore.
```bash
codex "Refactor this function for readability"
@@ -118,6 +128,53 @@ codex -m "anthropic/claude-opus-5" "Explain this stack trace"
codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
```
+### Claude Code
+
+With the `claude` CLI installed:
+
+```bash
+ocx claude
+```
+
+`ocx claude` starts the proxy if it is not running and launches Claude Code with its environment
+pointed at opencodex. Extra arguments pass through. See [Claude Code](/guides/claude-code/) for
+tier defaults, other launchers and how to opt out.
+
+### Claude Desktop
+
+```bash
+ocx claude desktop
+```
+
+On a new install this sets up the gateway profile for the whole app. An install that already uses
+the first-party mode keeps it; run `ocx claude desktop apply --gateway` to switch explicitly. The
+first-party mode, which keeps Desktop on claude.ai and routes only its Code tab, is described under
+[Claude Desktop modes](/guides/claude-code/#claude-desktop-modes-gateway-default-and-first-party).
+
+Fully quit and reopen Claude Desktop after switching modes.
+
+### Grok Build
+
+On a local listener, `ocx start` registers the visible models in `~/.grok/config.toml` when
+`~/.grok` exists. Pick an `ocx-*` model in Grok Build. Remote setups and the managed block are
+covered in [Grok Build](/guides/grok-build/).
+
+After you add providers or models, run `ocx ensure` or restart the proxy so Grok Build sees them.
+
+### Other tools
+
+Tools with a managed integration (OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code and more) are
+connected from the dashboard's **Connect** page or with:
+
+```bash
+ocx integration client enable --client
+```
+
+Each tool's config file and when the change takes effect are listed in
+[Integrations](/guides/integrations/). Any other tool that accepts a custom endpoint can use the
+OpenAI-compatible base URL `http://localhost:10100/v1` or the Anthropic base URL
+`http://localhost:10100`.
+
## Choose sub-agent models (optional)
A fresh config features five native models in Codex's sub-agent picker: `gpt-5.5`,
@@ -154,6 +211,12 @@ It stops before publishing a new runtime or changing those settings. Retry start
local connectivity recovers; an installed background service retries through its service manager.
A timeout does not permanently turn the new process into a secondary instance.
+`Proxy ownership is unverified; startup refused` means the local owner check was
+inconclusive. OpenCodex preserves the PID record, recovery journal and shared client
+settings. Retry after the owner responds or exits; a failed health probe alone does
+not authorize crash recovery. A dead recorded process with no listener still recovers
+automatically.
+
IPv6 does not need to be enabled on loopback. Startup checks whether each loopback
address can be bound locally and skips an unavailable family. Timeouts on an available
family still stop startup until ownership can be verified.
diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md
index 4a815b3c6d1..68d8b86e879 100644
--- a/docs-site/src/content/docs/guides/claude-code.md
+++ b/docs-site/src/content/docs/guides/claude-code.md
@@ -109,7 +109,7 @@ Operational contract when enabled:
not postpone usage probes or clear a failed usage probe's unavailable status. Measurements
whose known reset time has passed are discarded as unknown, including retained model-specific
windows. Values without a known reset are preserved; missing data is never reported as zero usage.
-- Affinity is **process-local** (lost on proxy restart).
+- Affinity survives proxy restart within the same config home and provider instance (24-hour idle lifetime, newest 2,000 bindings). The private snapshot stores session digests and login fences, never raw session keys or tokens. Legacy accounts without a persistent login identity remain process-local until their next login. Token refresh preserves affinity; replacement login, account removal, pause/reauthentication and explicit runtime account selection invalidate it. A transient throttle detour preserves the original binding even when its pause expires before the retry commits.
- A complete, structured **403** account-entitlement or billing refusal can rotate before
output. Recognized cases include no Claude Code access, an expired/inactive subscription,
and an insufficient Anthropic credit balance. The refused account loses its affinities and
@@ -405,6 +405,8 @@ applies to it as well. Desktop and CLI catalog rewrites share bounded row and me
if adding routed models would exceed a limit, OpenCodex returns the original Anthropic catalog
unchanged rather than publishing a partial list.
+Before the first upstream response, a picker upload that makes no progress for 60 seconds is closed. An expired upload fails without a success response; HTTP/2 resets only the affected stream. This idle timer pauses when the upstream applies backpressure and is removed when the upload completes or the upstream first responds. This idle guard adds no overall upload-duration limit; existing runtime and protocol limits remain in effect. Response streams are excluded from this timer.
+
### Use opencodex models from the Desktop Code tab (first-party bindings)
In first-party mode the Code tab's model picker belongs to claude.ai: its rows (Opus 5.5,
@@ -678,6 +680,10 @@ Models with a long context window (1M, or at least the 829,800-token default com
Claude models on an Anthropic route need a genuine 1M) get an extra `…[1m]` picker row: selecting it makes
Claude Code account a full 1M context for that model (auto-compaction stays on) — the proxy strips
the marker before routing.
+Fast-eligible native OpenAI models also get a `…--fast[1m]` row labelled `· Fast · 1M`
+when they meet the same long-context rule. This combines the native Fast tier with Claude
+Code's long-context accounting; it does not raise the upstream input limit. The row keeps
+the native model's input ceiling and the existing auto-compaction policy.
Selecting one persists it to Claude Code's `settings.json` `model` field; inbound requests resolve
the alias back to the routed model. On older Claude Code versions the picker stays native — set
slots via
@@ -966,7 +972,7 @@ The proxy translates every Anthropic Messages API request into the Codex Respons
| Assistant text | `output_text` |
| Assistant `tool_use` | `function_call` (`input` → JSON-stringified `arguments`) |
| User `tool_result` | `function_call_output` (`is_error` → `[tool error]` prefix) |
-| `tool_reference` in a tool result | Text `Tool loaded: ` in the paired result; preserves loaded-tool names without adding declarations or enabling translated server-side deferral |
+| `tool_reference` in a tool result | Text `Tool loaded: ` in the paired result by default. The [append-only opt-in](#preserve-cache-prefixes-when-client-tools-load) may place a currently declared deferred function at its successful client discovery result on an eligible final Responses route; a reference alone never grants a tool or enables server-side search |
| `thinking` / `redacted_thinking` replay | `reasoning` items with bounded `ocxr1` envelopes for signatures and redacted payloads |
| Function tools | `{type: "function"}` (`web_search*` → `{type: "web_search"}`). On a route to `api.anthropic.com`, the typed `web_search_*` and `web_fetch_*` declarations are also sent to Anthropic as declared and run server-side, unless the web-search sidecar supplies `web_search` itself. A search or fetch with a matching call and complete input comes back to the client as its `server_tool_use` block and its `web_search_tool_result` or `web_fetch_tool_result`, verbatim. Orphan results are dropped without synthesizing a call, while the surrounding text is preserved. When the client sends a complete pair back in its history, the next `api.anthropic.com` request carries it in place only if the following block uniquely identifies its original turn. Repeated anchors, unmatched or duplicate call/result ids, malformed call input, or a result appearing before its call drop the exchange. Call input must be an object with a nonempty search query or fetch URL string; other destinations never see the exchange |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, named function→`{type:"function",name}`, hosted WebSearch/web_search→`{type:"web_search"}` |
@@ -1096,6 +1102,13 @@ fields; `null` resets context/blocklist/compact-window values.
## Troubleshooting
+### Windows Desktop apply reports `client_lifecycle_lock_failed`
+
+Update OpenCodex if Desktop apply stalls while many old lock files are present. Lifecycle acquisition
+now verifies an already-private runtime directory before repeating its permission changes. It still
+refuses apply when required permission hardening fails. Keep the existing runtime lock files in place;
+removing them while clients are running can break coordination between processes.
+
**Claude Code says "Did 0 searches"** — Current builds translate completed Responses
`web_search_call` items into paired Anthropic `server_tool_use` and `web_search_tool_result` blocks,
including `usage.server_tool_use.web_search_requests`. Update opencodex if an older build completed
@@ -1134,15 +1147,22 @@ configured, every tool schema is inlined before you type anything. Re-enable def
{ "claudeCode": { "toolSearch": true } }
```
-This is off by default because it only pays off on **native Anthropic passthrough** routes.
-Deferral is a server-side optimisation, not a smaller request: Claude Code still sends every
-tool definition, and Anthropic's API is what keeps deferred schemas out of the model's context
-and answers the `tool_search` server tool with `tool_reference` blocks. A translated (routed)
-model never reaches that machinery — opencodex drops the `tool_search` tool and ignores
-`defer_loading` — so the provider still receives every schema while Claude Code stops counting
-them and therefore stops compacting. With `claudeCode.compatibility: "enforce"` the same
-request is rejected with 400 instead. Turn it on when `ocx claude` is routed to an Anthropic
-model; leave it off for third-party routes until translated deferral is supported.
+This remains off by default. **Native Anthropic passthrough** preserves Anthropic's server-side
+tool search: the client still sends deferred definitions, and Anthropic keeps them out of the
+model context until discovery. The default translated path does not implement that server tool;
+it drops the typed search declaration and ordinarily sends function schemas eagerly, which can
+make the client's context accounting undercount the provider's input.
+
+There is a separate, narrow exception for **client-executed `ToolSearch`** on a verified Responses
+destination selected by [`claudeCode.appendOnlyToolLoadingModels`](#preserve-cache-prefixes-when-client-tools-load).
+For that final physical route, the opt-in keeps unused deferred definitions out of the initial
+catalog and adds current definitions at successful structured discovery results. Client-side
+search must already work and an eager `ToolSearch` function must be declared. The launcher
+`toolSearch` setting can enable client discovery where the client supports it, but does not by
+itself activate the opt-in or provide server-side search. Verify this client-and-gateway combination
+before enabling it; leave it off on other third-party routes. Compatibility-enforcement admission
+is unchanged and can still reject unsupported deferral requests. Neither setting guarantees
+cache hits or full native-tool-search parity.
Accepted values follow Claude Code's own parser: `true`, `"auto"`, `"auto:N"` (N at least 100)
and `"force"`. A value you export yourself always wins over the injected one.
@@ -1167,6 +1187,77 @@ reuse still requires stable session identity and upstream cache availability.
Changes to earlier instructions or tools, and conversation compaction, can still
affect cache hits; preserving reminder order alone does not guarantee reuse.
+### Preserve cache prefixes when client tools load
+
+For a **direct Responses route verified to accept `additional_tools`**, an opt-in can preserve
+client ToolSearch loading history instead of adding each discovered function to the initial
+catalog:
+
+```json
+{
+ "claudeCode": {
+ "appendOnlyToolLoadingModels": ["my-gateway/gpt-example"]
+ }
+}
+```
+
+Selectors match resolved `provider/model` names, not picker aliases. A single trailing wildcard
+such as `my-gateway/gpt-*` selects a verified model family; a global `*` is not accepted. The
+list defaults to off, with a maximum of 128 entries of 512 characters each. Only a request whose
+final physical route is a direct Responses provider gets the outbound rewrite. The destination
+is checked again when the adapter is bound after a late subagent fallback or replacement; selecting
+the original route does not authorize a different fallback. The request's original catalog remains
+available to other attempts. A combo or policy request keeps the original catalog even when
+selected, because its fallbacks may not keep the chronological placement.
+
+OpenCode Go's Responses endpoint and the official xAI Responses hosts keep the
+original complete tool catalog even when selected. These destinations flatten or reject
+`additional_tools`, so they cannot preserve this chronological loading prefix. Keep this opt-in
+off for any other gateway that promotes discoveries to top-level `tools`.
+
+The optimization needs a string `metadata.user_id` and an eager declared `ToolSearch` function.
+Unused deferred definitions stay out of the initial catalog before discovery as well as after it;
+a successful structured ToolSearch result adds their currently declared definitions at that position.
+It never grants tools from reference text or restores revoked definitions. Tools used before
+being discovered and explicitly forced undiscovered tools remain in the initial catalog. Schemas, strictness and call/result pairing are
+preserved. Missing metadata, absent/empty configuration, and native Anthropic passthrough keep
+existing behavior. Malformed configuration is rejected on write; load recovery disables only
+this optional setting rather than discarding providers.
+
+This is not server-side deferred-tool-search support and does not change compatibility-enforcement
+admission or turn on tool search in the client. It applies when the client already discovers tools.
+It does not guarantee cache hits: schema changes, revocations, model switches, compaction and
+upstream cache availability can still require a new prefix.
+
+#### Claude models behind a gateway that also serves GPT
+
+`appendOnlyToolLoadingModels` cannot fix Claude models that are routed through a Responses
+gateway. Such a gateway can lift `additional_tools` back into Claude's initial `tools` array, and
+Claude caches tools before the system prompt, so the request that loads a tool still misses. For a
+key-authenticated gateway that also serves Anthropic Messages, send Claude models natively and keep
+the other models on Responses with `modelAdapters`:
+
+```json
+{
+ "protocols": { "rollout": { "managedMessagesNative": true } },
+ "providers": {
+ "my-gateway": {
+ "adapter": "anthropic",
+ "modelAdapters": { "gpt-example": "openai-responses" }
+ }
+ }
+}
+```
+
+List every non-Claude model the provider serves in `modelAdapters`; an unlisted one would be sent
+as Messages. Native Messages keep Claude Code's deferred tool definitions and `tool_reference`
+results in place, so loading a tool adds to the cached prefix rather than replacing it.
+`claudeCode.stabilizePromptCache` does not apply on that path and is not needed there; it still
+helps any Claude request that stays on the bridge. Combos, policies, pinned efforts, synthetic
+rows, the web-search sidecar and vision preprocessing keep requests on the bridge, as listed under
+[rollout switches](/guides/protocol-paths/#rollout-switches). Third-party compatible providers do
+not receive caller `anthropic-beta` values, so the gateway must accept these requests without them.
+
### Intercept token recovery
The running authenticated proxy validates the current token for each new CONNECT request.
diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md
index eec26ef3049..4eb3e800e34 100644
--- a/docs-site/src/content/docs/guides/codex-integration.md
+++ b/docs-site/src/content/docs/guides/codex-integration.md
@@ -427,6 +427,14 @@ $CODEX_HOME/models_cache.json
On macOS, opencodex checks the Codex app’s bundled CLI in `/Applications/ChatGPT.app` and then `~/Applications/ChatGPT.app` after explicit selections, shims, PATH, established installs, and the runnable bare `codex` fallback, requires the bundle’s XML `Info.plist` identity to be `com.openai.codex`, and never automatically replaces a runnable persisted selection with an app candidate.
+Execution checks also recognize `Codex.app` and `ChatGPT.app` at any location, including relocated
+bundles, explicit `CODEX_CLI_PATH` or configured runtime commands, and symlink aliases. A recognized
+name triggers verification; it does not grant trust. Before each execution, the canonical bundle
+and exact binary must pass strict signature verification with OpenAI's Team ID and the ownership,
+permission and ancestor-directory policy. The binary must resolve inside the bundle's `Contents/`
+(including `Codex.app/Contents/Resources/codex-aarch64-apple-darwin`); unresolved bundles or binaries
+are refused. Arbitrary non-app CLIs and third-party app bundles keep their existing execution policy.
+
On WSL, if `CODEX_HOME` is unset and the Linux `~/.codex` directory is absent or holds no Codex state
(`config.toml`, `auth.json`, `sessions`, `history.jsonl`), opencodex also
checks for a single Windows Codex Desktop home at `/mnt/c/Users/*/.codex/config.toml`. When exactly
@@ -1032,6 +1040,12 @@ its two modes:
It never substitutes a Pool or provider credential for the assertion, and it never forwards or
logs the assertion. Use `dual_header`, or turn the feature off, when Codex runs through opencodex.
+### Launch with `ocx codex`
+
+The key is supplied only when the configuration and its namespaces are protected from other principals; the current user and privileged OS authorities are trusted. Same-user processes are trusted: an actor with your write access can already read your OpenCodex key file. Same-user configuration replacement between verification and native startup is outside this threat model; the launcher does not freeze configuration at handoff.
+
+On Windows, default ProgramData creation rights withhold the key even when `ProgramData\OpenAI\Codex` is absent. The system Codex configuration folder under ProgramData must exist and be writable only by Administrators/SYSTEM, with its ancestors protected against replacement by other users. Ask an administrator to review the directory policy. opencodex never changes ACLs itself and provides no provisioning command. Alternatively, supply your own key by exporting `OPENCODEX_API_AUTH_TOKEN`; `ocx codex` leaves it untouched. See [launcher requirements and diagnostics](/reference/cli/agents/#ocx-codex-codex-args). Other refusals also produce one short specific stderr line without key values, key-file contents or key-file paths.
+
### Codex autostart shim
Run `ocx codex-shim install` to install the optional launcher wrapper. It runs
diff --git a/docs-site/src/content/docs/guides/codex-prompt.md b/docs-site/src/content/docs/guides/codex-prompt.md
index 75610d3a24b..5ecd7f46cd4 100644
--- a/docs-site/src/content/docs/guides/codex-prompt.md
+++ b/docs-site/src/content/docs/guides/codex-prompt.md
@@ -36,6 +36,12 @@ unavailable, which is not the case.
Click a layer name to see the text it sends. The dialog reads it from
`codex debug prompt-input`, so it is the real thing rather than a description.
+The read has one fixed 15-second caller budget, including time spent
+reading metadata and waiting on a shared probe. Synchronous metadata reads finish
+before expiry can be checked; an exhausted budget then prevents a new probe.
+If it expires, the dialog reports that the prompt
+could not be read; locally available base instructions are still returned.
+
Sometimes there is nothing to show, and the dialog says which reason applies:
- **The file exists but is empty.** Your `~/.codex/AGENTS.md` is zero bytes, so
diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md
index d32a8dcf122..4651ef4799f 100644
--- a/docs-site/src/content/docs/guides/combos.md
+++ b/docs-site/src/content/docs/guides/combos.md
@@ -685,8 +685,16 @@ the combo returns the last real upstream failure without sending to that target.
When `cooldownMs` is unset, a hopped target uses an upstream fallback: 5 seconds for request-rate
429s with upstream code `1302` or `1305`, 10 minutes for a spent account usage window, and 60
seconds otherwise. A usage window is recognised by upstream code (`usage_limit_exceeded`,
-`usage_limit_reached`, `1308`) or by the prose `usage limit reached`, independent of HTTP status —
-the ChatGPT Codex backend reports a spent window as a 502 rather than the documented 429. Credential and
+`usage_limit_reached`, `1308`) or by quota exhaustion phrasing (such as `usage limit reached`,
+`token-plan ... quota has been exhausted`, or `weekly usage quota exceeded`)
+outside 400 and 422 responses, independent of HTTP status — the ChatGPT Codex backend reports a spent
+window as a 502 rather than the documented 429. A 400 or 422 response needs a structured code: a
+validation error that merely quotes quota wording (e.g. `quota exhausted`) does not hop. Codes
+inferred from that prose by the proxy's formatter do not count as upstream structured evidence.
+Devin's explicit weekly/monthly/daily or usage-window exhaustion becomes 429 and uses the
+10-minute target hold; per-minute/per-hour and MCP configuration `failed_precondition` trailers stay
+400 and stop. Raw Devin trailer text is redacted. A spent
+window cools only that target, so other accounts of the same provider stay eligible. Credential and
billing failures that already black out the whole provider (`invalid_api_key`, `insufficient_quota`,
`payment_required`, and the other provider-scoped codes) take the same 10-minute hold. When it is set, `cooldownMs`
applies whenever no usable upstream `Retry-After` or Codex reset signal exists, including those
@@ -1068,3 +1076,10 @@ enforced. Explicit provider Retry-After values may therefore be retried early on
when the operator enables this option. Preview/list operations never clear cooldowns.
The interval is tracked per combo in memory and resets on proxy restart or combo
selection-state reset. Existing cooldown-wait behavior is unchanged when disabled.
+
+A recovery pass belongs only to the request that reserved it; other requests still
+observe the saved cooldowns. That reservation follows the request through cooldown
+waits and JEV selection. A target that becomes usable again during a wait can still
+be retried if its cooldown has not changed. A newer cooldown cancels that target's
+reservation. Success clears only the unchanged cooldown of the target that answered;
+untried targets and newer failures keep their cooldowns.
diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md
index 73e9367061f..c675de15c33 100644
--- a/docs-site/src/content/docs/guides/desktop-app.md
+++ b/docs-site/src/content/docs/guides/desktop-app.md
@@ -116,18 +116,32 @@ is missing** means the login item names an app that no longer exists, so delete
means the registration could not be read. While the app runs the proxy, the Startup page keeps the
background service installer disabled; to use it instead, quit OpenCodex and run
`ocx service install` in a terminal. The Codex launcher shim can still be installed there.
-When a Codex launch finds no proxy, the shim waits for OpenCodex to restart it, opens
-OpenCodex on macOS if it quit and its recorded ownership matches a valid **Start at Login**
-registration, and otherwise reports an error instead of starting a second proxy. Automatic
+When a Codex launch finds no proxy, the bundled CLI waits for independently verified Desktop
+recovery, including when Desktop has no recorded ownership claim. A matching Desktop that has
+stopped its proxy, or whose recovery cannot be verified, produces an error — except that on a
+platform without live-image verification a recorded ownership claim with the app still running
+keeps the existing bounded wait. A process name or
+locator file alone does not block ordinary CLI startup. The shim opens OpenCodex on macOS if it
+quit and its recorded ownership matches a valid **Start at Login** registration. Automatic
reopening selects that registration's app bundle path and checks that its declared main
executable matches the registered Desktop executable. If registration is missing, stale or
inconsistent, open OpenCodex manually; another copy with the same bundle identifier is not
selected as a fallback. If the shim reads "activation not confirmed", add
`. ~/.opencodex/codex-shell-env.sh` to your shell startup file after other PATH setup.
+Recovery verification covers the matching bundled CLI on macOS/Linux and the protected system
+Debian installation. An old npm-created shim can pin a separate Bun/CLI installation and bypass
+Desktop's terminal-command handoff. That entrypoint cannot verify a macOS or Windows Desktop,
+including an official signed copy, or an arbitrary per-user Linux copy. Recreate the shim from
+Desktop's Startup page or invoke its bundled CLI. Windows cannot yet establish the live image
+identity required for this recovery proof; an identified running sibling earns no recovery credit
+and, without a recorded Desktop claim, reports an error instead of starting a CLI proxy — even
+when its on-disk signatures are valid. With a recorded claim and the app running, the CLI keeps
+its existing bounded wait for Desktop's proxy.
+
The bundled CLI is `/Applications/OpenCodex.app/Contents/MacOS/ocx` on macOS and
`/usr/bin/ocx` for the Linux deb package. Run that executable with `status` to check
-the app's proxy. Windows also verifies live supervision, but **Start at Login** remains
+the app's proxy. Windows verifies live supervision when the proxy child is already running, but **Start at Login** remains
unverified there and Desktop receives no startup protection credit.
While the CLI detects Desktop supervising the proxy, `ocx service install`, `repair`,
diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md
index fdb813c12b6..b478bae8d9b 100644
--- a/docs-site/src/content/docs/guides/integrations.md
+++ b/docs-site/src/content/docs/guides/integrations.md
@@ -1,6 +1,6 @@
---
title: Integrations
-description: Connect opencodex to OpenCode, Pi, Gentle Shell, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Cline CLI, Kilo, Command Code and Factory Droid from the dashboard — one switch per client, with a backup taken before every write.
+description: Connect opencodex to OpenCode, Pi, Gentle Shell, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Aside, Raycast, omo, Qoder, Cline CLI, Kilo, Command Code, Factory Droid and T3 Code from the dashboard — one switch per client, with a backup taken before every write.
---
The **Connect** page writes opencodex's provider block into a client's own config
@@ -31,6 +31,7 @@ file, and removes it again. Each client below has its own switch:
| Kilo | first existing `kilo.jsonc`, `kilo.json`, `opencode.jsonc`, `opencode.json`, or `config.json` under `~/.config/kilo` | JSONC | new sessions | `OPENCODEX_KILO_API_KEY` |
| Command Code | `~/.commandcode/providers.json` (on Windows with the default home: use `HOME ?? USERPROFILE` only if nonblank and Windows-absolute, otherwise `homedir()`; a separately supplied home is preserved; append `.commandcode\providers.json`) | JSON | next launch of Command Code | none — keyless loopback (`apiKey: false`) |
| Factory Droid | `~/.factory/settings.json` (`%USERPROFILE%\.factory\settings.json` on Windows) | JSON | immediately via file watching | none — keyless loopback |
+| T3 Code | `$T3CODE_HOME/userdata/settings.json` (default `~/.t3/userdata/settings.json`) | JSON | immediately — T3 Code watches the file | non-secret loopback marker |
## Gentle Shell
@@ -128,6 +129,48 @@ loopback listener is enabled, exports point Qoder at that listener. `ocx sync` a
changes refresh an already enabled Qoder integration like the other managed clients.
Model IDs containing `,` or `]` (for example `kimi/k3[1m]`) are exported like any other model.
+## T3 Code
+
+[T3 Code](https://github.com/pingdotgg/t3code) drives the Codex and Claude Code CLIs already
+installed on the machine. Its Codex threads route through opencodex once the
+[Codex integration](/guides/codex-integration/) is enabled, because T3 Code lists Codex models
+through `codex app-server`, which reads `$CODEX_HOME/config.toml` like every Codex client. Routed
+models then appear under Codex with no T3 Code switch, as long as T3 Code runs Codex with the same
+Codex home opencodex manages (`CODEX_HOME`, default `~/.codex`). A T3 Code Codex instance pointed at
+another home does not see the routed catalog.
+
+The **T3 Code** switch adds the Claude side. It needs the Messages API, which every Claude turn
+uses: while Messages is off on the API page, enable and refresh are refused, and disable still
+works.
+
+```bash
+ocx export --client t3code --json
+ocx integration client enable --client t3code
+ocx integration client disable --client t3code
+```
+
+Enable writes one provider instance, `providerInstances.opencodex`, into T3 Code's server settings
+and leaves every other instance and setting alone. It is a Claude instance named **opencodex** that
+runs your own `claude` binary with `ANTHROPIC_BASE_URL` set to the proxy, the non-secret
+`opencodex-proxy` marker as `ANTHROPIC_AUTH_TOKEN`, an explicitly empty `ANTHROPIC_API_KEY`, and
+`CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1` so a `settings.json` env block in your Claude config cannot
+re-point it. Each routed model is a custom model keyed by its opencodex selector
+(`provider/model`), which T3 Code sends verbatim. It appears in T3 Code's model picker under
+**opencodex**. T3 Code reloads the file on its own, so no restart is needed; catalog changes and
+`ocx sync` refresh the instance.
+
+T3 Code keeps its data in `T3CODE_HOME` when set, otherwise `~/.t3`. Set the same variable on the
+opencodex process if you relocate it; a relative value is refused. On Windows, use a fully qualified
+drive path (`C:\shared\t3`) or UNC path (`\\server\share\t3`); paths without a drive, such as
+`\shared\t3` or `/shared/t3`, are refused. `~` expands against the effective user home.
+The marker is not a credential, so the integration needs loopback access and refuses an authenticated non-loopback bind unless the
+unauthenticated loopback listener is enabled.
+
+Treat the instance as opencodex's. Marking one of its variables **Sensitive** in T3 Code, adding
+a custom model to it, or toggling it off rewrites the block. The switch then shows a conflict
+instead of overwriting your edit, and **Replace** restores the generated block. To use
+different settings, add your own Claude instance in T3 Code beside it.
+
Generated catalogs include only enabled models from each provider selection. This applies to both
downloads and managed integrations, including Pi and Aside. The management model list still shows
the full roster so you can enable additional models.
@@ -284,6 +327,12 @@ still reads as installed, while a config-path override moves only the file. If y
are still on the older `.clawdbot` layout, that is found too: the modern directory
wins when it exists, and the legacy one is used when it is the only one there.
+On Windows, overrides requiring absolute paths must include a drive letter
+(`C:\Users\me\client`) or a UNC share (`\\server\share\client`). Drive-less
+rooted forms such as `\Users\me\client` or `/client/config` are refused because
+they depend on the current drive. A selector documented to support `~` still expands
+it against the effective home; Gentle Shell does not expand `~`.
+
These must be **absolute paths** or start with `~`. A relative one is refused rather
than resolved, because it would mean whatever directory each process happened to
start in — and that path is stored with the backup, so it has to name the same file
@@ -381,6 +430,17 @@ own backups and rollback history, and the other profiles stay unchanged. Every p
`model.default` and other providers: OpenCodex adds only `providers.opencodex`. With no named profiles,
the page works as before and changes the default profile.
+Catalog changes and `ocx sync` automatically refresh only the default profile resolved from the
+running proxy's environment (including `HERMES_HOME`). Named profiles keep their previous catalog.
+After changing the model selection, open **Connect → Hermes**, select each named profile showing
+**Update needed**, and review and confirm **Apply**. The CLI has no named-profile selector.
+With an unchanged catalog, refresh stays **Current** after edits to comments or settings outside
+`providers.opencodex`; it changes neither the file nor the recorded baseline. When a catalog update
+is pending, automatic refresh refuses any Hermes file changed since the last Apply, including those
+unrelated edits. When the catalog shows **Update needed**, review and Apply
+the pending catalog change to resume automatic updates. Apply with an unchanged catalog is a no-op
+and does not accept a new file baseline.
+
The management API selects a profile with `profile=` on `/api/client-integrations/hermes` and with
`client=hermes&profile=` on the preview, journal, and restore endpoints;
`GET /api/client-integrations/hermes/profiles` lists the profiles. A directory under `profiles/` counts
@@ -479,6 +539,38 @@ ocx integration client history --client hermes
ocx integration client restore --op [--confirm-drift]
```
+### Opt-in alias model names
+
+Hermes and Command Code accept `--model-names alias|routed`. The default is
+`routed`, independent of `preferModelAliasIds`. For example:
+
+```bash
+ocx export --client hermes --model-names alias --json
+ocx integration client preview --client hermes --operation apply --model-names alias --json
+ocx integration client enable --client hermes --model-names alias
+ocx integration client enable --client commandcode --model-names alias
+```
+
+Export is a preview: it uses an already loaded, current catalog and writes no
+client configuration or integration state. If that catalog is unavailable, load
+the model catalog separately and retry. `--out` explicitly writes an export
+artifact; it does not connect the client.
+
+The connection stores the mode. Omitting the flag on later Apply (including the
+dashboard) preserves it; explicit `--model-names routed` resets it. A qualified
+alias such as `AC/AC-model-x` replaces `acme/vendor-model-x` only when the router
+proves the same provider and model. Missing or unsafe aliases and collisions
+fall back to the routed key. Combos retain their existing keys.
+
+After an alias rename or removal, run `ocx sync` or explicitly Apply the connection:
+the next successful managed refresh replaces the old key with the newly validated
+alias or routed fallback. Alias edits do not atomically update client files.
+Refresh remains idempotent and refuses foreign edits. If an alias changes while
+Apply or refresh waits, the write is refused; retry against the current catalog.
+Each named Hermes profile
+stores its own mode; automatic catalog refresh covers only the default profile,
+so named profiles need explicit Apply. Other exporters retain routed names.
+
### Preview and bind a terminal write
The existing direct commands remain supported. To bind a write to a reviewed
@@ -575,10 +667,12 @@ ocx mcode
```
Once connected, `ocx sync` and `POST /api/sync` refresh owned MCode, Pi, Aside,
-Raycast, omo, OpenCode, and Kilo catalogs with the current model selection, context windows, and
-reasoning-effort ladders. Proxy startup refreshes an owned Raycast catalog. Changes to
+Raycast, omo, OpenCode, Kilo, and Hermes catalogs with the current model selection, context windows, and
+reasoning-effort ladders. Background catalog changes also refresh the connected Hermes default profile unattended;
+an edited Hermes file with a pending catalog update is refused and reported; a never-connected or disabled integration stays untouched.
+Named profiles require [explicit Apply](#hermes-profiles). Proxy startup refreshes an owned Raycast catalog. Changes to
model visibility, provider selection, or presets also refresh connected Pi, Aside,
-Raycast, omo, OpenCode, and Kilo catalogs.
+Raycast, omo, OpenCode, Kilo, and Hermes catalogs.
Missing, foreign-edited, or unsafe blocks stay untouched, as do previously owned blocks
you removed manually.
An enabled Aside profile is an exception to the usual owned-only refresh: if its account
@@ -899,6 +993,8 @@ ocx agent roles suggest
ocx agent roles suggest --model xai/grok-4.5 --apply
```
+Under the table, **Change history** lists the newest changes a save, an auto-assign apply or `ocx agent roles set` wrote, newest first, in whole batches and never more than 50, so an Apply all is listed with every role it wrote or not at all, with each role's model and effort before and after. One Apply all is one entry. **Restore** writes the earlier values back through the same save, so a restore is listed too. A save never removes a model pin, so a role that had none keeps its current model and gets only its effort back. The history lives in `integrations/codex-agent-roles.jsonl` under the opencodex config directory, which retains at most 256 KiB and 1000 rows by dropping its oldest whole batches. Compaction targets 128 KiB and preserves a Restore together with its complete source batch when they fit both limits, making room by dropping unrelated older batches; history can still expire as later writes arrive. The writer captures the bounded history before appending so a Restore does not evict its own source at the retention boundary; the omo card's Restore center above tracks only `~/.omo/agent/models.json`. Each change records the Codex home it was made in, and only the current home's changes are listed or restored, so after pointing `CODEX_HOME` elsewhere the list starts empty and a Restore from another home's history is refused. If the history file cannot be read, the section says so in place of the list, and the role table keeps working.
+
## Kilo
Kilo CLI, VS Code, and JetBrains share one global config. This integration writes
@@ -912,6 +1008,9 @@ remove `provider.opencodex` from those files before enabling the integration. An
candidate also blocks the write. Disable can still remove a block owned in the recorded file
while another candidate conflicts or cannot be parsed; the other candidate is left untouched.
+OpenCodex reads these candidate files as JSONC. A `//` line comment ends at a CR, LF, or CRLF
+line ending, so a file saved with any of these line endings keeps the settings that follow a comment.
+
The owned fragment is only `provider.opencodex` (OpenCode V1 shape: `npm`, `options`,
`models`). Kilo's published schema has no OpenCode V2 `providers` key, so that block is
not emitted; Kilo instead receives the per-model reasoning choices as a variant map inside
@@ -927,6 +1026,9 @@ sync`, `POST /api/sync`, and visibility, provider, or preset changes refresh an
block under the same safe owned-only rules as every other client, and a new Kilo session
loads the updated file.
+When reading these candidate files, OpenCodex accepts a trailing comma after an array element or
+object property. Write an empty array or object as `[]` or `{}`; `[,]` and `{,}` are refused as malformed.
+
```bash
ocx integration client enable --client kilo
ocx export --client kilo --out ./kilo.jsonc
diff --git a/docs-site/src/content/docs/guides/model-ordering.md b/docs-site/src/content/docs/guides/model-ordering.md
index b3ca4772592..35e2b5a6dc0 100644
--- a/docs-site/src/content/docs/guides/model-ordering.md
+++ b/docs-site/src/content/docs/guides/model-ordering.md
@@ -30,8 +30,18 @@ Without complete-picker ordering, the relevant no-selector priorities are:
| `subagentModels[i]` | `i` (`0` through `4`) | The featured rank map in `src/codex/catalog/sync.ts` |
| Other routed models | `5` | Routed entry creation in `src/codex/catalog/sync.ts` |
| Non-featured routed models listed in `modelPickerOrder` | `1000 + i` | Display-only picker rank in `src/codex/catalog/sync.ts` |
-| Native GPT slugs by default | `9` | Native entry creation in `src/codex/catalog/sync.ts` |
-| Unselected native models while a featured list exists | At least `featured.length + 100` | Native catalog merge in `src/codex/catalog/sync.ts` |
+| Native GPT slugs without a featured list | `sourcePriority` (fallback `9`) | Shared native source-priority resolver |
+| Unselected native models while a featured list exists | `featured.length + 100 + sourcePriority` | Shared native source-priority resolver |
+
+Native source priority comes from the current discovered descriptor, then the pristine catalog
+backup, then the same model's retained source value, then the upstream snapshot. Each candidate
+must be an integer from `0` through `1000`; invalid values are skipped and the final fallback is
+`9`. Published display priorities are never reused as source priorities. With four featured
+models, native source priorities `3`, `8` and missing become `107`, `112` and `113` respectively.
+This preserves native source order behind featured and ordinary routed rows, including after
+repeated sync or featuring and later unfeaturing a native. The live Codex model list, published
+catalog and models cache use the same priorities for the same inputs. Account-selector groups
+retain their separate stride policy; complete-picker ordering remains the final override.
The management API limits `subagentModels` to five entries with `slice(0, 5)` in
`src/server/management/agent-settings-routes.ts`. This matches the Codex `spawn_agent` surface, which
@@ -73,7 +83,7 @@ With no eligible account selectors and a non-empty featured list, the resulting
3. Unselected native models, pushed below the featured block during catalog merge.
Without `subagentModels`, routed models remain at priority `5`, native GPT entries use their normal
-priority (normally `9` for entries built by opencodex), and the routed group remains provider/id
+source priority (fallback `9`), and the routed group remains provider/id
alphabetical.
## Example
@@ -101,7 +111,7 @@ The picker begins as follows:
| 5 | `gpt-5.6-terra` | `4` | Fifth selection |
| 6 | `anthropic/claude-fable-5` | `5` | First remaining routed id in provider/id alphabetical order |
| 7 onward | Remaining routed models | `5` | Provider alphabetically, then model id alphabetically |
-| After routed models | Remaining native models | `featured.length + 100` or higher | Unselected natives are moved below the featured block |
+| After routed models | Remaining native models | `featured.length + 100 + sourcePriority` | Source order is preserved within the native band |
The first five entries are the overrides advertised to `spawn_agent`; the rest continue in the
normal picker order. With account selectors, the five-entry limit applies after bare native choices
diff --git a/docs-site/src/content/docs/guides/opencode.md b/docs-site/src/content/docs/guides/opencode.md
index cc61a911c53..9b25081ac24 100644
--- a/docs-site/src/content/docs/guides/opencode.md
+++ b/docs-site/src/content/docs/guides/opencode.md
@@ -201,6 +201,43 @@ CLI export, dashboard export, managed refresh and the launcher share the effecti
The `opencodex` provider block is regenerated on every launch, so per-model tweaks made inside it
will not survive. Keep custom entries under a provider key of your own instead.
+## Slow local models (prefill timeouts)
+
+opencode waits a bounded time for the first response header from a provider (five minutes by
+default in current V2), and a separate, longer bound between streamed chunks. Behind the
+`opencodex` provider, a local model (LM Studio, vLLM,
+Ollama, or any self-hosted endpoint) can spend longer than that in prompt *prefill* on a large
+prompt — processing the prompt before it emits a single token. On a non-streaming turn the proxy
+sends opencode nothing until the upstream completes, so opencode's own header wait fires first and
+the turn fails with `Timed out waiting for response headers`, even though the model is still
+working.
+
+Because the generated `opencodex` provider multiplexes every routed upstream under one block and
+opencode's timeouts are provider-wide, raise or disable the wait for the whole provider with
+`opencodeClientExport` in your opencodex config. It is omitted by default, so opencode keeps its
+native waits (header and the longer chunk bound) unless you opt in:
+
+```json
+{
+ "opencodeClientExport": {
+ "headerTimeoutMs": false,
+ "chunkTimeoutMs": 1800000
+ }
+}
+```
+
+| Field | opencode setting | Meaning |
+| --- | --- | --- |
+| `headerTimeoutMs` | `settings.headerTimeout` | Wait for the proxy's first response header. A number of milliseconds, or `false` to disable. |
+| `chunkTimeoutMs` | `settings.chunkTimeout` | Wait between streamed response chunks. A number of milliseconds, or `false` to disable. |
+| `timeoutMs` | `settings.timeout` | Whole-request bound. A number of milliseconds, or `false`. opencode applies no default. |
+
+Each value is emitted verbatim into both the `provider.opencodex` (`options`) and
+`providers.opencodex` (`settings`) blocks. Disabling `headerTimeout` removes opencode's safety net
+against a genuinely hung proxy for every model routed through it, so prefer a large explicit value
+(for example `1800000` for thirty minutes) unless you specifically want no bound. A malformed value
+is dropped and opencode keeps its native default rather than failing to load.
+
## Requirements
opencode must be installed and on `PATH`:
diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md
index f2d5f813a86..fd3f1f6bf58 100644
--- a/docs-site/src/content/docs/guides/providers.md
+++ b/docs-site/src/content/docs/guides/providers.md
@@ -195,6 +195,11 @@ Two exceptions are worth knowing because you can hit them:
| OrcaRouter | `ocx login orcarouter-oauth` — consent mints a user-owned, long-lived `sk-orca-…` key, and the request then carries a key | `orcarouter` — the same key pasted by hand |
| Meta Muse | `ocx login meta-muse` can import a local Muse Code CLI key or start device login. Meta scopes that credential to its own CLI, so this is an unsupported use: how the calls settle is not observable from the API, and you should treat every call as billable against your account | `meta-model` is the supported path — every call is metered per token, and a Muse Code subscription does not work there |
+When the Grok subscription catalog publishes selectable context windows, opencodex uses the
+widest published rung: `[256000, 500000]` exposes 500k for Grok 4.5–4.7, including the 4.7 Fast
+lane. Your configured model window and enabled Context cap still lower that value. If your plan
+serves a shorter window, configure that limit; the catalog does not verify account entitlement.
+
Cursor, Kiro, Zed Hosted AI and Nous Portal are login-only and have no API-key equivalent. Google Antigravity is
login-only too: `ocx login google-antigravity` signs in with your Google account over the Cloud Code
Assist wire, and the `google` preset beside it is the AI Studio Gemini API — a different product
@@ -377,6 +382,13 @@ revokes it, run `ocx login zed` again.
For Zed-hosted OpenAI models, OpenCodex translates `developer` instructions to `system` messages and removes OpenAI-private fields and unsupported built-in tools such as `web_search`. Function and custom tools remain available through the delegate; a tool choice with no supported tools left becomes `none`.
+Current-turn tool declarations carried in Codex's `additional_tools` input are promoted into the
+supported tool catalog. Function calls and custom-tool input are relayed in order, including
+interleaved calls, so a tool-using turn can continue after its introductory text. Incomplete calls
+keep only received arguments and remain incomplete; cancellation releases buffered data. Conflicting
+completed custom input, including later deltas, fails the turn with incomplete calls. Function arguments accept JSON whitespace differences outside strings, decoded string escapes, and compatible partial snapshots while preserving delivered bytes. Number literals must match exactly (`1` and `1.0` conflict); object-key order and duplicate members must match too. These conservative conflicts fail the turn. JSON nesting is limited to 65,536 containers; deeper input fails with a clear depth-limit message. Late fragments are validated incrementally. On upstream failure, received text and
+reasoning remain visible and usage retains numeric counters only. The relay is covered by synthetic stream fixtures; live hosted-stream compatibility remains unverified.
+
For the canonical Kimi Coding Plan presets (`kimi` account login and `kimi-code` API key),
opencodex forwards only a caller-supplied stable `prompt_cache_key` to the Chat Completions request
(the Responses wire accepts the same field, but opencodex does not send it there today);
@@ -928,6 +940,7 @@ OpenCodex keeps it in the catalog from the preset. An install whose saved MiniMa
still the previous default receives it on the next start; a list you edited is left as it is;
register it by hand with
`ocx models add minimax MiniMax-M3.1-Flash --context-window 1000000`.
+The seed refresh applies only to official MiniMax endpoints; Custom URLs keep their model list.
**OpenCode Go** routes `gemini-4-argon-contributor` through its Gemini API, including
streaming and tool-result continuations. The existing `opencode-go` preset keeps its
@@ -987,7 +1000,7 @@ MiMo's malformed ``. The local pairing form appears only when pairing is missing and the dashboard and API use the same loopback origin. Copy the form's `ocx gui pair --origin "http://127.0.0.1:"` command, run it in a terminal on this computer, and paste the one-use code into the form. Use the exact origin shown in the form; a provider API key or admin token is not a pairing code. Missing pairing is separate from a configured-port mismatch.
@@ -169,7 +173,7 @@ The version a Child reports is its own claim, bound to its link key. OpenCodex u
The existing `ocx gui pair --origin ` command works while OpenCodex runs as a background service. Run it under the account that owns the service's private OpenCodex home, then enter the returned one-use code in the local dashboard. No server terminal, additional password, Windows Hello or hardware key is required. The CLI and server must use the same current version and configuration home; restart an older server after updating. Mixed-version Hub invitations also require updating the CLI and server together. Pairing refuses a symlinked configuration home or intent directory; use the actual protected directory as `OPENCODEX_HOME`.
-Internally, the CLI creates a short-lived authorization commitment and attempts to remove it after use or failure. Reading runtime information alone does not authorize pairing, and the verifier is not stored on disk. This is a private-home write-access boundary, not biometric or human-presence authentication: a program with full access to that user's files is still trusted. Existing Hub invitation authorization is unchanged. If a pairing request fails, run a fresh command after resolving the reported problem. This does not add Windows SSH Remote Link support.
+Internally, the CLI creates a short-lived authorization commitment and attempts to remove it after use or failure. Reading runtime information alone does not authorize pairing, and the verifier is not stored on disk. This is a private-home write-access boundary, not biometric or human-presence authentication: a program with full access to that user's files is still trusted. Existing Hub invitation authorization is unchanged. If a pairing request fails, run a fresh command after resolving the reported problem. SSH Remote Link on Windows uses the separate PowerShell execution path described above.
If a process is interrupted during pairing, rerun the command to obtain a new authorization. Do not manually remove pairing state while the service or a pairing command is running.
@@ -181,13 +185,13 @@ The check covers the intent folder and record. It does not protect against repla
On the Home, run `ocx link probe child-a --json` to inspect the offered SSH fingerprint, key type and OpenCodex version. Verify the fingerprint through a trusted source. Then run `ocx link add child-a --expect-fingerprint SHA256:… --json`. The command probes again and refuses a mismatch before storing trust or issuing a key. Without the fingerprint flag, it requires an interactive terminal and an explicit `yes` after showing the fingerprint. It never prints the link key.
-Home-initiated connections now restart the Child after saving its connection, including capable older Child installations. The Child must already serve standalone OpenCodex on its configured port, expose bound restart support, and have curl installed. Update or disconnect a Child that fails this preflight. Desktop and system OpenCodex installations take precedence over generic PATH shims; broken candidates are skipped. Windows Children remain unsupported.
+Home-initiated connections now restart the Child after saving its connection, including capable older Child installations. The Child must already serve standalone OpenCodex on its configured port and expose bound restart support. POSIX Children need curl; Windows uses a bounded loopback HTTP request through .NET. Update or disconnect a Child that fails this preflight. Desktop and system OpenCodex installations take precedence over generic PATH shims; broken candidates are skipped. Windows x64 Children use encoded PowerShell commands without consuming key stdin.
### Windows computers
-`ocx link add` and the dashboard refuse a Windows computer during the probe or host confirmation, before any link key is issued or tunnel started. The refusal covers Windows OpenSSH with Git Bash, MSYS2 or Cygwin as its shell (`uname -s` reports MINGW, MSYS or CYGWIN) and with cmd or PowerShell as its shell. Connect a Windows computer in Hub mode instead: run `ocx hub invite` on a Hub and run the printed `ocx connect` command on the Windows computer.
+A Windows x64 Child needs OpenSSH Server, Windows PowerShell and the standalone `ocx.exe`. Home recognizes Windows during the probe, records it with the link, and runs every Child command through PowerShell over the same pinned SSH connection, whether the SSH default shell is cmd or PowerShell. The link key still travels only on standard input. Closing the SSH connection ends the remote `ocx` command. If Windows PowerShell on the Child cannot load the helper that ties `ocx.exe` to the connection, for example under Constrained Language Mode, the command is refused and nothing runs. If Home cannot tell which platform the Child runs, it stops before issuing a link key.
-A Windows Child linked by an older release keeps its link. Run `ocx link cleanup` on the Home to find such links; add `--link-id ` to check one, or `--json` for a machine-readable report. It asks each Home-initiated Child for its platform over the link's pinned SSH trust and changes nothing. For each Windows Child it prints the recovery order: run `ocx disconnect` and `ocx restart` on the Windows computer, then `ocx link revoke --link-id --force --yes` on the Home, then connect it in Hub mode. A plain revoke fails on these Children because its remote disconnect runs a POSIX command that Windows refuses.
+Run `ocx link cleanup` on Home to inspect recorded Child platforms without changing them. Current Home releases disconnect Windows Children through PowerShell. If a remote disconnect fails, resolve the reported shell or installation error and retry; **Remove here only** remains an explicit recovery action that can leave the Child needing a local disconnect.
Restart waits for running turns and may take several minutes. Success requires the restarted Child to serve the client role and relay a fresh complete catalog to this Home. SSH setup has a bounded allowance derived from measured latency; relay verification starts after restart with its own 15-second window. Errors name the failing phase. If an error reports residual Child configuration, run `ocx disconnect` and `ocx restart` on that Child before retrying. Home cleanup alone does not restore Child configuration. A CLI timeout reports an uncertain outcome; inspect Home `ocx link status` and Child `ocx connect status` first.
diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md
index e25d49dbfe7..d977b2ec025 100644
--- a/docs-site/src/content/docs/guides/sidecars.md
+++ b/docs-site/src/content/docs/guides/sidecars.md
@@ -283,3 +283,9 @@ Claude Code OAuth fingerprint precedent, but should be soak-tested with the
intended account and workload.
See the [Configuration reference](/reference/configuration/server/#sidecars) for every field.
+
+Search responses with malformed optional fields retain valid answer text and citations. Invalid error-message values use a generic failure message.
+
+When a search answer ends with a Sources list, an unmatched list item stays in the answer instead of being removed as a citation. A title followed by its URL is still consumed as one citation. Non-HTTP URI-like text inside inline code, such as `mode:fast`, stays in the answer; code-formatted HTTP(S) URLs are still citations.
+
+Long search queries and answers are shortened without splitting a UTF-16 surrogate pair, so a single character such as 😀 is never cut in half by the result limits. A sequence of several characters, such as emoji joined with a zero-width joiner, can still be cut between its parts.
diff --git a/docs-site/src/content/docs/guides/sub-agent-surface.md b/docs-site/src/content/docs/guides/sub-agent-surface.md
index d0d52fee5ba..a7bd6c543eb 100644
--- a/docs-site/src/content/docs/guides/sub-agent-surface.md
+++ b/docs-site/src/content/docs/guides/sub-agent-surface.md
@@ -93,7 +93,7 @@ depend on the catalog:
| Catalog | Rows that fill the remaining slots |
| --- | --- |
-| No Codex account selectors | Other routed `provider/model` rows. They all share priority `5`, so they appear by provider name and then model id, alphabetically. Unfeatured native GPT rows sit at priority `100` plus the number of featured ids or higher, so they come later. |
+| No Codex account selectors | Other routed `provider/model` rows. They all share priority `5`, so they appear by provider name and then model id, alphabetically. Unfeatured native GPT rows use `featured.length + 100 + sourcePriority`, preserving native source order behind those routed rows. |
| Codex account selectors | Account-qualified native rows (`/`), which directly follow the featured rows. Unfeatured routed rows move to priority `1005` and come after them. |
For example, with no account selectors and this roster:
@@ -109,7 +109,12 @@ For example, with no account selectors and this roster:
| 3 | `deepseek/deepseek-v4-flash` | `5` | First remaining routed row alphabetically |
| 4 | `google/gemini-3-pro` | `5` | Next routed row |
| 5 | `moonshot/kimi-k3` | `5` | Next routed row |
-| 6+ | `zai/glm-5.2`, then native GPT rows | `5`, then `102` | Outside the five |
+| 6+ | `zai/glm-5.2`, then native GPT rows | `5`, then `102 + sourcePriority` | Outside the five |
+
+The native source rank uses current discovery before the pristine backup, exact-model retained
+source rank and upstream snapshot, with fallback `9`. Repeated sync and feature/unfeature preserve
+that unbanded rank. The live list, published catalog and cache agree for equal inputs. See
+[Model ordering](/guides/model-ordering/) for validation and picker-order overrides.
With an empty roster, opencodex pins nothing and the five follow the catalog's normal
priorities.
diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md
index 0792854aa24..27bc1a1aca1 100644
--- a/docs-site/src/content/docs/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/guides/web-dashboard.md
@@ -13,9 +13,11 @@ settings, and request traffic.
ocx gui
```
-This opens `http://localhost:` in your browser — or `http://127.0.0.1:` when hub
-management ingress is enabled — auto-starting the proxy first if needed. In
-development you can run the GUI dev server separately against a running proxy:
+This opens the running proxy's address in your browser: `http://127.0.0.1:` by default,
+or `http://[::1]:` for a literal IPv6 loopback bind. These literal addresses preserve the
+configured origin required for local browser pairing. With hub management ingress enabled, it opens
+`http://127.0.0.1:`. The proxy starts automatically first if needed. In development
+you can run the GUI dev server separately against a running proxy:
```bash
ocx start
@@ -35,6 +37,14 @@ password manager can offer to save and autofill it. The dashboard itself still k
in memory and does not write it to `localStorage` or `sessionStorage`; whether it is saved is entirely
the browser or password manager's decision.
+**Remember this device** is off by default. On a direct HTTPS dashboard (or local loopback), it exchanges the admin token for a server-issued HttpOnly, SameSite=Strict device cookie plus a separate device proof stored in this site's localStorage. HTTPS cookies are Secure and host-only. Neither the cookie nor the proof is the admin token, and neither works alone. Cookies are shared across ports of the same hostname; the origin-scoped proof binds the remembered device to this dashboard origin. Still prefer a dedicated hostname. The admin token is never written to localStorage or sessionStorage. If browser storage is unavailable or cannot save the proof, Remember reports unavailable.
+
+The private dashboard-sessions.json stores hashes of the cookie, proof, admin credential and browser identity, the server origin and a fixed 30-day expiry. Relaunching the Safari home-screen app resumes a short-lived GUI session while that device record is valid. Remembered sessions cannot approve consent actions: GitHub stars, reset-credit consumption, prompt-layer edits, Meta Muse sign-in, desktop compatibility changes or quota validation. Use a fresh local or paired session for those actions. Cross-origin/relayed dashboards and remote plaintext HTTP do not support Remember. Expiry, a browser identity change, or rotating the admin token and restarting the server requires sign-in again.
+
+Successful **Forget this device** in the sidebar or mobile action bar (and session logout) immediately revokes the record and all sessions resumed from it, and removes the browser proof. Failed logout keeps the proof so you can retry. The server syncs the record contents before publishing the change; directory-entry durability after a crash is best effort. Older remembered bearer-token keys are removed without being read or migrated. Use only on a device you trust. This does not make Safari offer password AutoFill.
+
+An already-issued session that is otherwise valid can still **Forget this device** after a browser User-Agent change; ordinary access and resumption continue to require the original browser identity.
+
### Finding the admin token
You only need this on a non-loopback bind. A local dashboard never asks, and if a local one
@@ -118,7 +128,7 @@ is visible) and never forces an upstream refresh.
| **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. |
| **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. |
| **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. |
-| **Providers** | Add, edit, set the default (enabled providers only), enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Removing the current default switches to the first remaining enabled provider when one exists; otherwise deletion is refused and the current default is kept. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. For Claude (Anthropic) OAuth pools, each logged-in account shows its own 5-hour and weekly rate-limit bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. The Provider Overview shown when no provider is selected carries a **Refresh all quotas** control that forces one server-side re-read of every configured provider; a provider whose upstream probe fails keeps its last-good row, so the status line reports that the check completed rather than claiming every value is fresh, and each row's own age stays the per-provider freshness signal. |
+| **Providers** | Add, edit, set the default (enabled providers only), enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Removing the current default switches to the first remaining enabled provider when one exists; otherwise deletion is refused and the current default is kept. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. For Claude (Anthropic) OAuth pools, each logged-in account shows its own 5-hour and weekly rate-limit bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. Refreshing quota skips paused OAuth accounts and disabled API keys; skipped rows without a cached reading remain unavailable but do not affect the refresh result. Refreshing reports failure when an unpaused OAuth account or enabled API key that supports quota reads has an unavailable reading. The Provider Overview shown when no provider is selected carries a **Refresh all quotas** control that forces one server-side re-read of every configured provider; a provider whose upstream probe fails keeps its last-good row, so the status line reports that the check completed rather than claiming every value is fresh, and each row's own age stays the per-provider freshness signal. |
| **Add provider** | One search above the tabs reaches all four at once — Accounts, Free, Local, Paid. While a query is live the results are grouped by tab with a count each, and the selected tab stays put rather than jumping. Press ArrowDown in the search box to focus the first available result action, skipping disabled buttons; if no action is available, focus stays in the search box. Local runtimes (Ollama, vLLM, LM Studio, LiteLLM) have their own tab, and a long provider note clamps to two lines with the full text one click away. Claude Code CLI (subscription) sits under Paid with a Subscription CLI badge, and selecting it warns that it bills the signed-in Claude subscription through `claude -p` and never uses an API key (use `anthropic-apikey` for key billing). |
| **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. |
| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. |
@@ -451,6 +461,8 @@ The GUI is a thin client over the proxy's JSON management API. Useful endpoints
| `GET` / `PUT /api/subagent-models` | Read or set the five featured `spawn_agent` override models. |
| `POST /api/stop` | Stop the proxy/service, restore native Codex, and exit. Refused with `respawnable_service` on the Windows Task Scheduler backend, with `self_unload_service` when this proxy is itself the installed launchd/systemd job, and with `service_state_unknown` when the Task Scheduler state cannot be read; nothing is changed in any of those cases. A dashboard session is also refused, with `desktop_supervised`, while the OpenCodex desktop app runs the proxy. |
+Stop rechecks connection ownership before restoring configuration. A new hub connection keeps its routing, catalog and Grok configuration. If ownership is still changing or cannot be confirmed, the API returns HTTP 409 with `sharedTeardown: "refused"` and keeps serving; finish the connection operation and retry. See the [Remote Link stop workflow](/guides/remote-link/).
+
:::tip
Adding **Ollama Cloud** or another catalog provider from the dashboard copies its text-versus-vision
classification into the saved provider config, so the [vision sidecar](/guides/sidecars/)
diff --git a/docs-site/src/content/docs/index.mdx b/docs-site/src/content/docs/index.mdx
index 985cbde406f..fcdddab345b 100644
--- a/docs-site/src/content/docs/index.mdx
+++ b/docs-site/src/content/docs/index.mdx
@@ -1,12 +1,12 @@
---
-title: "opencodex — Run Codex on any LLM"
-description: Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI, App, SDK, and Claude Code.
+title: "opencodex — One local gateway for Codex, Claude Code and more"
+description: "One local gateway for your AI coding tools and models: use the providers you choose with Codex, Claude Code, Claude Desktop and Grok Build, and connect more tools through managed configs."
template: splash
head:
# Brand-first, no " | opencodex" suffix: Google matches the site name against
# the home page title, so the wordmark leads and is not duplicated.
- tag: title
- content: "opencodex — Run Codex on any LLM"
+ content: "opencodex — One local gateway for Codex, Claude Code and more"
- tag: meta
attrs:
property: og:locale
diff --git a/docs-site/src/content/docs/ja/contributing.md b/docs-site/src/content/docs/ja/contributing.md
index 9ad672176ec..ba08d50ca25 100644
--- a/docs-site/src/content/docs/ja/contributing.md
+++ b/docs-site/src/content/docs/ja/contributing.md
@@ -12,7 +12,7 @@ bun install
bun run setup:hooks # 旧管理対象 pre-push・post-merge の削除
bun run dev:proxy # 開発モードのプロキシ API
bun run dev:gui # ダッシュボード dev サーバー(別ターミナル)
-bun run typecheck # bun x tsc --noEmit
+bun run typecheck # bun check --no-script --noEmit
bun run test # 全テストスイート(既定)
```
@@ -122,7 +122,7 @@ Go ネイティブポートを担っていた `dev2-go` は廃止し、2 本の
## 規約
-- **ES Modules のみ**(`import`/`export`)、TypeScript、`strict` モード。`bun x tsc --noEmit` をクリーンに
+- **ES Modules のみ**(`import`/`export`)、TypeScript、`strict` モード。`bun run typecheck` をクリーンに
保ってください。
- **ファイルあたり最大約 500 行** — 責任ごとに分割してください。単一の `index.ts` の後に小さく集中したモジュールを置いた
`web-search/` と `vision/` サイドカーが良い例です。
diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md
index 216e3891dac..82f68cdf175 100644
--- a/docs-site/src/content/docs/ja/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md
@@ -1,9 +1,9 @@
---
title: クイックスタート
-description: 最初のプロバイダーを構成し、3 つのコマンドで OpenAI Codex を opencodex 経由でルーティングします。
+description: "opencodex をインストールして最初のプロバイダーを追加し、Codex、Claude Code、Claude Desktop、Grok Build など、普段使う対応コーディングツールを接続します。"
---
-このガイドでは、新規インストールから非 OpenAI モデルに対して Codex を実行するまでを説明します。
+このガイドでは、新規インストールから普段使うコーディングツールで最初のリクエストを送るまでを説明します。手順 1 と 2 は全ツール共通で、手順 3 はツール別です。[クライアント対応ページ](/ja/reference/client-support/)に各ツールと接続方法を記載しています。
## スタンドアロンバイナリ(npm 不要)
@@ -55,9 +55,13 @@ ocx status
ocx gui # open the dashboard on the live port
```
-## 3.Codexを使用する
+## 3. ツールを接続する
-Codex は透過的に opencodex と通信するようになりました。
+使用するツールの手順を選んでください。複数のツールを接続でき、プロバイダーとモデルを共有します。
+
+### Codex
+
+手順 1 で Codex の確認に同意した場合、Codex は透過的に opencodex と通信します。確認を拒否した場合、または既存の設定があるためにセットアップが終了した場合は、Codex を実行する前にダッシュボードの **Connect** ページにある Codex スイッチをオンにします。[Codex 統合](/ja/guides/codex-integration/)では変更内容と復元方法を説明しています。
```bash
codex "Refactor this function for readability"
@@ -70,6 +74,42 @@ codex -m "anthropic/claude-opus-5" "Explain this stack trace"
codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
```
+### Claude Code
+
+`claude` CLI をインストールした状態で実行します。
+
+```bash
+ocx claude
+```
+
+`ocx claude` はプロキシが停止中なら起動し、opencodex を指す環境で Claude Code を起動します。追加の引数はそのまま渡されます。階層のデフォルト、他のランチャー、無効化の方法は [Claude Code](/ja/guides/claude-code/)を参照してください。
+
+### Claude Desktop
+
+```bash
+ocx claude desktop
+```
+
+新規インストールではアプリ全体にゲートウェイプロファイルを設定します。すでに first-party モードを使用している場合はそのまま維持され、`ocx claude desktop apply --gateway` を実行すると明示的に切り替えられます。Desktop を claude.ai のままにして Code タブのみをルーティングする first-party モードは、[Claude Code](/ja/guides/claude-code/)の Claude Desktop モードの節を参照してください。
+
+モードを切り替えた後は、Claude Desktop を完全に終了してから開き直してください。
+
+### Grok Build
+
+ローカルリスナーでは、`~/.grok` が存在すると `ocx start` が表示対象のモデルを `~/.grok/config.toml` に登録します。Grok Build で `ocx-*` モデルを選択します。リモート設定と管理対象ブロックは [Grok Build](/ja/guides/grok-build/)を参照してください。
+
+プロバイダーやモデルを追加した後は、`ocx ensure` を実行するかプロキシを再起動すると Grok Build に反映されます。
+
+### その他のツール
+
+管理対象の統合があるツール(OpenCode、Pi、Qoder、WorkBuddy、Kilo、Cline、T3 Code など)は、ダッシュボードの **Connect** ページまたは次のコマンドで接続します。
+
+```bash
+ocx integration client enable --client
+```
+
+各ツールの設定ファイルと変更の反映時期は[統合](/ja/guides/integrations/)を参照してください。カスタムエンドポイントに対応するその他のツールでは、OpenAI 互換ベース URL `http://localhost:10100/v1` または Anthropic ベース URL `http://localhost:10100` を使えます。
+
## サブエージェント モデルの選択 (オプション)
新しい設定には、Codex のサブエージェント ピッカーの 5 つのネイティブ モデル、`gpt-5.5`、`gpt-5.6-sol`、`gpt-5.6-terra`、`gpt-5.6-luna`、および `gpt-6-astra` が含まれています。 `ocx gui` を開いて、最大 5 つのネイティブ モデルまたはルーティング モデルを置換または並べ替えます。ダッシュボードでは、優先サブエージェント モデルと推論負荷を 1 つ設定することもできます。 v1/base/v2 を選択し、ガイダンス、ネイティブのデフォルト、およびフォールバックがいつ適用されるかを理解するには、[サブエージェントサーフェス](/guides/sub-agent-surface/) を参照してください。
diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md
index 8476e7eb932..68415977f04 100644
--- a/docs-site/src/content/docs/ja/guides/claude-code.md
+++ b/docs-site/src/content/docs/ja/guides/claude-code.md
@@ -192,6 +192,8 @@ Picker プロファイルを選択した後は、Claude Desktop を完全に終
Picker モードは 1P の一部なので、[1P のアカウントリスク](#1pオプトイン)も同じように適用されます。
+上流サーバーの最初の応答前に、60 秒間転送が進まない Picker アップロードは終了します。タイムアウトしたアップロードに成功応答は返しません。HTTP/2 では対象のストリームだけをリセットします。上流のバックプレッシャーで転送が一時停止している間は、このタイマーも停止します。アップロード完了時または上流の最初の応答時にタイマーを解除します。このアイドル監視はアップロード全体の所要時間に追加の上限を設けません。既存のランタイムやプロトコルの制限は引き続き適用されます。応答ストリームはこのタイマーの対象外です。
+
### Code タブで opencodex モデルを使う(1P バインディング)
1P モードでは、Code タブのモデルピッカーは claude.ai が提供します。各行(Opus 5.5、Sonnet 5、
diff --git a/docs-site/src/content/docs/ja/guides/codex-prompt.md b/docs-site/src/content/docs/ja/guides/codex-prompt.md
index fa1639d47bf..30fa39a9188 100644
--- a/docs-site/src/content/docs/ja/guides/codex-prompt.md
+++ b/docs-site/src/content/docs/ja/guides/codex-prompt.md
@@ -36,6 +36,11 @@ Codex が実際には使用しない順序を示すことになります。
レイヤー名をクリックすると、送信されるテキストを確認できます。ダイアログは
`codex debug prompt-input` から内容を読み取るため、説明ではなく実際の送信内容です。
+読み取りには、メタデータの読み取りと共有プローブの待機時間を含む、呼び出しごとに固定の
+15 秒の制限があります。同期的なメタデータの読み取りが終わってから時間切れを確認し、
+制限に達していれば新しいプローブを開始しません。時間切れになると、ダイアログはプロンプトを
+読み取れなかったと表示しますが、ローカルで読み取れた基本指示は引き続き返されます。
+
表示する内容がない場合もあり、ダイアログに該当する理由が表示されます。
- **ファイルは存在しますが空です。** `~/.codex/AGENTS.md` が 0 バイトのため、
diff --git a/docs-site/src/content/docs/ja/guides/combos.md b/docs-site/src/content/docs/ja/guides/combos.md
index cb31f349e60..70c327e76d1 100644
--- a/docs-site/src/content/docs/ja/guides/combos.md
+++ b/docs-site/src/content/docs/ja/guides/combos.md
@@ -273,3 +273,7 @@ ocx combo remove --yes
通常の 400 エラーは終了扱いですが、`user` の明示的な拒否、`reasoning.effort`/`reasoning_effort` の非対応値、モデル固有の画像入力拒否(`param: input`)、またはルートが読み取れない入力メディアの拒否(`unsupported_input_modality`)を示す構造化エラーでは、出力開始前に次の適格なターゲットへ進めます。この不一致ではクールダウンを記録しません。安全ポリシーによる拒否、キャンセル、出力開始後の再実行は禁止のままです。
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
+
+## クールダウン回復の予約
+
+`cooldownReprobe: { enabled: true }` で有効にした回復試行は、予約したリクエストだけに属し、他のリクエストには保存済みのクールダウンが引き続き適用されます。予約はクールダウン待機と JEV 選択を経ても維持されます。待機中に再び利用可能になったターゲットは、クールダウンが変わっていなければ再試行できますが、新しいクールダウンの記録はそのターゲットの予約を無効にします。成功時は応答したターゲットの変更されていないクールダウンだけを解除し、未試行のターゲットや新しい失敗のクールダウンは残します。プロバイダーの無効化、クォータ、リクエストの適格性、キャンセルの確認は引き続き有効です。[設定と制限](/guides/combos/#retry-all-cooled-targets-opt-in)を参照してください。
diff --git a/docs-site/src/content/docs/ja/guides/desktop-app.md b/docs-site/src/content/docs/ja/guides/desktop-app.md
index 4bac00e4382..8a802d6743c 100644
--- a/docs-site/src/content/docs/ja/guides/desktop-app.md
+++ b/docs-site/src/content/docs/ja/guides/desktop-app.md
@@ -46,6 +46,10 @@ sudo apt install ./OpenCodex--linux-amd64.deb
macOS では、ダッシュボードを閉じてもアプリはメニューバーで動作し続けます。Dock または Finder から OpenCodex を再度開くと、プロキシを再起動せずにダッシュボードが再表示されます。
+Codex の起動でプロキシが見つからない場合、同梱 CLI は独立して検証された Desktop の復旧を待ちます。Desktop に記録された所有権クレームがない場合も同様です。一致する Desktop がプロキシを停止済みか復旧を検証できないときはエラーになりますが、実行イメージ検証のないプラットフォームでは、記録された所有権クレームがありアプリが実行中なら既存の制限付き待機を維持します。プロセス名や検出ファイルだけでは通常の CLI 起動を妨げません。shim は macOS でアプリが終了しており、記録された所有権が有効な **Start at Login** 登録と一致すれば OpenCodex を開きます。自動で開き直すときはその登録の app バンドルパスを選択し、宣言されたメイン実行ファイルが登録済み Desktop の実行ファイルと一致するか確認します。登録がない・古い・不整合の場合は OpenCodex を手動で開いてください。同じバンドル識別子を持つ別コピーは代替として選択されません。shim が "activation not confirmed" と表示する場合は、シェル起動ファイルの他の PATH 設定の後に `. ~/.opencodex/codex-shell-env.sh` を追加してください。
+
+復旧検証の対象は、macOS/Linux の一致する同梱 CLI と保護されたシステム Debian インストールです。古い npm 製の shim は別の Bun/CLI インストールを固定し、Desktop のターミナルコマンド受け渡しを迂回する場合があります。そのエントリポイントでは、公式署名コピーを含む macOS/Windows の Desktop や任意のユーザー単位 Linux コピーを検証できません。Desktop の Startup ページで shim を作り直すか、同梱 CLI を実行してください。Windows はこの復旧証明に必要な実行イメージの同一性をまだ確立できません。識別された実行中の兄弟プロセスは復旧クレジットを得られず、記録された Desktop クレームがなければ CLI プロキシを起動する代わりにエラーを報告します(ディスク上の署名が有効でも同様です)。記録されたクレームがありアプリが実行中なら、CLI は Desktop のプロキシへの既存の制限付き待機を維持します。
+
## トレイでの使用量表示
macOS と Windows ではトレイアイコンをクリックするとコンパクトな使用量ウィンドウが開きます。トレイの **Show usage** 操作でも開けます。これはトレイのクリックイベントを転送しない Linux デスクトップでも使えます。Linux では、デスクトップ環境にトレイアイコンが表示されなくても起動時にダッシュボードが開きます。
diff --git a/docs-site/src/content/docs/ja/guides/integrations.md b/docs-site/src/content/docs/ja/guides/integrations.md
index 3a952318d4e..8017713dde2 100644
--- a/docs-site/src/content/docs/ja/guides/integrations.md
+++ b/docs-site/src/content/docs/ja/guides/integrations.md
@@ -5,6 +5,8 @@ description: ダッシュボードから opencodex を OpenCode、Pi、OMP、Her
**接続** タブは、各クライアントの設定ファイルに opencodex のプロバイダーブロックを書き込み、必要に応じて削除します。以下のクライアントは、それぞれのスイッチで管理できます。
+Windows で絶対パスが必要なオーバーライドには、ドライブ文字(`C:\Users\me\client`)または UNC 共有(`\\server\share\client`)が必要です。現在のドライブに依存する `\Users\me\client` と `/client/config` は拒否されます。`~` 対応と明記されたセレクターは有効なホームを基準に展開します。Gentle Shell は `~` を展開しません。
+
| クライアント | 設定ファイル | 形式 | 変更が反映される時点 | 認証情報 |
|---|---|---|---|---|
| OpenCode | `~/.config/opencode/opencode.json` | JSON | 次回の直接起動時 | `OPENCODEX_OPENCODE_API_KEY` |
@@ -276,3 +278,19 @@ GitHub Copilot デスクトップアプリでは、opencodex を OpenAI 互換
## Factory Droid
Factory Droid は `~/.factory/settings.json`(Windows では `%USERPROFILE%\.factory\settings.json`)を使用します。`ocx integration client enable --client droid` で明示的に有効化し、`/model` でカスタムモデルを選択します。管理対象の行はキーを使わず、ループバックでのみ動作します。無効化すると管理対象の行が削除され、Undo で保存済みのバイト列が復元されます。従来の `config.json` に OpenCodex の行がある場合や、`settings.local.json` が `customModels` を上書きする場合は、有効化する前に競合を解消してください。[Factory BYOK のドキュメント](https://docs.factory.ai/model-independence/byok)も参照してください。
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/ja/guides/model-ordering.md b/docs-site/src/content/docs/ja/guides/model-ordering.md
index 74febb1e782..a00f1cd0c01 100644
--- a/docs-site/src/content/docs/ja/guides/model-ordering.md
+++ b/docs-site/src/content/docs/ja/guides/model-ordering.md
@@ -28,7 +28,7 @@ selector がない場合の priority は次のとおりです。
--- | ---: | --- |
| `subagentModels[i]` | `i`(`0` から `4`) | `src/codex/catalog/sync.ts` の featured rank map |
| その他のルーティングモデル | `5` | `src/codex/catalog/sync.ts` のルーティング項目生成 |
-| デフォルトネイティブ GPT スラッグ | `9` | `src/codex/catalog/sync.ts` のネイティブ項目生成 |
+| デフォルトネイティブ GPT スラッグ | `sourcePriority` | `src/codex/catalog/sync.ts` のネイティブ項目生成 |
| featured リストがあるとき選択されていないネイティブモデル | 最小 `featured.length + 100` | `src/codex/catalog/sync.ts` のネイティブカタログマージ |
管理 API は `src/server/management/agent-settings-routes.ts` の `slice(0, 5)` で `subagentModels` を最大
@@ -69,7 +69,7 @@ Codex の priority ソートでもこの先頭順序は保存されます。
3. カタログマージ過程で featured ブロックの下に押し下げられた選択されていないネイティブモデル
`subagentModels` がない場合、ルーティングモデルは priority `5` を維持し、ネイティブ GPT 項目は通常 priority
-(opencodex が作った項目は通常 `9`)を使います。ルーティンググループ内部は引き続きプロバイダー/ID
+(sourcePriority、情報がなければ `9`)を使います。ルーティンググループ内部は引き続きプロバイダー/ID
アルファベット順です。
## 例
diff --git a/docs-site/src/content/docs/ja/guides/providers.md b/docs-site/src/content/docs/ja/guides/providers.md
index 2c5920a8c38..8f6283a5c10 100644
--- a/docs-site/src/content/docs/ja/guides/providers.md
+++ b/docs-site/src/content/docs/ja/guides/providers.md
@@ -293,7 +293,7 @@ Responses では OpenGateway がネイティブのカスタムツール呼び出
正規の `opencode-zen` プロバイダーで API キー認証と公式ルート `https://opencode.ai/zen/v1` または `https://opencode.ai/inference/v1` を使用すると、Gemini は既定の `googleMode: ai-studio` で Google ネイティブ API を自動的に使用します。Zen のモデル ID は、`-tiered` 接尾辞のない Flash ID もそのまま保持します。Claude は `googleMode` に関係なく Anthropic Messages を使用します。保存済み設定の読み込み時、これらのネイティブモデルの適格な旧 Chat/Responses オーバーライドと、適格な Responses 専用 `image_generation` ホストツール設定のみを無視します。カスタムプロバイダー、キー認証以外のルート、その他の設定は変更しません。
-**`opencode-free` のサポートは削除されました。** OpenCode の上流無料枠の制限により、すべてのモデルが `403 permission_denied` を返すためです([#7255](https://github.com/lidge-jun/opencodex/issues/7255))。既存設定の読み込み時は該当項目と依存するモデル参照をメモリ上で無効にし、プロセスごとに一度警告します。ファイルは変更しません。既定のプロバイダーだった場合、標準の OpenAI OAuth 経路を既定にします。`opencode-go`、API キー付きの `opencode-zen`、または別のプロバイダーを使用してください。[OpenCode Zen 規約](https://opencode.ai/docs/zen/)。
+**`opencode-free` のサポートは削除されました。** OpenCode の上流無料枠の制限により、すべてのモデルが `403 permission_denied` を返すためです([#7255](https://github.com/lidge-jun/opencodex/issues/7255))。既存設定の読み込み時は該当項目と依存するモデル参照をメモリ上で無効にし、プロセスごとに一度警告します。読み込み時にファイルは変更しません。次回の設定保存時に、これらの項目と参照は config.json から削除されます。保存前に元のファイルが隣の `config.json.pre-opencode-free-removal.bak` に自動保存されます。その名前が使用中なら一意の名前のコピーを作成します。バイトが同一の独立したコピーは、所有者のみがアクセスできることを検証できる場合に限り再利用し、それ以外は隣に保護された新しいコピーを作成します。既存ファイルは上書きしません。初回の読み込みが重なると、同一の保護されたコピーが複数残る場合があります。警告には復元用コピーのパスが表示されます。ダウングレード後などに、そのコピーを config.json に上書きして復元できます。コピーに失敗した場合は、すぐに config.json をコピーするよう警告されます。既定のプロバイダーだった場合、標準の OpenAI OAuth 経路を既定にします。`opencode-go`、API キー付きの `opencode-zen`、または別のプロバイダーを使用してください。[OpenCode Zen 規約](https://opencode.ai/docs/zen/)。 パッシブ読み取りではメモリ内の無効化のみを行い、コピーの作成や警告は行いません。
正常な Claude Desktop の割り当てと既定値は保持されます。ソースモデルの許可リストが空になるルールは無効になり、注入モデルが削除された場合はネイティブサブエージェント同期も無効になります。その後の設定保存では、削除済み参照を復元せず、正常な同時編集を保持します。
diff --git a/docs-site/src/content/docs/ja/guides/remote-link.md b/docs-site/src/content/docs/ja/guides/remote-link.md
index 905cfb3db49..65b6a310fa3 100644
--- a/docs-site/src/content/docs/ja/guides/remote-link.md
+++ b/docs-site/src/content/docs/ja/guides/remote-link.md
@@ -10,10 +10,10 @@ description: SSH で OpenCodex の Home コンピューターと Child コンピ
- Home から Child に OpenSSH キーでログインできること。
- Child から開始するリンクでは、Child から Home に OpenSSH キーでログインできる必要があります(パスワードログインには対応していません)。
- Child に OpenCodex 2.66.0 以降がインストールされていること(Child から開始するリンクでは Home にも)。
-- 両方のコンピューターが macOS または Linux であること。
+- 両方のコンピューターで macOS、Linux、Windows x64 を使用できます。Windows には OpenSSH Server と Windows PowerShell が必要です。Git Bash と curl は不要です。
- Home から Child を追加するダッシュボードは Home 上で開くか、ペアリング済みの Hub セッションを使用します。現在のコンピューターを Child にするには、オペレーターがペアリングしたダッシュボードセッションが必要です。認証情報なしのローカルセッションでは、このルーティング変更を確定できません。
-パスワード SSH と Windows は現在のフローに含まれません。リンクはどちら側からでも開始できます。次の手順のように Home から開始するか、後述の「このコンピューターを Child として接続する」のように Child から開始します。
+パスワード SSH はサポートされません。リンクは Home または Child から開始できます。Windows では検証済みの OpenSSH 接続経由で PowerShell を実行します。
## `#remote` から Child を追加する
diff --git a/docs-site/src/content/docs/ja/guides/sub-agent-surface.md b/docs-site/src/content/docs/ja/guides/sub-agent-surface.md
index af233ba13d0..25a084a0fca 100644
--- a/docs-site/src/content/docs/ja/guides/sub-agent-surface.md
+++ b/docs-site/src/content/docs/ja/guides/sub-agent-surface.md
@@ -57,7 +57,7 @@ Codex は `spawn_agent` ツールの説明に最大 5 つのモデルを列挙
| 3 | `deepseek/deepseek-v4-flash` | `5` | アルファベット順で最初の残りのルーティング済み行 |
| 4 | `google/gemini-3-pro` | `5` | 次のルーティング済み行 |
| 5 | `moonshot/kimi-k3` | `5` | 次のルーティング済み行 |
-| 6+ | `zai/glm-5.2`、続いてネイティブ GPT 行 | `5`、続いて `102` | 5 つの枠外 |
+| 6+ | `zai/glm-5.2`、続いてネイティブ GPT 行 | `5`、続いて `102 + sourcePriority` | 5 つの枠外 |
ロスターが空の場合、opencodex は何も固定せず、5 つの枠はカタログ本来の priority に従います。
diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md
index bc1e933f309..b45983256c2 100644
--- a/docs-site/src/content/docs/ja/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md
@@ -13,7 +13,7 @@ opencodex はプロキシが提供するローカルウェブダッシュボー
ocx gui
```
-ブラウザで `http://localhost:` を開きます。ハブで管理イングレスが有効な場合は `http://127.0.0.1:<管理ポート>` を開きます。プロキシがオフなら先に自動で起動します。
+ブラウザで実行中のプロキシのアドレスを開きます。既定は `http://127.0.0.1:`、IPv6 ループバックの `::1` にバインドしている場合は `http://[::1]:` です。このリテラルアドレスにより、ローカルブラウザのペアリングに必要な設定済みオリジンが保たれます。ハブで管理イングレスが有効な場合は `http://127.0.0.1:<管理ポート>` を開きます。プロキシがオフなら先に自動で起動します。
開発中は実行中のプロキシと GUI 開発サーバーを別々に起動できます。
```bash
@@ -27,6 +27,12 @@ bun run dev:gui
リモートダッシュボードでは標準のパスワードフォームが表示され、ブラウザのパスワードマネージャーで保存・自動入力できます。ダッシュボード自体はトークンをメモリ内だけに保持し、`localStorage` や `sessionStorage` には書き込みません。保存するかどうかはブラウザまたはパスワードマネージャーだけが決定します。
+**このデバイスを記憶する** は既定でオフです。直接アクセスする HTTPS ダッシュボード(またはローカルループバック)で、管理者トークンをサーバー発行の HttpOnly・SameSite=Strict デバイス Cookie と、このサイトの localStorage に保存する別のデバイス証明に交換します。HTTPS の Cookie は Secure でホスト限定です。Cookie も証明も管理者トークンではなく、どちらも単独では機能しません。同じホスト名のポート間では Cookie が共有されるため、オリジン単位の証明が記憶したデバイスをこのダッシュボードのオリジンに結び付けます。それでも専用ホスト名を推奨します。管理者トークンは localStorage や sessionStorage に保存しません。ブラウザーのストレージが使えない、または証明を保存できない場合、記憶機能は利用不可と表示されます。
+
+非公開の dashboard-sessions.json は Cookie・証明・管理者認証情報・ブラウザー識別のハッシュ、サーバーのオリジンと固定30日間の有効期限を保存します。Safari ホーム画面アプリを再起動すると、有効なデバイス記録から短期セッションを再開します。記憶したセッションでは、GitHub スター、リセットクレジットの消費、プロンプト層の編集、Meta Muse サインイン、デスクトップ互換性の変更、クォータ検証などの同意操作を承認できません。これらには新しいローカルまたはペアリング済みセッションを使ってください。中継・別オリジン・リモートの平文 HTTP は記憶機能に対応しません。期限切れ、ブラウザー識別の変更、管理者トークン変更後のサーバー再起動では再ログインが必要です。
+
+サイドバーやモバイル操作の **このデバイスを解除**、またはログアウトが成功すると、記録とすべての派生セッションが即座に失効し、ブラウザーの証明を削除します。ログアウトに失敗した場合は再試行のため証明を保持します。サーバーは変更公開前に記録の内容を同期しますが、クラッシュ時のディレクトリエントリの永続性はベストエフォートです。旧トークン保存キーは読み取りや移行をせず削除します。信頼できるデバイスのみで使用してください。Safari の自動入力を有効にするものではありません。
+
## クォータ概要バー
起動安全性ページを除くすべてのページ上部の 1 行の概要に、各プロバイダーの現在のクォータ使用率が表示されます。例: `OpenAI 31% | Claude 54% | xAI 12% | Google 8%`。プロバイダー画面と同じクォータレポート(`GET /api/provider-quotas`、タブが表示されている間は 60 秒ごと)を読み取り、上流への強制更新は行いません。
diff --git a/docs-site/src/content/docs/ja/index.mdx b/docs-site/src/content/docs/ja/index.mdx
index 7c44164ceab..03759a3dd05 100644
--- a/docs-site/src/content/docs/ja/index.mdx
+++ b/docs-site/src/content/docs/ja/index.mdx
@@ -1,10 +1,10 @@
---
-title: "opencodex — Codex を任意の LLM 上で"
-description: OpenAI Codex & Claude Code 向けの汎用プロバイダープロキシ — Codex CLI、App、SDK と Claude Code で任意の LLM を使えます。
+title: "opencodex — Codex、Claude Code などをつなぐローカルゲートウェイ"
+description: "AI コーディングツールとモデルをつなぐ 1 つのローカルゲートウェイ。Codex、Claude Code、Claude Desktop、Grok Build で好きなプロバイダーを使い、管理された設定でほかのツールも接続できます。"
template: splash
head:
- tag: title
- content: "opencodex — Codex を任意の LLM 上で"
+ content: "opencodex — Codex、Claude Code などをつなぐローカルゲートウェイ"
- tag: meta
attrs:
property: og:locale
diff --git a/docs-site/src/content/docs/ja/reference/cli.md b/docs-site/src/content/docs/ja/reference/cli.md
index c766a1cdfaf..c43b4363845 100644
--- a/docs-site/src/content/docs/ja/reference/cli.md
+++ b/docs-site/src/content/docs/ja/reference/cli.md
@@ -20,7 +20,7 @@ opencodex CLI は `ocx` です。最初のコマンド名でディスパッチ
管理コマンドは、2 番目の構成パスを維持するのではなく、記録されたランタイム ポートと ID チェックを使用して、稼働中のプロキシの管理 API をラウンドトリップします。停止したプロキシまたは到達不能なプロキシは HTTP 503 として表され、ゼロ以外の CLI 終了が生成されます。オフライン構成操作として明示的に文書化されているコマンドは、代わりに、稼働中のプロキシを使用せずに設定ファイルを検証および編集できます。
-`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
+`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows x64 は、証明にバインドされた絶対パスの標準 npm `codex.cmd` と保持ハンドルにバインドされた Node/npm ツールチェーン(存在する場合はプレフィックスローカル、存在しないことが確認された場合はランチャーアーキテクチャにアンカーされたキャプチャ済み PATH ペア)、検証済みのインプレフィックスプラットフォームバッキングもサポートし、リパースを拒否する保持ハンドルを使用します。他の Windows レイアウトは引き続き延期または非管理です。Windows の `managed` 結果は観測されたインストールレイアウトのみを分類し、バインドされた Node/npm はアップデーターの読み取り専用メタデータツールチェーンであって、後の `codex.cmd` 呼び出しがどのインタープリターを解決するかを保証するものではありません。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。Windows x64 の apply には、検証済みのプライベート書き込み権限を持つ制限付き npm プレフィックスが必要で、無関係なパッケージ・shim を保持します。適用条件を満たさない観測レイアウトは `applicable: false` と `refusal: windows_apply_disabled` を返し、PATH のみの選択・OpenCodex 自動起動ラッパー・global/vendor フォールバックは引き続き延期されます。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
Windows x64 インストールの観測は [`attest` コマンド](/ja/reference/cli/agents/)を参照してください。明示パスを省略すると、証明にバインドされたランチャースナップショットが特定した選択候補を観測します。更新権限やランタイム選択は証明しません。
diff --git a/docs-site/src/content/docs/ja/reference/cli/agents.md b/docs-site/src/content/docs/ja/reference/cli/agents.md
index ebe43edfffc..961c01c9bf0 100644
--- a/docs-site/src/content/docs/ja/reference/cli/agents.md
+++ b/docs-site/src/content/docs/ja/reference/cli/agents.md
@@ -130,6 +130,39 @@ ocx claude desktop import [--apply] Validate and import JSON
ファミリは `opus`、`fable`、`sonnet`、および `haiku` です。新しいルートは `opus` で始まります。 `none` は、そのファミリーが空の場合にのみ有効です。従来の適用フラグ `--static`、`--hybrid`、および `--discovery-only` は引き続きサポートされます。クロードコードの設定には`ocx claude config ...`を使用してください。
+### `ocx codex [codex args...]`
+
+キーは設定とその名前空間が他の主体による変更から保護されている場合にのみ渡します。現在の利用者と特権 OS 主体を信頼します。同じ利用者のプロセスは信頼対象です。その利用者の書き込み権限を持つ主体は、既にその利用者の OpenCodex キーファイルも読めます。検証からネイティブ起動までの同一利用者による設定置換はこの脅威モデルの対象外で、引き渡し時に設定を固定しません。
+
+通常の Windows の既定 ProgramData 権限では他の利用者がディレクトリを作成できます。そのため `ProgramData\OpenAI\Codex` がなくても自動認証情報を保留します。ProgramData 配下のシステム Codex 設定フォルダーは存在し、Administrators/SYSTEM だけが書き込める必要があり、他の利用者による祖先ディレクトリの置換も防ぐ必要があります。管理者にディレクトリポリシーの確認を依頼してください。opencodex 自身は ACL を変更せず、準備用のコマンドも提供しません。
+
+代わりに利用者が自分でキーを指定できます。例えば `OPENCODEX_API_AUTH_TOKEN` を環境変数としてエクスポートすると、`ocx codex` はその値を変更しません。既存のシステム設定ファイルがあれば引き続き自動認証情報を保留し、別途確認が必要です。各保留理由はキー値、キーファイルの内容やパスを含まない短い具体的な stderr 診断一行で示されます。
+
+単語一つのプロンプトの前に `--` を置き、未知の Codex サブコマンド選択を防いでください。
+
+設定ファイルがなくても、machine 設定パスに他の利用者が作成できない必要があります。Windows ProgramData/OpenAI/Codex の保護を確認できなければ注入を保留し、ディレクトリを作成しません。macOS は拒否のみの ACL と読み取り専用の許可を受け入れます。書き込み・追記・削除・属性・セキュリティ・所有者の変更を許可するエントリは、先行する拒否があっても拒否します。不明なタグや検査エラーも拒否します。
+
+新しいサービストークンを読み込むのは、argv が下記の許可リスト内で、空でない HTTP(S)/ALL proxy や `CODEX_*`/`OPENAI_*`/`AZURE_OPENAI_*` 環境変数の上書きがなく(正規の `CODEX_HOME` は許可)、ホーム・設定・システムの名前空間が所有権検査を通過した場合だけです。設定は監査済みキーと正確な管理 loopback provider のみで、追加の資格情報利用先や設定層がない必要があります。`-c`/`--config`、`-p`/`--profile`、未知のフラグ、`resume`/`fork` など許可外の引数ではトークンを保留し、値を含まない診断を出します。明示した資格情報と元の引数は維持します。
+
+正規ホームと `config.toml` は現在の利用者所有である必要があります。POSIX は group/other の書き込みを、Windows は現在の利用者・SYSTEM・Administrators 以外への DACL 書き込み許可を拒否し、所有者 SID を確認します。祖先パスの置換権限も確認し、macOS は強制 Codex 設定 preference がないことも要求します。同じ利用者や管理者の悪意あるプロセスを隔離するものではありません。
+
+| Allowlist | Flags |
+| --- | --- |
+| Switches | `--no-daemon`, `--json`, `--ephemeral`, `--skip-git-repo-check`, `--no-alt-screen`, `--full-auto`, `--dangerously-bypass-approvals-and-sandbox`, `--search` |
+| One value | `--model`/`-m`, `--sandbox`/`-s`, `--ask-for-approval`/`-a`, `--image`/`-i` |
+
+
+
+自動認証情報の読み込みでは `CODEX_HOME` の `~` と空白をリテラルなパスとして解釈して検証し、同じ正規絶対パスを子プロセスに渡します。ホームを読めない場合は読み込みません。大文字小文字を問わず、空でない `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` があれば `NO_PROXY` に関係なく自動読み込みを省略します。拒否した起動とユーザーがエクスポートした認証情報は変更しません。新しいトークンを渡す場合、既存の除外項目を保持し、子の `NO_PROXY` と `no_proxy` に `127.0.0.1` と `::1` を追加します。`features.respect_system_proxy` の有効化や `--enable`/`--disable` の転送では自動読み込みを省略します。
+
+元の引数で Codex を起動する前にプロキシの起動を試みます。ルーティングは Codex 設定が所有し、ランチャーは書き換えません。ネイティブと外部ゲートウェイでは起動を省略します。不明なルートでは警告して起動を試み、失敗しても警告後に続行します。ヘルプ、バージョン、`login`、`doctor`、`help` などの内部コマンドは起動と自動トークン読み込みを省略します。
+
+選択した provider の全転送設定が、このインストールのループバックリスナーまたは保存されたローカルクライアント接続向けの管理ブロックと一致する場合だけ、不足する `OPENCODEX_API_AUTH_TOKEN` をこのインストールの `service-api-token` から読みます。名前や注入コメントだけでは許可しません。不明・リモートの接続先、既定 profile、プロジェクト設定、`OPENAI_BASE_URL`、転送した `-c`/`--config`、`-p`/`--profile`、`-C`/`--cd`、`--remote` は自動読み込みを省略します。ユーザーがエクスポートした認証情報と引数を保持し、トークンファイルの上書きと管理トークンの衝突を拒否します。
+
+一致を確認した(`matched`)autostart wrapper だけが背後の Codex を直接実行します。検査例外を含む不明な状態では検証済みのバイパスが必要で、管理対象の Windows CMD wrapper は生成テンプレート全体と一致した場合だけ `OCX_SHIM_BYPASS=1` で実行します。通常の npm ランチャーは利用できます。検証不能または古い wrapper は終了コード 1 で拒否し、Codex は開始しません。`ocx codex-shim install` で更新してください。この起動は shim を修復せず、内容検査は Windows backing ファイルの出所や並行したパス置換を保証しません。
+
+ランチャーのヘルプは `ocx help codex`、Codex のヘルプは `ocx codex --help` です。後で通常の `codex` を起動する動作は変わりません。
+
### `ocx opencode [opencode args...]`
プロキシが実行されていることを確認し、OpenCode のインライン ランタイム層 (`OPENCODE_CONFIG_CONTENT`) で生成された `provider.opencodex` および `providers.opencodex` ブロックを使用してオープンコードを起動します。既存のインライン設定は保持され、今回の起動ではこの 2 つのキーのみが置き換えられます。グローバルまたはプロジェクトの `opencode.json` ファイルは、既存の上書きについて警告するために読み取られることがありますが、ディスク上のファイルは変更されません。ルーティングされたモデルは `opencodex//` として表示されます。後でプレーン `opencode` を起動すると、以前とまったく同じように動作します。
@@ -218,7 +251,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows は候補や設定ファイルを読まずに検査を延期します。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
+`check` はレジストリへの問い合わせも Codex の実行も行いません。POSIX では、公開ランチャーが記録した環境候補とランタイムリゾルバーが選ぶ正規パスが一致すると `selectionAttested: true` になり、リンクされた npm-global パッケージのマニフェストで所有権を確認でき、候補バージョンが報告されている場合はそれがマニフェストのバージョンと一致するときに `managed: true` になります。永続化された候補は認証されません。Windows x64 は、証明にバインドされた絶対パスの標準 npm `codex.cmd` と保持ハンドルにバインドされた Node/npm ツールチェーン(存在する場合はプレフィックスローカル、存在しないことが確認された場合はランチャーアーキテクチャにアンカーされたキャプチャ済み PATH ペア)、検証済みのインプレフィックスプラットフォームバッキングもサポートし、リパースを拒否する保持ハンドルを使用します。他の Windows レイアウトは引き続き延期または非管理です。Windows の `managed` 結果は観測されたインストールレイアウトのみを分類し、バインドされた Node/npm はアップデーターの読み取り専用メタデータツールチェーンであって、後の `codex.cmd` 呼び出しがどのインタープリターを解決するかを保証するものではありません。Bun やソースからの直接実行にはランチャーの証明がありません。`plan` は分離した一時 npm 設定・キャッシュ・ログで公式レジストリに問い合わせ、実行中のセッションや不完全なプロセス観測を拒否します。インストールやプロセスへのシグナル送信は行いません。Windows x64 の apply には、検証済みのプライベート書き込み権限を持つ制限付き npm プレフィックスが必要で、無関係なパッケージ・shim を保持します。適用条件を満たさない観測レイアウトは `applicable: false` と `refusal: windows_apply_disabled` を返し、PATH のみの選択・OpenCodex 自動起動ラッパー・global/vendor フォールバックは引き続き延期されます。[コマンドと拒否理由の詳細(英語)](/reference/cli/agents/#read-only-codex-cli-update-plan)を参照してください。
Windows で `CODEX_CLI_PATH=codex` のような単純なコマンド名、リモートパス、デバイスパスが候補としてキャプチャされた場合は、`candidate_path_unavailable` を報告します。候補は取得されていますが、そのパスはこの検査の対象になりません。
@@ -252,3 +285,19 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_
Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available.
The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support.
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
index 17b27dc3f4f..f66b720fdbb 100644
--- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md
@@ -30,9 +30,13 @@ ocx start --socks5-off
実行中のプロキシを (PID によって) 停止し、PID ファイルを削除します。スタンドアロンの所有者ではネイティブ Codex を復元します。マネージド バックグラウンド サービスがインストールされている場合、`ocx stop` はそれを最初に停止するため、プロキシを再起動できません。Web ダッシュボードの **停止** ボタンは同じ処理 (`POST /api/stop`) を実行しますが、Windows タスク スケジューラだけは例外です。タスク終了後もラッパーがプロキシを再起動しうるため、ダッシュボードは `respawnable_service` で拒否し、何も変更せずに `ocx stop` の実行を促します。
-接続済みの hub クライアントまたは Remote Link Child では、`ocx stop`(成功した API 停止を含む)は接続が所有する Codex のルーティング、モデルカタログ、Grok 設定を保持します。`ocx start` はルーティングを再注入せず、その設定で再び処理します。Codex を自身のアカウントへ戻すには `ocx disconnect`、既にネイティブルーティングへ戻した後の再適用には `ocx sync --restart-codex` を使います。
+接続済みの hub クライアントでは、`ocx stop`(成功した API 停止を含む)は接続が所有する Codex のルーティング、モデルカタログ、Grok 設定を保持します。`ocx start` はルーティングを再注入せず、その設定で再び処理します。Codex を自身のアカウントへ戻すには `ocx disconnect`、既にネイティブルーティングへ戻した後の再適用には `ocx sync --restart-codex` を使います。
-互換性を確認できない古い通常プロキシは、サービスやプロセスを停止する前に拒否されます。監視元からランタイムを更新・再起動して再試行してください。
+Remote Link Child を明示的に停止すると、共有設定の復元が成功した場合、ネイティブ Codex のルーティング、カタログ、バックアップ済みの再開履歴メタデータを復元し、管理対象の Grok 設定を削除します。保存済みの Home 接続は保持され、`ocx disconnect` で削除できます。Codex から Home を再び使うには、`ocx start`(サービス導入済みなら `ocx service start`)の後に `ocx sync --restart-codex` を実行してください。起動だけではルーティングは再適用されず、sync は Grok ブロックを再作成しません。
+
+サービス停止前に接続の所有権を確認できない場合、停止 API は HTTP 409 と `sharedTeardown: "refused"` を返し、プロキシとサービスマネージャーを稼働させたままにします。接続または切断の処理が完了してから再試行してください。
+
+接続済みクライアントの停止では、保護されたローカルのランタイム記録とリスナーの証明を照合します。クライアント用リスナーと通常プロキシの両方にこの確認が必要で、通常プロキシでは設定の保持機能も認証付きの読み取りで確認します。古いランタイムや確認できないランタイムは、サービスやプロセスを停止する前に拒否されます。監視元からランタイムを更新・再起動して再試行してください。
既に停止済みのクライアントでは、サービス管理側が非アクティブで、既知のローカル接続先にランタイムが存在しないことを確認してから、通常の未実行結果を返します。
@@ -311,7 +315,7 @@ Windows ステータス トレイ アイコンをインストールして制御
### `ocx gui`
-`http://localhost:` で [ウェブダッシュボード](/guides/web-dashboard/) を開き、プロキシが実行されていない場合は自動起動します。ハブで管理イングレスが有効な場合は `http://127.0.0.1:<管理ポート>` を開きます。
+`http://127.0.0.1:` で [ウェブダッシュボード](/guides/web-dashboard/) を開き、プロキシが実行されていない場合は自動起動します。ハブで管理イングレスが有効な場合は `http://127.0.0.1:<管理ポート>` を開きます。
## 更新
diff --git a/docs-site/src/content/docs/ja/reference/client-support.md b/docs-site/src/content/docs/ja/reference/client-support.md
new file mode 100644
index 00000000000..4e5a7ce8e00
--- /dev/null
+++ b/docs-site/src/content/docs/ja/reference/client-support.md
@@ -0,0 +1,60 @@
+---
+title: "クライアント対応"
+description: "opencodex が接続するコーディングツール、それぞれの接続方法、使用するプロトコル。"
+---
+
+
+
+
+opencodex は、AI コーディングツールとモデルをつなぐ 1 つのローカルゲートウェイです。一部のツールはネイティブに設定し、それ以外はローカルプロキシを指す設定ファイルをエクスポートまたは管理します。このページはコードのレジストリから生成されるため、現在のリリースの内容と正確に一致します。
+
+## サポートレベル
+
+- **implemented**: レジストリに登録されたクライアントです。エクスポートされる設定では、表のプロトコルは opencodex が生成する設定から読み取られ、変わると生成が失敗します。*レビュー済み*は、エンドポイントが設定そのものではなくレビューに基づくことを示します(ネイティブクライアントと、設定を特定のエンドポイントに変換するクライアント)。レビューしたコードが残っているかを生成時に確認します。「OpenAI 互換」の行は特定のエンドポイントを主張しません。
+- **test-covered**: implemented であり、レビューで選んだこのクライアントのテストケースへのリンクがあります。生成時に確認するのはテストが存在し無効化されていないことだけです。何を検証しているかはテストを読んでください。
+- **実行確認**: ある時点の観察記録です。表のクライアント製品・バージョンを表の opencodex バージョンで、記載のシナリオにより、その日付に人が実行し、証拠をリンクしました。ほかのバージョンや製品については何も示しません。証拠は結果の追加時にレビューされ、生成時に再実行はされません。ダッシュは記録がないことを示します。
+
+## クライアント
+
+| クライアント | ネイティブ | 設定エクスポート | 管理ファイル連携 | 受信プロトコル | サポートレベル | 実行確認 | 備考 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| Codex (CLI, app, SDK) | はい | — | — | Responses (レビュー済み) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/codex-integration/codex-inject-missing-config.test.ts)) | — | opencodex が直接設定します。エクスポートされる設定ファイルはありません。 [ガイド](/ja/guides/codex-integration/) |
+| Claude Code | はい | — | — | Anthropic Messages (レビュー済み) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/claude-integration/claude-cli.test.ts)) | — | opencodex が直接設定します。エクスポートされる設定ファイルはありません。 [ガイド](/ja/guides/claude-code/) |
+| Claude Desktop | はい | — | — | Anthropic Messages (レビュー済み) | implemented | — | opencodex が直接設定します。エクスポートされる設定ファイルはありません。 |
+| Grok Build | はい | — | — | Responses (レビュー済み) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/providers/xai/grok-config-inject.test.ts)) | — | opencodex が直接設定します。エクスポートされる設定ファイルはありません。 [ガイド](/ja/guides/grok-build/) |
+| Qoder | — | JSON | はい | OpenAI 互換(エンドポイントはクライアントが選択) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/qoder-client.test.ts)) | — | ローカルリスナーのみ。 |
+| Qoder CN | — | JSON | はい | OpenAI 互換(エンドポイントはクライアントが選択) | implemented | — | ローカルリスナーのみ。 |
+| WorkBuddy | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/agent-clients.test.ts)) | — | ローカルリスナーのみ。適用前にクライアント固有のチェックを実行し、拒否することがあります。 |
+| WorkBuddy AI | — | JSON | はい | Chat Completions | implemented | — | ローカルリスナーのみ。適用前にクライアント固有のチェックを実行し、拒否することがあります。 |
+| OpenCode | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | リモートリスナーにも対応します(API キーヘッダーを追加)。 [ガイド](/ja/guides/opencode/) |
+| Pi | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | ローカルリスナーのみ。 [ガイド](/ja/guides/pi/) |
+| Oh My Pi (omp) | — | YAML | はい | Chat Completions; Responses (ネイティブ OpenAI モデルのみ) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | ローカルリスナーのみ。自分の YAML ブロックだけを編集します。 |
+| Hermes | — | YAML | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/hermes-catalog-refresh.test.ts)) | — | リモートリスナーにも対応します(API キーヘッダーを追加)。自分の YAML ブロックだけを編集します。 |
+| OpenClaw | — | JSON5 | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/gui/integrations-invariants.test.ts)) | — | リモートリスナーにも対応します(API キーヘッダーを追加)。 |
+| Kimi CLI | — | TOML | はい | OpenAI 互換(エンドポイントはクライアントが選択) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export-new-clients.test.ts)) | — | ローカルリスナーのみ。 |
+| Gajae | — | YAML | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-modality-enum.test.ts)) | — | ローカルリスナーのみ。 |
+| DSH | — | YAML | はい | Responses | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | ローカルリスナーのみ。クライアントの現在のプロバイダーストアに書き込みます。自分の YAML ブロックだけを編集します。設定ファイル横のロックファイルで書き込みを調整します。 |
+| MCode | — | YAML | はい | Anthropic Messages | implemented | — | ローカルリスナーのみ。設定ファイル横のロックファイルで書き込みを調整します。 |
+| ZCode | — | JSON | はい | Responses (レビュー済み) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/integrations-current-store.test.ts)) | — | ローカルリスナーのみ。クライアントの現在のプロバイダーストアに書き込みます。クライアントがこの設定からエンドポイントを選びます(クライアントの文書化された動作に基づきレビュー済み)。 |
+| Command Code | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/command-code-client.test.ts)) | — | ローカルリスナーのみ。設定ファイル横のロックファイルで書き込みを調整します。 |
+| Prime | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/prime-client.test.ts)) | — | ローカルリスナーのみ。 |
+| Aside | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/aside-profiles-catalog-refresh.test.ts)) | — | ローカルリスナーのみ。 |
+| Raycast | — | YAML | はい | Chat Completions (レビュー済み) | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/raycast-client.test.ts)) | — | ローカルリスナーのみ。クライアントがこの設定からエンドポイントを選びます(クライアントの文書化された動作に基づきレビュー済み)。 |
+| omo | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/omo-client.test.ts)) | — | ローカルリスナーのみ。 |
+| Cline | — | JSON | はい | Responses | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/cline-client.test.ts)) | — | ローカルリスナーのみ。設定ファイル横のロックファイルで書き込みを調整します。 |
+| Kilo Code | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-reasoning-controls.test.ts)) | — | リモートリスナーにも対応します(API キーヘッダーを追加)。 |
+| Factory Droid | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/droid-client.test.ts)) | — | ローカルリスナーのみ。 [ガイド](/ja/guides/factory-droid/) |
+| Gentle Shell | — | JSON | はい | Chat Completions | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/gentle-shell-client.test.ts)) | — | ローカルリスナーのみ。 |
+| T3 Code | — | JSON | はい | Anthropic Messages | test-covered ([テスト](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/t3code-client.test.ts)) | — | ローカルリスナーのみ。適用前にクライアント固有のチェックを実行し、拒否することがあります。 |
+
+## 受信エンドポイント
+
+プロキシは次のプロトコルを受け付けます。上の表にないツールでも、いずれかのカスタムエンドポイントを設定できれば接続できます。
+
+- `POST /v1/responses`: Responses
+- `POST /v1/chat/completions`: Chat Completions
+- `POST /v1/messages`: Anthropic Messages
+
+## このページを最新に保つ
+
+このページは `scripts/generate-client-support-matrix.ts` がエクスポート・管理連携・ネイティブ連携のレジストリから生成します。クライアントを追加するには `scripts/client-support-matrix/metadata.ts` にエントリを追加し、`bun run docs:clients` を実行してください。コミット済みのページが古いと `bun run docs:clients:check` が失敗します。
diff --git a/docs-site/src/content/docs/ja/reference/configuration.md b/docs-site/src/content/docs/ja/reference/configuration.md
index df2527ef866..6ccfab9b3e5 100644
--- a/docs-site/src/content/docs/ja/reference/configuration.md
+++ b/docs-site/src/content/docs/ja/reference/configuration.md
@@ -19,6 +19,11 @@ opencodex は、永続的な設定を `$OPENCODEX_HOME/config.json` (通常は `
ファイルを解析できない場合、opencodex はファイルを `config.json.invalid-` としてバックアップし、コンソールに警告を表示し、デフォルトで起動します。欠落しているファイルでも、新規インストールのデフォルトである 1 つの `openai` フォワード プロバイダーが使用されます。
+ローカルの `ocx config set`、`unset`、`import`、オフラインの `ocx effort`、カスタムモデルの add/remove、
+プロバイダーの add/remove/set-default は、既存の `config.json` が無効または読み取り不能な場合、
+元のバイト列を変更せずに拒否します。`ocx config validate` を実行し、ファイルを修復してから再試行してください。
+既存の設定がある場合、import にも読み取り可能で有効な保存先ファイルが必要です。
+
## 優先順位とデフォルト
`config.json` の有効な値は、組み込みのデフォルトをオーバーライドします。省略可能なフィールドが欠落している場合は、ドメイン ページに記載されているデフォルトが使用されます。 `OPENCODEX_HOME` は、デフォルトの構成ディレクトリよりも優先されます。 `apiKey: "${PROVIDER_API_KEY}"` などの環境参照を受け入れるフィールドは、リクエスト時にその変数を解決します。送信プロキシの場合、すでに設定されている `HTTP_PROXY` または `HTTPS_PROXY` が最上位の `proxy` フィールドよりも優先されます。
diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md
index fc82f872333..4ed880176bd 100644
--- a/docs-site/src/content/docs/ja/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md
@@ -62,6 +62,8 @@ GUI で登録または OAuth ログインが完了すると、Models ページ
| `codexPinnedTransientPolicy?` | `"hold" \| "detour-new-threads"` | `"hold"` | 手動ピンが劣化したとき、`hold` は警告を出して使い続け、`detour-new-threads` は新しいスレッドだけを別アカウントへ置きます。送信済みのターンは再送しません。 |
| `codexWsReuseAcrossTurns?` | `boolean` | `false` | 同一アカウントとスレッドで Codex WebSocket をターンをまたいで再利用します。既定はオフ。オンにすると忙しいソケットを最大750ms待ち、モデルやティアの変更では最大2本を残します。 |
| `upstreamHostCircuitThreshold?` | `number` | `0` | ネイティブOpenAI forwardのResponses送信とcompact送信で、実証済みの接続前DNS/TCP障害に適用するオプトインのサーキットしきい値です。`0`で無効、`1`〜`20`ではその回数の終端論理リクエストが失敗するとprovider-originを30秒間遮断します。遮断中はアカウント選択やupstream送信の前に`Retry-After`付き`503`を返し、時間経過後はhalf-openリクエストを1件だけ許可します。タイムアウトとHTTP応答は数えず、HTTP応答が1件でもあれば回路を閉じます。 Codex Pool ルーティングでアカウントが固定されていない場合にのみ適用され、`codexAccountMode: "direct"` とアカウント修飾セレクターでは動作しません。 |
+| `maxInboundBodyBytes?` | `number` | `0` | 受信する JSON 本文 1 件のデコード後のサイズを制限するバイト単位の設定です。省略または `0` でデフォルトの 256 MiB を維持し、設定値は 1〜512 MiB に制限されます。変更後は再起動が必要です。超過時はコード `inbound_body_too_large` のローカル HTTP 413 を返します。適用される上限が 256 MiB を超えると `maxConcurrentInboundBodyBytes` の共有ゲートも有効になり、リクエスト単位の上限だけを上げても共有予算は増えません。対象ルートと詳細は[受信リクエスト本文の受け入れ](/ja/reference/inbound-body-admission/)を参照してください。 |
+| `maxConcurrentInboundBodyBytes?` | `number` | `536870912` (512 MiB) | バイト単位の任意のトップレベル共有受け入れ予算です。適用される `maxInboundBodyBytes` が 256 MiB を超える場合にのみ有効です。省略、`0`、無効な値は従来のデフォルト 512 MiB を使います。有効な正の整数は使用時に 512 MiB〜2 GiB (`536870912`〜`2147483648`) に制限されます。再起動が必要で、同一プロセス内のすべてのリスナーが 1 つの予算を共有します。受け入れの計上用であり、メモリの事前割り当て、RSS 上限、RAM に応じた自動調整ではありません。2 GiB には運用者の明示的な選択が必要です。設定例、キューとレスポンスのライフサイクルに伴う制約、デフォルトで解析前の完全な 512 MiB アップロードが 1 件だけになる理由は[受信リクエスト本文の受け入れ](/ja/reference/inbound-body-admission/)を参照してください。 |
| `modelCacheTtlMs?` | `number` | `300000` |プロバイダーごとの `/models` キャッシュの鮮度ウィンドウ。 |
| `cacheRetention?` | `"none" \| "short" \| "long"` | Anthropic ファーストパーティ: `"long"`、その他の Anthropic 互換エンドポイント: `"short"` | Anthropic プロンプト キャッシュ ポリシー: 無効、5 分間の一時的、または 1 時間の延長。未設定の場合、`api.anthropic.com`(API キーまたは OAuth アカウント プール)へのリクエストは 1 時間の TTL を使い、互換ゲートウェイやベンダーのエンドポイントは 5 分の TTL のままです。明示した値が常に優先されます。 |
| `tokenGuardian?` | `OcxTokenGuardianConfig` |オフ |オプションのプロアクティブな OAuth 更新および Codex アカウントのウォームアップ ポリシー。 |
diff --git a/docs-site/src/content/docs/ja/reference/configuration/server.md b/docs-site/src/content/docs/ja/reference/configuration/server.md
index f7f1665247e..e4143c5323f 100644
--- a/docs-site/src/content/docs/ja/reference/configuration/server.md
+++ b/docs-site/src/content/docs/ja/reference/configuration/server.md
@@ -15,7 +15,7 @@ description: リスナー、リモート アクセス、アドミッション
| `emptyCompletionRetry?` | `boolean` | `false` | テキストもツール呼び出しもない Responses ターンを、ターミナルイベント前にストリームが終了した場合も含め、同一リクエストで 1 回再試行するよう明示的に有効化します。再試行は課金対象になる場合があります。`OCX_EMPTY_COMPLETION_RETRY=0` で設定を変更せず無効化できます。combo と routed-compaction turn は対象外です。 |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Codex Responses パススルーから Codex の safety-buffering ヒントを除去します。対象は `x-codex-safety-buffering-enabled` / `x-codex-safety-buffering-faster-model` 応答ヘッダー、`safety_buffering` 型の `response.metadata` SSE イベント、およびその他の SSE イベントにある `safety_buffering` フィールドです。Codex TUI はこれらを、既定の操作でセッションをより弱いモデルに切り替える「より高速なモデルで再試行」プロンプトとして表示します。その他の `x-codex-*` ヘッダーと SSE イベントの内容は、そのフィールドの除去を除いて変更せずに転送されます。既定ではオフです。 |
| `stallTimeoutSec?` | `number` | `300`(public)/ 無効(local) | ストリームが遮断されるまでの、有効な上流進捗がない秒数(Responses とネイティブ Chat)。未設定では**ローカル**上流(loopback・プライベート・`.local`/`.lan` 名)は無効が既定、public 上流は 300 秒。正の値は両方に適用(最小 1 秒)、`0` で無通信 watchdog を全面無効化。canonical ChatGPT SSE を非ストリーミング JSON にまとめる Responses リクエストには、watchdog が無効でも独立した 15 分の全体上限が残る。`/v1/responses/compact` の保留ボディ読み取りもこの予算を共有するが、ローカル上流でも既定は 300 秒。明示値(`0` を含む)が優先される。 |
-| `connectTimeoutMs?` | `number` | `200000` |試行ごとの DNS/TCP/TLS/最終ヘッダーの期限。本体が生成される前に終了します。 |
+| `connectTimeoutMs?` | `number` | `200000`(public)/ 無効(local) | リクエスト送信から上流が最初の応答ヘッダーを返すまでの試行ごとの期限(DNS/TCP/TLS とヘッダー待ち)。本体生成の前に終了し、本体ストリームを制御する `stallTimeoutSec` とは無関係です。未設定では**ローカル**上流(loopback・プライベート・`.local`/`.lan` 名)は無効が既定、public 上流は 200 秒。正の値は両方に適用、`0`(または任意の非正値)でヘッダー期限を全面無効化します。無効にすることで、ローカルモデルが大きなプロンプトで数分の prompt prefill を行っても、最初のヘッダーのタイマーが健全なリクエストを切断しません。 |
| `shutdownTimeoutMs?` | `number` | `5000` |アクティブなターンが中止される前の正常な排出期限。 |
| `websockets?` | `boolean` | `false` | クライアント向け Responses WebSocket パスを広告して許可します。false の場合クライアントは HTTP/SSE を使いますが、対象となる canonical ChatGPT upstream WS 最適化は無効にしません。 |
| `corsAllowOrigins?` | `string[]` | `[]` | 追加の正確な CORS origin。ループバック origin は常に許可します。`chrome-extension://` など authority ベースのブラウザー拡張 origin に対応し、`*` はワイルドカードではありません。Firefox と Safari は拡張 UUID を(インストール/ブラウザー起動ごとに)再生成するため、origin が変わったらエントリを更新してください。 |
@@ -26,6 +26,7 @@ description: リスナー、リモート アクセス、アドミッション
| `serviceHealthRecovery.enabled?` | `boolean` | `false` | バックグラウンドサービス向けのオプトイン復旧。小さな監視プロセスが `/healthz` を確認し、プロキシが生きたまま連続プローブに約 1 分間まったく応答しない場合に終了させ、サービスマネージャー(タスク スケジューラ、launchd、systemd、WinSW)に再起動させます。管理対象サービスの子プロセスでのみ起動し、停止したプロセスが保持していたリクエストは再送しません。1 時間あたり最大 3 回。再起動が必要です。自動復旧には数値のバインドアドレス(または既定の localhost/ワイルドカード マッピング)が必要です。DNS バインド名は監視されません。記録されたアドレスまたはポートが変更されると監視が停止します。 |
| `codexAutoStart?` | `boolean` | `true` | Codex を起動する前に、Codex シムで `ocx ensure` を実行させます。 False を指定すると、操作が行われないことが保証されます。 |
| `codexShimAutoRestore?` | `boolean` | `true` |完了した外部 Codex アップデートによってインストールされたシムが置き換えられた後、インストールされているシムを復元します。環境オプトアウト: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`。 |
+| `nativeMainRefresh?` | `"codex" \| "opencodex"` | auto | `CODEX_HOME` 内のネイティブ メイン ChatGPT ログインを更新する主体を選択します。キーリングの応答はその照会だけに適用されます。トークンがなければその認証情報の準備は失敗しますが、次の照会が同じトークンを返しても利用できます。永続的な拒否状態は保持せず、準備済みのリクエストは完了できます。ファイルベースの動作は変わりません。自動選択を含む動作の詳細は[英語版 Server fields](/reference/configuration/server/#server-fields)を参照してください。 |
| `syncResumeHistory?` | `boolean` | `true` | Codex App 履歴の互換性を元に戻すことができます。元のメタデータは `ocx stop` / `ocx restore` によってバックアップおよび復元されます。 |
| `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` |オフ |認識された Codex ヘルパー/シャドウ呼び出しを、リクエストに設定された推論エフォートを維持したまま選択したモデルにリダイレクトします。デフォルトのソースプレフィックスは `gpt-6-luna`, `gpt-5.6-luna` です。0.144.x 以前のクライアントでは `gpt-5.4-mini` が使われており、`sourceModels` で復元できます。 |
| `webSearchSidecar?` | `OcxWebSearchSidecarConfig` |使用可能な場合はオン | Web 検索サイドカー オプション。 |
@@ -245,3 +246,11 @@ CLI 内部の再試行・ツールターンは要求全体の送信回数上限
完全だが不正な記録(最終行の `null` も含む)は上限付きの受け入れを拒否します。圧縮は有効な残高を保持して続行し、
実行中の拒否はクリーンなジャーナルを再起動で読み直すまで残ります。途中で切れた最終 JSON 行は従来の復旧規則に従います。
詳しい境界とエラーは[英語の正本](/reference/configuration/server/#historical-pool-continuity)を参照してください。
+
+## ローカル upstream のタイムアウト分類
+
+この分類はタイムアウト方針であり、ネットワークアクセスの許可ではありません。DNS を参照せず HTTP(S) の宛先を分類します。正確な `localhost`(大文字小文字と末尾のドットを許容)、`0.0.0.0`、IPv4 loopback (127/8)、RFC 1918 private、link-local (169.254/16)、IPv6 loopback、link-local (fe80::/10)、unique-local (fc00::/7)、IPv4-mapped アドレス、`.local`/`.lan` 名がローカルです。他の DNS 名は loopback に解決されても公開向けの期限を維持します。不正な宛先、非対応スキーム、`localhost.example` も同様です。
+
+ヘッダー期限を無効にしてもクライアントのキャンセルと上位の全体期限は維持されます。明示的な正の `stallTimeoutSec` はローカル本文の無応答を制限します。ローカルの header/stall が未設定なら、ヘッダー到着前の自動期限はありません。ネイティブ Chat/Messages のローカル JSON 読み取りは stall 予算を使用し、公開 JSON 読み取りは従来の `max(connectTimeoutMs ?? 200000, 5000)` を維持します。Compact 本文の既定 300 秒と画像・検索 sidecar の予算は変わりません。
+
+任意の `opencodeClientExport` は `headerTimeoutMs`、`chunkTimeoutMs`、`timeoutMs` に正のミリ秒または無効化する `false` を指定できます。省略時は OpenCode の既定値を維持し、不正なフィールドは出力しません。V1 options と V2 settings の provider 全体に適用します。[遅いローカルモデル](/guides/opencode/#slow-local-models-prefill-timeouts)を参照してください。
diff --git a/docs-site/src/content/docs/ja/reference/inbound-body-admission.md b/docs-site/src/content/docs/ja/reference/inbound-body-admission.md
index c458b118b09..f2ac83d5542 100644
--- a/docs-site/src/content/docs/ja/reference/inbound-body-admission.md
+++ b/docs-site/src/content/docs/ja/reference/inbound-body-admission.md
@@ -5,22 +5,55 @@ description: リクエスト本文の上限引き上げが、同時 HTTP リク
[プロバイダー設定リファレンス](/ja/reference/configuration/providers/)では、受信する JSON 本文 1 件のデコード後の最大サイズである `maxInboundBodyBytes` を説明しています。デフォルトは引き続き 256 MiB で、設定可能な値は 1 MiB から 512 MiB です。変更後はプロキシを再起動し、リスナーと読み取り処理の両方に意図した上限を適用してください。
+従来の共有予算のデフォルト 512 MiB を維持しても、以下に説明するキューやレスポンスのライフサイクル中の予約保持に伴う制約は残ります。
+
+## 共有予算を明示的に選ぶ
+
+`maxConcurrentInboundBodyBytes` は、バイト単位で指定する `config.json` の任意の**トップレベル**設定です。デフォルトは従来と同じ **512 MiB (`536870912`)** です。省略、`0`、無効な値の場合はこのデフォルトを使います。有効な正の整数は、使用時に **512 MiB〜2 GiB (`536870912`〜`2147483648`)** の範囲に制限されます。設定後はプロキシを再起動してください。同一プロセス内のすべてのリスナーは 1 つの予算を共有し、`appOwnedMemoryBudgetMb` とは別に扱います。
+
+リクエストごとの上限を 512 MiB にし、共有予算を明示的に 2 GiB にするには、既存の `config.json` のトップレベルに次のフィールドを追加し、再起動します。
+
+```json
+{
+ "maxInboundBodyBytes": 536870912,
+ "maxConcurrentInboundBodyBytes": 2147483648
+}
+```
+
+これはメモリ使用量のピークが増える可能性を踏まえた運用者の選択です。プロキシは利用可能な RAM を自動検出せず、それに応じた予算も選びません。
+
## 上限を引き上げると同時実行数が変わる
-解決された上限が 256 MiB を超える場合、対象の各 HTTP リクエストは、プロセス全体で共有する 512 MiB の受け入れ予算から、設定した許容量の全量を予約します。複数のリスナーが同じプロセスを共有する場合も、同時に実行できる対象リクエストは最大 1 件です。小さな本文でも許容量全体を予約し、小さい Content-Length や圧縮によって受け入れ制御を回避することはできません。予約は JSON の解析直後ではなく、レスポンスストリームが終了、失敗、またはキャンセル処理を完了するまで続きます。
+適用される上限が 256 MiB を超える場合、対象の各 HTTP リクエストは、**本文を読み取る前に**リクエストごとの許容量全体を予約する必要があります。予約はプロセス全体の `maxConcurrentInboundBodyBytes` 予算を共有し、デフォルトは引き続き **512 MiB** です。
+
+サイズを制限した JSON 解析が完了するまでは、`Content-Length` や圧縮された転送本文が小さくても予約量を減らしません。これらの値を、デコード後や正規化後の JSON サイズの根拠として信頼することはありません。
+
+サイズ制限内で JSON 解析に成功すると、読み取り処理は、実測した転送バイト数とデコード後のバイト数のうち大きい値まで予約量を減らします。変換処理が正規化 JSON の測定用予算を渡した場合は、既存の正規化 JSON の測定値も含めた最大値を使います。予約量は最初のリクエスト許容量を上限として減ることしかなく、正規化後の測定値がその上限を超えても増えません。既存の UTF-8 と JSON 再シリアル化の検査は維持され、入力バイト長を、より大きくなり得る正規化 JSON サイズの代わりには使用しません。
+
+**デフォルトの共有予算 512 MiB** と**リクエストごとの上限 512 MiB** では、許容量全体を予約できる未解析のアップロードは **1 件だけ**です。解析後の予約が **1 バイト**まで減っても、残りの容量は 512 MiB 未満です。次の完全な許容量は、十分な容量が解放されるまで待つ必要があります。そのため、デフォルトではリクエストごとの上限が 512 MiB の場合の元の同時実行問題は**解決しません**。
+
+`maxConcurrentInboundBodyBytes` に `2147483648` を明示すると **2 GiB** を選択し、解析と実測による予約削減の前に **512 MiB 全体を予約するアップロードを最大 4 件**受け入れられます。この 2 GiB 予算は、解析後の同時実行数を 4 件に固定しません。例えば、先に解析したリクエストの予約量が減って次の受け入れに余裕ができれば、共有予算と他の制限の範囲内で、**非常に小さいリクエスト 8 件が同時にレスポンスをストリーミング**できます。
+
+減量後の予約は、非同期処理やプロバイダーの応答待ちの間も保持され、レスポンスストリームの EOF、エラー、またはキャンセル処理の完了時に解放されます。解析だけでは解放されません。クライアントが切断しても、生成側のキャンセル処理が完了するまで保持されます。エラー、中断、キャンセルが競合しても、解放は正確に 1 回だけ行われます。
対象となる POST エンドポイントは `/v1/responses`、`/v1/responses/compact`、`/v1/chat/completions`、`/v1/messages`、`/v1/messages/count_tokens`、`/v1/images/generations`、`/v1/images/edits`、`/v1/alpha/search` です。画像、検索、トークン計数には、従来の設定可能な本文ごとの上限も適用されます。内部の直接変換やコンボ呼び出しは、HTTP の許容量を二重に予約しません。管理、音声、コンテキスト履歴、WebSocket の制限は変わりません。
-設定を省略またはゼロにした場合、あるいは解決された上限が 256 MiB 以下の場合、この追加の同時実行ゲートは有効になりません。既存のリクエスト件数やその他のリソース制限は引き続き適用されます。
+この追加の同時実行ゲートは、適用される `maxInboundBodyBytes` が 256 MiB を超える場合にのみ有効です。このリクエスト単位の設定を省略するか `0` にすると、デフォルトの 256 MiB が使われ、ゲートは無効のままです。一方、`maxConcurrentInboundBodyBytes` を省略または `0` にすると共有予算のデフォルト 512 MiB が選ばれ、有効なゲートを無効にはしません。既存のリクエスト件数やその他のリソース制限は引き続き適用されます。
## 一時的な拒否と大きすぎる本文の違い
-許容量が一時的に使えない場合、プロトコル解析やプロバイダーへの送信前に、HTTP **503**、`Retry-After: 1`、エラーコード `server_busy` が返されます。Messages とトークン計数のクライアントには、型が `overloaded_error` の Anthropic 形式のエラーが返されます。OpenAI 互換クライアントには `server_error` が返されます。リトライヘッダーとクライアント側のバックオフに従い、現在のリクエストが完了してから再試行してください。本文の上限をさらに引き上げても、予算不足は解消しません。
+許容量全体を一時的に予約できない場合、リクエストは **最大 32 件の FIFO 待機キュー**に入り、**最大 120 秒**待機できます。待機中はアプリケーションがリクエスト本文を読み取りません。測定後の予約量削減や最終解放で空きができると、待機順に受け入れます。待機中に中断されたリクエストはキューから除去され、未読の本文は消費せずキャンセルされます。
-本文がリクエスト単位の上限を超えた場合は、従来どおり HTTP **413** になります。宣言されたサイズが上限超過なら、混雑による拒否には置き換わりません。切断時のキャンセル動作も従来どおりです。認証とオリジンの確認は、この同時実行ゲートより先に行われます。
+キューが満杯、または待機時間が満了すると、本文の解析やプロバイダーへの送信前に HTTP **503**、`Retry-After: 1`、エラーコード `server_busy` が返されます。拒否時には未読の本文がキャンセルされます。Messages とトークン計数のクライアントには、型が `overloaded_error` の Anthropic 形式のエラーが、OpenAI 互換クライアントには `server_error` が返されます。認証とオリジンの確認は、受け入れやキューへの追加より先に行われます。
+
+再試行時は、リトライヘッダーとクライアント側のバックオフに従ってください。上限のある待機と実測に基づく予約量の削減は、回避可能な容量不足による拒否を減らしますが、すべての 503 をなくすものではありません。リクエスト単位の上限をさらに引き上げても、継続的な容量不足は解決しません。
+
+本文がリクエスト単位の上限を超えた場合は、従来どおり HTTP **413** になります。宣言されたサイズが上限超過なら、混雑による拒否には置き換わりません。
## 予算が測るもの
-512 MiB は受け入れたリクエストごとの許容量の合計であり、**プロセスのメモリ使用量が 512 MiB 未満に収まる保証ではありません**。デコード、文字列、オブジェクトグラフ、リクエストのコピー、その他のアプリケーション状態には追加のメモリが必要です。既存の UTF-8 と JSON の再シリアル化による測定は維持されます。入力のバイト長を、より大きくなり得る正規化後の JSON サイズの代わりには使用しません。
+設定した予算は受け入れを計上するもので、解析前は許容量全体、解析後は実測に基づく予約量を合算します。**2 GiB や設定容量を事前に割り当てるものではなく**、**プロセス RSS や解析済みオブジェクトのヒープの上限でもありません**。デコード、文字列、オブジェクトグラフ、リクエストのコピー、その他のアプリケーション状態には追加のメモリが必要で、実際のメモリ使用量は予算を超えることがあります。
+
+共有予算のデフォルト 512 MiB に代えて 2 GiB を選ぶと、予約できる合計量は 4 倍になります。リクエストごとの上限が 512 MiB なら、この明示的な選択により解析前の完全なアップロードを 1 件に代えて最大 4 件受け入れ、メモリ使用量のピークが増える可能性があります。運用環境のメモリの余裕に応じて予算を選んでください。リクエストごとの上限が 256 MiB 以下なら、共有予算の設定にかかわらず追加ゲートは無効です。
-小さなリクエストを並列実行する場合は、可能な限りデフォルトの上限を維持してください。長い履歴を扱うために引き上げる際は、追加の同時実行制限を理解したうえで設定してください。
+可能な限り、リクエスト単位のデフォルト上限を維持してください。長い履歴を扱うために引き上げる場合は、メモリ使用量のピークが大きくなる可能性と待機の上限を考慮してください。
diff --git a/docs-site/src/content/docs/ko/contributing.md b/docs-site/src/content/docs/ko/contributing.md
index cb8ba43379e..bcaaa4610e1 100644
--- a/docs-site/src/content/docs/ko/contributing.md
+++ b/docs-site/src/content/docs/ko/contributing.md
@@ -12,7 +12,7 @@ bun install
bun run setup:hooks # 기존 관리형 pre-push·post-merge 제거
bun run dev:proxy # 개발 모드 프록시 API
bun run dev:gui # 대시보드 dev 서버(다른 터미널)
-bun run typecheck # bun x tsc --noEmit
+bun run typecheck # bun check --no-script --noEmit
bun run test # 전체 테스트 스위트 (기본)
```
@@ -121,7 +121,7 @@ Go 네이티브 포트를 담당했던 `dev2-go`는 정리했고, 두 라인을
## 컨벤션
-- **ES Modules 전용**(`import`/`export`), TypeScript, `strict` 모드. `bun x tsc --noEmit`을 깨끗하게
+- **ES Modules 전용**(`import`/`export`), TypeScript, `strict` 모드. `bun run typecheck`을 깨끗하게
유지하세요.
- **파일당 최대 약 500줄** — 책임별로 나누세요. 단일 `index.ts` 뒤에 작고 집중된 모듈을 둔
`web-search/`와 `vision/` 사이드카가 좋은 예입니다.
diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md
index 77cc5a35d1e..8165688935d 100644
--- a/docs-site/src/content/docs/ko/getting-started/quickstart.md
+++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md
@@ -1,9 +1,9 @@
---
title: 빠른 시작
-description: 첫 프로바이더를 설정하고 명령어 세 개로 OpenAI Codex를 opencodex로 라우팅합니다.
+description: "opencodex를 설치하고 첫 프로바이더를 추가한 뒤 Codex, Claude Code, Claude Desktop, Grok Build 또는 다른 지원 코딩 도구를 연결합니다."
---
-이 가이드는 새로 설치한 상태에서 OpenAI가 아닌 모델로 Codex를 실행하기까지의 과정을 안내합니다.
+이 가이드는 새 설치부터 사용 중인 코딩 도구의 첫 요청까지 안내합니다. 1, 2단계는 모든 도구에 공통이며 3단계는 도구별로 나뉩니다. [클라이언트 지원 페이지](/ko/reference/client-support/)에서 도구와 연결 방법을 확인하세요.
## 독립 실행형 바이너리(npm 없음)
@@ -53,9 +53,13 @@ ocx status
ocx gui # open the dashboard on the live port
```
-## 3. Codex 사용
+## 3. 도구 연결
-이제 Codex는 opencodex와 투명하게 연결됩니다:
+사용하는 도구에 해당하는 안내를 선택하세요. 여러 도구를 연결할 수 있으며 프로바이더와 모델은 공유됩니다.
+
+### Codex
+
+1단계에서 Codex 프롬프트를 수락했다면 이제 Codex가 opencodex와 투명하게 통신합니다. 프롬프트를 거절했거나 기존 설정이 있어 설정이 종료됐다면 Codex를 실행하기 전에 대시보드 **Connect** 페이지의 Codex 스위치를 켜세요. [Codex 통합](/ko/guides/codex-integration/)에서 변경 내용과 복원 방법을 설명합니다.
```bash
codex "Refactor this function for readability"
@@ -68,6 +72,42 @@ codex -m "anthropic/claude-opus-5" "Explain this stack trace"
codex -m "ollama-cloud/glm-5.2" "Write a SQL migration"
```
+### Claude Code
+
+`claude` CLI가 설치된 상태에서:
+
+```bash
+ocx claude
+```
+
+`ocx claude`는 프록시가 실행 중이 아니면 시작하고 opencodex를 가리키는 환경으로 Claude Code를 실행합니다. 추가 인수는 그대로 전달됩니다. 계층 기본값, 다른 실행기와 사용 중지 방법은 [Claude Code](/ko/guides/claude-code/)를 참조하세요.
+
+### Claude Desktop
+
+```bash
+ocx claude desktop
+```
+
+새로 설치할 때는 앱 전체에 게이트웨이 프로필을 설정합니다. 이미 first-party 모드를 사용하는 설치는 해당 모드를 유지하며, `ocx claude desktop apply --gateway`를 실행해 명시적으로 전환할 수 있습니다. Desktop은 claude.ai를 유지하고 Code 탭만 라우팅하는 first-party 모드는 [Claude Code](/ko/guides/claude-code/)의 Claude Desktop 모드 항목을 참조하세요.
+
+모드를 전환한 뒤에는 Claude Desktop을 완전히 종료했다가 다시 여세요.
+
+### Grok Build
+
+로컬 리스너에서 `~/.grok`이 존재하면 `ocx start`가 표시되는 모델을 `~/.grok/config.toml`에 등록합니다. Grok Build에서 `ocx-*` 모델을 선택하세요. 원격 설정과 관리 블록은 [Grok Build](/ko/guides/grok-build/)를 참조하세요.
+
+프로바이더나 모델을 추가한 뒤에는 `ocx ensure`를 실행하거나 프록시를 재시작해야 Grok Build에 반영됩니다.
+
+### 다른 도구
+
+관리형 통합이 있는 도구(OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code 등)는 대시보드의 **Connect** 페이지에서 연결하거나 다음을 사용하세요:
+
+```bash
+ocx integration client enable --client
+```
+
+각 도구의 설정 파일과 적용 시점은 [통합](/ko/guides/integrations/)을 참조하세요. 사용자 지정 엔드포인트를 지원하는 다른 도구는 OpenAI 호환 기본 URL `http://localhost:10100/v1` 또는 Anthropic 기본 URL `http://localhost:10100`을 사용할 수 있습니다.
+
## Sub-agent 모델 선택(선택 사항)
새 구성에는 Codex의 sub-agent 선택기에 네이티브 모델 다섯 개인 `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-6-astra`가 표시됩니다. `ocx gui`를 열어 네이티브 또는 라우팅 모델을 최대 다섯 개까지 바꾸거나 순서를 다시 정할 수 있습니다. 대시보드에서는 선호하는 sub-agent 모델과 추론 강도도 설정할 수 있습니다. [Sub-agent Surface](/guides/sub-agent-surface/)에서 v1/base/v2를 고르고, guidance, 네이티브 기본값, fallback이 언제 적용되는지 확인합니다.
diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md
index 14ebff76cba..aa68c2db6c3 100644
--- a/docs-site/src/content/docs/ko/guides/claude-code.md
+++ b/docs-site/src/content/docs/ko/guides/claude-code.md
@@ -217,6 +217,8 @@ Picker 모드를 끄거나 Desktop을 완전히 다시 시작할 때까지 Deskt
Picker 모드는 1P의 일부이므로 [1P 계정 위험](#1p직접-선택)도 그대로 적용돼요.
+상위 서버의 첫 응답 전에는 60초 동안 전송이 진행되지 않는 Picker 업로드를 종료해요. 유휴 시간 만료는 성공 응답을 보내지 않고 업로드를 실패 처리하며, HTTP/2에서는 해당 스트림만 재설정해요. 상위 서버가 데이터를 받지 못해 전송을 일시 중지한 동안에는 이 타이머도 멈추고, 업로드가 끝나거나 첫 응답이 오면 해제해요. 이 유휴 타이머는 전체 업로드 시간에 추가 제한을 두지 않으며, 기존 런타임·프로토콜 제한은 그대로 적용돼요. 응답 스트림은 이 타이머의 적용 대상이 아니에요.
+
### Code 탭에서 opencodex 모델 쓰기 (1P 바인딩)
1P 모드에서 Code 탭의 모델 선택기는 claude.ai가 채워요. Opus 5.5, Sonnet 5, Haiku 4.5와
@@ -573,7 +575,7 @@ Messages → Responses로 변환되는 요청에서는 Claude Code의 `/effort`
| Assistant 텍스트 | `output_text` |
| Assistant `tool_use` | `function_call`(`input` → JSON 문자열로 변환한 `arguments`) |
| 사용자 `tool_result` | `function_call_output`(`is_error` → `[tool error]` 접두사) |
-| 도구 결과의 `tool_reference` | 짝지어진 결과에 `Tool loaded: ` 텍스트로 보존합니다. 도구 선언을 추가하거나 번역 경로의 서버 측 지연 로딩을 활성화하지는 않습니다. |
+| 도구 결과의 `tool_reference` | 기본적으로 짝지어진 결과에 `Tool loaded: ` 텍스트를 보존합니다. [append-only 옵션](/guides/claude-code/#preserve-cache-prefixes-when-client-tools-load)을 켠 적격 최종 Responses 경로에서는 성공한 클라이언트 검색 결과 위치에 현재 선언된 지연 함수의 정의를 배치할 수 있습니다. 참조만으로 도구 권한이나 서버 측 검색 기능을 추가하지는 않습니다. |
| `thinking` / `redacted_thinking` 재생 | 서명과 비공개 페이로드를 제한된 `ocxr1` 봉투에 담은 `reasoning` 항목 |
| Function 도구 | `{type: "function"}`(`web_search*` → `{type: "web_search"}`). `api.anthropic.com`으로 번역하여 보내는 경로에서는 타입이 지정된 `web_search_*`와 `web_fetch_*` 선언도 Anthropic에 전달되어 서버에서 실행됩니다. 웹 검색 사이드카가 `web_search`를 제공하면 사이드카가 우선합니다. 검색과 가져오기 결과는 `server_tool_use` 및 대응하는 `web_search_tool_result` / `web_fetch_tool_result` 블록으로 클라이언트에 반환됩니다 |
| `tool_choice` | `auto`→`auto`, `none`→`none`, `any`→`required`, 이름 지정 함수→`{type:"function",name}`, 호스팅 WebSearch/web_search→`{type:"web_search"}` |
@@ -719,6 +721,12 @@ Anthropic 백엔드를 명시하면 의도적으로 실패 후 중단해요.
언급하면 자동으로 불러와요. 네이티브 패스스루에서는 정상이며, 라우팅 모델에서는 opencodex가
기본적으로 스텁으로 바꿔요(`blockedSkills: ["claude-api"]`).
+**첫 턴부터 MCP 도구 스키마가 컨텍스트를 채움** — `claudeCode.toolSearch: true`는 클라이언트의 도구 검색을 켜는 선택 설정이며 기본값은 꺼짐입니다. 네이티브 Anthropic 경로는 서버 측 검색과 지연 도구 참조를 그대로 전달합니다. 기본 번역 경로는 같은 서버 기능을 제공하지 않으므로 모든 타사 경로에서 켜도 된다는 뜻은 아닙니다.
+
+예외적으로, 클라이언트가 직접 실행하는 eager `ToolSearch` 함수와 구조화된 검색 결과를 제공하고 최종 Responses 목적지가 `additional_tools`를 지원한다면 `claudeCode.appendOnlyToolLoadingModels`로 그 목적지를 명시하여 사용할 수 있습니다. 이 옵션은 사용하지 않은 지연 정의를 초기 카탈로그에서 제외하고 성공한 검색 위치에 현재 정의를 배치합니다. 런처의 `toolSearch` 설정만으로 이 동작이나 서버 측 검색이 활성화되지는 않습니다. 지원되는 클라이언트와 게이트웨이 조합에서만 켜고, 그 밖의 타사 경로에서는 꺼 두세요. 기존 compatibility-enforcement 검사는 그대로 적용되므로 지원하지 않는 요청을 거절할 수 있습니다. 캐시 적중이나 네이티브 검색과의 완전한 동등성은 보장하지 않습니다. [영문 설정 안내](/guides/claude-code/#preserve-cache-prefixes-when-client-tools-load)를 참고하세요.
+
+OpenCode Go의 Responses 엔드포인트와 공식 xAI Responses 호스트는 선택 목록에 있어도 원래 전체 도구 카탈로그를 유지합니다. 이 목적지들은 `additional_tools`를 초기 `tools`로 승격하거나 거절하므로 시간순 로딩 prefix를 보존할 수 없습니다. 검색 결과의 정의를 초기 `tools`로 승격하는 다른 게이트웨이에서도 이 옵션을 꺼 두세요.
+
**서브에이전트가 잘못된 모델로 디스패치됨** — 로스터 에이전트(`ocx-*`)는 Agent 도구의 `model`
인자가 아니라 `` 지시문을 사용해요. 지시문이 원하는 라우트와 일치하는지
확인하고, 모델 자리 표시자로 `"haiku"`를 전달하세요.
diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md
index ad7c94bb7d5..717a24f1da0 100644
--- a/docs-site/src/content/docs/ko/guides/codex-integration.md
+++ b/docs-site/src/content/docs/ko/guides/codex-integration.md
@@ -213,6 +213,14 @@ $CODEX_HOME/models_cache.json
macOS에서는 명시적 선택, shim, PATH, 기존 설치 경로와 실행 가능한 기본 `codex` 후보 뒤에 `/Applications/ChatGPT.app`, `~/Applications/ChatGPT.app` 순서로 Codex 앱의 번들 CLI를 확인하며, 번들의 XML `Info.plist` 식별자가 `com.openai.codex`여야 하고 실행 가능한 저장된 선택을 앱 후보로 자동 교체하지 않습니다.
+실행 시에는 위치와 관계없이 `Codex.app`과 `ChatGPT.app`을 인식하며, 이동한 번들, 명시적
+`CODEX_CLI_PATH`·설정 런타임 명령, 심볼릭 링크 별칭에도 같은 검증을 적용합니다. 앱 이름은 검증
+대상만 결정하고 신뢰를 허용하지 않습니다. 매 실행 전에 정규화한 번들과 정확한 실행 파일의
+엄격한 서명 검증, OpenAI Team ID, 소유권·권한·상위 디렉터리 정책을 확인합니다. 실행 파일은
+번들의 `Contents/` 안에 있어야 하며(Codex.app의 `Contents/Resources/codex-aarch64-apple-darwin`
+포함), 번들이나 실행 파일을 정규화할 수 없으면 실행을 거부합니다. 일반 CLI와 다른 회사 앱의
+번들은 기존 실행 정책을 유지합니다.
+
WSL에서는 `CODEX_HOME`이 비어 있고 Linux `~/.codex` 디렉터리가 없거나 Codex 상태(`config.toml`, `auth.json`, `sessions`, `history.jsonl`)가 전혀 없을 때 `/mnt/c/Users/*/.codex/config.toml` 아래의 단일 Windows Codex Desktop home도 확인합니다. 후보가 정확히 하나면 그 디렉터리를 사용하므로 WSL app-server mode와 Windows Codex Desktop이 같은 config와 auth 파일을 공유합니다. 이 탐지를 덮으려면 `CODEX_HOME`을 명시하세요.
Windows에서 Orca shell은 `CODEX_HOME`과 `ORCA_CODEX_HOME`을 Orca의 번들 런타임 home으로 설정할 수 있지만, ChatGPT/Codex app은 여전히 `%USERPROFILE%\\.codex`를 읽습니다. `ocx status`와 `ocx doctor`는 이 정확한 불일치를 경고하고, 경로는 가린 채 대상 home을 출력합니다. 해당 Orca shell에서 background service를 설치했다면 먼저 원래 shell에서 uninstall하고, `CODEX_HOME`을 app home으로 설정한 뒤 `ORCA_CODEX_HOME`을 해제하고, sync/restore를 다시 실행한 다음 service를 다시 설치하세요.
diff --git a/docs-site/src/content/docs/ko/guides/codex-prompt.md b/docs-site/src/content/docs/ko/guides/codex-prompt.md
index 318468b05d8..d01f82b5df1 100644
--- a/docs-site/src/content/docs/ko/guides/codex-prompt.md
+++ b/docs-site/src/content/docs/ko/guides/codex-prompt.md
@@ -35,6 +35,11 @@ Codex가 실제로 쓰지 않는 순서를 보여 주게 됩니다.
레이어 이름을 클릭하면 전송되는 텍스트를 볼 수 있습니다. 대화 상자는
`codex debug prompt-input`에서 내용을 읽으므로 설명문이 아닌 실제 전송 내용입니다.
+읽기에는 호출마다 고정된 15초 제한이 있으며, 메타데이터를 읽는 시간과 공유 프로브를
+기다리는 시간도 포함됩니다. 동기식 메타데이터 읽기가 끝난 뒤에 시간 초과를 확인하므로,
+그때 제한에 도달했다면 새 프로브를 시작하지 않습니다. 시간이 초과되면 대화 상자는
+프롬프트를 읽지 못했다고 표시하지만, 로컬에서 읽은 기본 지침은 계속 반환됩니다.
+
표시할 내용이 없을 때도 있으며, 대화 상자에 해당하는 이유가 나옵니다.
- **파일은 있지만 비어 있습니다.** `~/.codex/AGENTS.md`가 0바이트라서
diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md
index e5d8c844516..6abe4f0241f 100644
--- a/docs-site/src/content/docs/ko/guides/combos.md
+++ b/docs-site/src/content/docs/ko/guides/combos.md
@@ -289,3 +289,7 @@ opencodex 인스턴스에 기록했는지 확인하세요.
일반적인 400 오류는 종료되지만, `user`를 명시적으로 거부하거나 `reasoning.effort`/`reasoning_effort`의 지원되지 않는 값, 모델별 이미지 입력 거부(`param: input`) 또는 경로가 읽을 수 없는 입력 미디어 거부(`unsupported_input_modality`)를 나타내는 구조화된 오류는 출력 시작 전에 다음 적격 대상으로 넘어갈 수 있습니다. 이 경우 쿨다운을 기록하지 않습니다. 보안 정책 거부, 취소 및 이미 시작된 출력은 재실행하지 않습니다.
[Canonical compatibility details](/guides/combos/#request-local-target-compatibility).
+
+## 쿨다운 복구 예약
+
+`cooldownReprobe: { enabled: true }`로 활성화한 복구 시도는 이를 예약한 요청에만 속하며, 다른 요청에는 저장된 쿨다운이 계속 적용됩니다. 예약은 쿨다운 대기와 JEV 선택을 거쳐 유지됩니다. 대기 중 다시 사용 가능해진 대상은 기존 쿨다운이 바뀌지 않았다면 재시도할 수 있지만, 새 쿨다운 기록은 그 대상의 예약을 무효화합니다. 성공하면 응답한 대상의 변경되지 않은 쿨다운만 해제하며, 시도하지 않은 대상과 새로운 실패의 쿨다운은 유지합니다. 공급자 비활성화, 할당량, 요청 적격성 및 취소 검사는 계속 적용됩니다. [설정과 제한](/guides/combos/#retry-all-cooled-targets-opt-in)을 참고하세요.
diff --git a/docs-site/src/content/docs/ko/guides/desktop-app.md b/docs-site/src/content/docs/ko/guides/desktop-app.md
index 37c38585ff1..37ff66aba54 100644
--- a/docs-site/src/content/docs/ko/guides/desktop-app.md
+++ b/docs-site/src/content/docs/ko/guides/desktop-app.md
@@ -91,18 +91,30 @@ macOS에서 메뉴의 **Start at Login** 항목은 시작 안전 페이지도
**status unknown**은 등록을 읽지 못했다는 뜻입니다. 앱이 프록시를 실행하는 동안 시작 안전 페이지는
백그라운드 서비스 설치를 막아 둡니다. 서비스를 대신 쓰려면 OpenCodex를 종료하고
터미널에서 `ocx service install`을 실행하세요. Codex 런처 shim은 이 페이지에서 설치할 수 있습니다.
-Codex를 실행할 때 프록시가 없으면 shim은 OpenCodex가 프록시를 다시 띄우기를 기다립니다. macOS에서
-앱이 종료돼 있고 기록된 소유 정보가 유효한 **Start at Login** 등록과 일치하면 앱을 열고, 그 밖의
-경우에는 두 번째 프록시를 띄우지 않고 오류를 알립니다. 자동으로 다시 열 때는 그 등록에 기록된 앱
+Codex를 실행할 때 프록시가 없으면 번들 CLI는 독립적으로 확인된 Desktop의 복구를 기다립니다.
+기록된 소유 정보가 없어도 적용됩니다. 확인된 Desktop이 프록시를 중지했거나 복구 상태를 검증할 수
+없으면 오류를 알립니다. 다만 플랫폼이 복구 검증 자체를 지원하지 않을 때는 기록된 소유 정보와
+실행 중인 앱이 기존의 한정된 대기를 그대로 허용합니다. 프로세스 이름이나 검색용 파일만으로는 일반 CLI 시작을 막지 않습니다.
+macOS에서 앱이 종료돼 있고 기록된 소유 정보가 유효한 **Start at Login** 등록과 일치하면 앱을 엽니다.
+자동으로 다시 열 때는 그 등록에 기록된 앱
번들 경로를 사용하고, 번들이 선언한 주 실행 파일이 등록된 Desktop 실행 파일과 같은지 확인합니다.
등록이 없거나 오래됐거나 서로 맞지 않으면 OpenCodex를 직접 여세요. 번들 식별자가 같은 다른 사본을
대신 고르지 않습니다.
shim이 "활성화 미확인"으로 보이면 셸 시작 파일의 다른 PATH 설정 뒤에
`. ~/.opencodex/codex-shell-env.sh`를 추가하세요.
+복구 검증은 macOS/Linux에서 같은 설치의 번들 CLI와 보호된 시스템 Debian 설치를 지원합니다.
+예전에 npm으로 만든 shim은 별도 Bun/CLI 경로를 고정하여 Desktop의 터미널 명령 전달을 거치지
+않을 수 있습니다. 이 진입점에서는 공식 서명된 사본을 포함한 macOS/Windows Desktop이나 임의의
+사용자별 Linux 사본을 검증할 수 없습니다. Desktop의 Startup 페이지에서 shim을 다시 만들거나
+번들 CLI를 실행하세요. Windows에서는 복구 증명에 필요한 실제 실행 이미지의 동일성을 아직 확인할
+수 없어, 같은 설치의 앱이 실행 중이면 복구 점수를 인정하지 않고 기록된 소유 정보가 없으면
+디스크 파일의 서명이 유효하더라도 CLI 프록시를 띄우는 대신 오류를 알립니다. 기록된 소유 정보가
+있고 앱이 실행 중이면 CLI는 Desktop 프록시를 기다리는 기존의 한정된 대기를 유지합니다.
+
번들 CLI 경로는 macOS에서 `/Applications/OpenCodex.app/Contents/MacOS/ocx`,
Linux deb 패키지에서 `/usr/bin/ocx`입니다. 해당 실행 파일에 `status`를 붙여
-앱의 프록시 상태를 확인할 수 있습니다. Windows에서도 실행 중인 감독 관계를 확인하지만,
+앱의 프록시 상태를 확인할 수 있습니다. Windows에서도 프록시 자식 프로세스가 실행 중이면 감독 관계를 확인하지만,
**Start at Login**은 검증하지 않으며 Desktop에 시작 안전 보호를 인정하지 않습니다.
CLI가 Desktop의 프록시 감독을 감지하면 `ocx service install`, `repair`, `start`,
diff --git a/docs-site/src/content/docs/ko/guides/integrations.md b/docs-site/src/content/docs/ko/guides/integrations.md
index aa51c5a3bf6..d017176dd71 100644
--- a/docs-site/src/content/docs/ko/guides/integrations.md
+++ b/docs-site/src/content/docs/ko/guides/integrations.md
@@ -5,6 +5,8 @@ description: 대시보드에서 OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code,
**연결** 페이지는 클라이언트의 설정 파일에 opencodex 프로바이더 블록을 쓰고 다시 제거합니다. 다음 클라이언트는 각각 스위치로 관리합니다.
+Windows에서 절대 경로가 필요한 오버라이드는 드라이브 문자(`C:\Users\me\client`) 또는 UNC 공유(`\\server\share\client`)를 포함해야 합니다. 현재 드라이브에 의존하는 `\Users\me\client`와 `/client/config`는 거부됩니다. `~`를 지원한다고 명시된 선택자는 유효 홈을 기준으로 확장합니다. Gentle Shell은 `~`를 확장하지 않습니다.
+
| 클라이언트 | 설정 파일 | 형식 | 변경 적용 시점 | 자격 증명 |
|---|---|---|---|---|
| OpenCode | `~/.config/opencode/opencode.json` | JSON | 다음 직접 실행 시 | `OPENCODEX_OPENCODE_API_KEY` |
@@ -92,6 +94,12 @@ Disable은 opencodex가 소유한다고 기록한 항목만 제거합니다. 이
Hermes 세션 식별 설정에는 예외가 있습니다. 기존 관리 설정에 `session_affinity_header: session-id`만 추가했다면 **Apply**로 수용할 수 있습니다. 다른 관리 필드의 수정은 계속 충돌로 처리됩니다. 적용 전에는 백그라운드 모델 목록 갱신도 보류됩니다. 이 설정은 provider의 모든 모델에 적용되며 해당 기능을 지원하는 Hermes 버전이 필요합니다. 캐시 적중률은 보장하지 않습니다. [영문 업그레이드 안내](/guides/integrations/#hermes-session-affinity)를 참조하세요.
+## Hermes 프로필의 모델 목록 갱신
+
+모델 목록 변경과 `ocx sync`는 실행 중인 프록시의 환경(`HERMES_HOME` 포함)에서 정한 기본 프로필만 자동 갱신합니다. 이름이 있는 프로필은 이전 목록을 유지하므로 **Connect → Hermes**에서 **Update needed**인 프로필을 선택하고 **Apply** 미리 보기를 검토한 뒤 적용하세요. CLI에는 이름이 있는 프로필 선택자가 없습니다.
+
+모델 목록이 바뀌지 않았다면 주석이나 `providers.opencodex` 밖 설정을 수정해도 갱신은 **Current** 상태를 유지하며 파일과 기록된 기준선을 바꾸지 않습니다. 모델 목록 변경이 대기 중이면 자동 갱신은 마지막 적용 이후 달라진 Hermes 파일을 이런 수정까지 포함해 거부합니다. 모델 목록 변경으로 **Update needed**가 표시되면 해당 변경을 **Apply**하여 자동 갱신을 다시 허용할 수 있습니다. 목록이 같은 상태의 Apply는 아무것도 바꾸지 않으며 새 파일 기준선도 저장하지 않습니다. 비활성화했거나 연결한 적 없는 설정은 자동으로 연결하지 않습니다. [영문 프로필 안내](/guides/integrations/#hermes-profiles)를 참조하세요.
+
## 변경 미리 보기와 확인
Apply, Replace, Disable, Restore는 미리 보기로 시작합니다. 대화 상자는 제한된 변경 경로와 각 값의 추가·갱신·삭제 여부를 포함해 어떤 관리 설정이 바뀔지 정확히 보여줍니다. 확인하기 전에 계획을 검토하세요.
@@ -243,10 +251,14 @@ Kilo CLI, VS Code, JetBrains는 전역 설정을 공유합니다. 이 연동은
Kilo는 이 전역 파일을 모두 병합합니다. 다른 후보 파일에도 `provider.opencodex`가 있으면 상태에 충돌 파일을 표시하고 적용과 교체를 거부합니다. 연동을 켜기 전에 해당 파일에서 `provider.opencodex`를 제거하세요. 이미 소유한 파일의 블록은 충돌 중에도 비활성화할 수 있습니다. 읽을 수 없거나 안전하지 않은 후보 파일도 쓰기를 막습니다.
+OpenCodex는 이 후보 파일을 JSONC로 읽습니다. `//` 한 줄 주석은 CR, LF, CRLF 줄바꿈에서 끝나므로, 어떤 줄바꿈으로 저장한 파일이든 주석 뒤의 설정을 그대로 읽습니다.
+
관리하는 부분은 OpenCode V1 형식의 `provider.opencodex`(`npm`, `options`, `models`)뿐입니다. OpenCode V2의 `providers` 키는 내보내지 않습니다. 대신 모델별 추론 선택지는 프로바이더 블록 안의 변형 맵으로, OpenCode 내보내기와 같은 유효 정규 메타데이터에서 기록됩니다. `$schema`, `model`, `enabled_providers`, MCP 등의 키는 사용자가 관리합니다. 적용한 뒤 Kilo에서 `opencodex/`을 선택하세요.
루프백에서는 `options.apiKey`로 `{env:OPENCODEX_KILO_API_KEY}`를 사용합니다. 루프백이 아닌 바인드에서는 인증을 `options.headers["x-opencodex-api-key"]`로 옮기며 실제 키를 저장하지 않습니다. 적용 시 전역 파일 전체를 보기 좋은 JSON으로 다시 쓰므로 다른 키의 주석과 후행 쉼표는 보존되지 않습니다. Kilo는 자동 카탈로그 갱신 대상입니다. `ocx sync`, `POST /api/sync`, 표시 여부·프로바이더·프리셋 변경은 다른 클라이언트와 같은 소유 블록만 갱신하는 안전한 규칙으로 소유한 Kilo 블록을 갱신하며, 새 Kilo 세션이 갱신된 파일을 읽습니다.
+후보 파일을 읽을 때 배열 요소나 객체 속성 뒤의 후행 쉼표는 허용합니다. 빈 배열과 객체는 `[]`, `{}`로 작성하세요. `[,]`, `{,}`는 잘못된 형식으로 거부합니다.
+
```bash
ocx integration client enable --client kilo
ocx export --client kilo --out ./kilo.jsonc
@@ -304,3 +316,19 @@ GitHub Copilot 데스크톱 앱에서 opencodex를 OpenAI 호환 모델 프로
행과 기본값이 함께 제거되며, 되돌리기는 저장된 행과 기본값을 함께 복원합니다.
Factory Droid는 `~/.factory/settings.json`(Windows에서는 `%USERPROFILE%\.factory\settings.json`)을 사용합니다. `ocx integration client enable --client droid`로 명시적으로 활성화한 다음 `/model`에서 사용자 지정 모델을 선택하세요. 관리되는 항목에는 키가 없으며 루프백에서만 동작합니다. 비활성화하면 관리되는 항목이 제거되고, Undo는 저장된 원본 바이트를 복원합니다. 기존 `config.json`에 OpenCodex 항목이 있거나 `settings.local.json`이 `customModels`를 덮어쓰면 활성화 전에 충돌을 해결하세요. [Factory BYOK 문서](https://docs.factory.ai/model-independence/byok)를 참고하세요.
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/ko/guides/model-ordering.md b/docs-site/src/content/docs/ko/guides/model-ordering.md
index e921bfdfbcd..5aa24876b6e 100644
--- a/docs-site/src/content/docs/ko/guides/model-ordering.md
+++ b/docs-site/src/content/docs/ko/guides/model-ordering.md
@@ -28,7 +28,7 @@ selector가 없을 때의 priority는 다음과 같습니다.
| --- | ---: | --- |
| `subagentModels[i]` | `i` (`0`부터 `4`) | `src/codex/catalog/sync.ts`의 featured rank map |
| 그 밖의 라우팅 모델 | `5` | `src/codex/catalog/sync.ts`의 라우팅 항목 생성 |
-| 기본 네이티브 GPT slug | `9` | `src/codex/catalog/sync.ts`의 네이티브 항목 생성 |
+| 기본 네이티브 GPT slug | `sourcePriority` | `src/codex/catalog/sync.ts`의 네이티브 항목 생성 |
| featured 목록이 있을 때 선택되지 않은 네이티브 모델 | 최소 `featured.length + 100` | `src/codex/catalog/sync.ts`의 네이티브 카탈로그 병합 |
관리 API는 `src/server/management/agent-settings-routes.ts`의 `slice(0, 5)`로 `subagentModels`를 최대
@@ -69,7 +69,7 @@ Codex의 priority 정렬에서도 이 선두 순서가 보존됩니다.
3. 카탈로그 병합 과정에서 featured 블록 아래로 밀린 선택되지 않은 네이티브 모델
`subagentModels`가 없으면 라우팅 모델은 priority `5`를 유지하고, 네이티브 GPT 항목은 정상 priority
-(opencodex가 만든 항목은 보통 `9`)를 사용합니다. 라우팅 그룹 내부는 계속 프로바이더/id
+(sourcePriority, 출처가 없으면 `9`)를 사용합니다. 라우팅 그룹 내부는 계속 프로바이더/id
알파벳순입니다.
## 예시
diff --git a/docs-site/src/content/docs/ko/guides/providers.md b/docs-site/src/content/docs/ko/guides/providers.md
index e7fe1c3c387..453aad8d639 100644
--- a/docs-site/src/content/docs/ko/guides/providers.md
+++ b/docs-site/src/content/docs/ko/guides/providers.md
@@ -302,7 +302,7 @@ Responses 도구 결과 배열은 텍스트로 변환하고 이미지는 짝이
정식 `opencode-zen` 제공자에서 API 키 인증과 공식 경로 `https://opencode.ai/zen/v1` 또는 `https://opencode.ai/inference/v1`을 사용하면 Gemini는 기본 `googleMode: ai-studio`에서 Google 네이티브 API를 자동으로 사용합니다. Zen 모델 ID는 `-tiered` 접미사가 없는 Flash ID도 그대로 유지합니다. Claude는 `googleMode`와 관계없이 Anthropic Messages를 사용합니다. 저장된 설정을 읽을 때 이 네이티브 모델의 조건에 맞는 기존 Chat/Responses 재정의와 조건에 맞는 Responses 전용 `image_generation` 호스팅 도구 선호 설정만 무시합니다. 사용자 지정 제공자, 키 인증이 아닌 경로, 그 밖의 선호 설정은 그대로 유지합니다.
-**`opencode-free` 지원이 제거되었습니다.** OpenCode의 업스트림 무료 등급 제한으로 모든 모델이 `403 permission_denied`를 반환하기 때문입니다([#7255](https://github.com/lidge-jun/opencodex/issues/7255)). 기존 설정을 불러오면 해당 항목과 종속 모델 참조를 메모리에서 비활성화하고 프로세스당 한 번 경고하며 파일은 변경하지 않습니다. 기본 공급자였다면 표준 OpenAI OAuth 경로가 기본값이 됩니다. `opencode-go`, API 키를 사용하는 `opencode-zen` 또는 다른 공급자를 사용하세요. [OpenCode Zen 약관](https://opencode.ai/docs/zen/).
+**`opencode-free` 지원이 제거되었습니다.** OpenCode의 업스트림 무료 등급 제한으로 모든 모델이 `403 permission_denied`를 반환하기 때문입니다([#7255](https://github.com/lidge-jun/opencodex/issues/7255)). 기존 설정을 불러오면 해당 항목과 종속 모델 참조를 메모리에서 비활성화하고 프로세스당 한 번 경고하며 불러올 때 파일은 변경하지 않습니다. 다음 설정 저장 시 해당 항목과 참조가 config.json에서 제거됩니다. 저장 전에 원본 파일을 같은 폴더의 `config.json.pre-opencode-free-removal.bak`에 자동 보관합니다. 해당 이름이 이미 사용 중이면 고유한 이름의 사본을 만듭니다. 바이트가 동일한 독립 사본은 소유자만 접근할 수 있음을 검증한 경우에만 재사용하고, 그렇지 않으면 그 옆에 보호된 새 사본을 만듭니다. 기존 파일은 덮어쓰지 않습니다. 첫 로드가 겹치면 동일하게 보호된 사본이 여러 개 남을 수 있습니다. 경고에 복구 사본 경로가 표시됩니다. 다운그레이드 후 복원하려면 이 사본을 config.json 위에 복사하세요. 자동 복사에 실패하면 config.json을 즉시 복사하라는 경고가 표시됩니다. 기본 공급자였다면 표준 OpenAI OAuth 경로가 기본값이 됩니다. `opencode-go`, API 키를 사용하는 `opencode-zen` 또는 다른 공급자를 사용하세요. [OpenCode Zen 약관](https://opencode.ai/docs/zen/). 수동적 읽기(passive read)에서는 메모리 내 비활성화만 적용하고 사본이나 경고를 만들지 않습니다.
정상 Claude Desktop 할당과 기본값은 유지됩니다. 소스 모델 허용 목록이 비게 되는 규칙은 비활성화되며, 주입 모델이 제거되면 네이티브 하위 에이전트 동기화도 비활성화됩니다. 이후 설정을 저장하면 제거된 참조는 다시 기록되지 않고 정상적인 동시 편집은 유지됩니다.
diff --git a/docs-site/src/content/docs/ko/guides/remote-link.md b/docs-site/src/content/docs/ko/guides/remote-link.md
index 5ad0f0ea302..1a0df583ecf 100644
--- a/docs-site/src/content/docs/ko/guides/remote-link.md
+++ b/docs-site/src/content/docs/ko/guides/remote-link.md
@@ -10,10 +10,10 @@ description: SSH로 OpenCodex Home 컴퓨터와 Child 컴퓨터를 연결합니
- Home 컴퓨터에서 OpenSSH 키 로그인으로 Child 컴퓨터에 접속할 수 있어야 합니다.
- 자식이 시작하는 링크에서는 자식에서 OpenSSH 키 로그인으로 홈에 접속할 수 있어야 합니다(비밀번호 로그인은 지원하지 않음).
- Child 컴퓨터에 OpenCodex 2.66.0 이상이 설치되어 있어야 합니다(자식이 시작하는 링크에서는 Home에도).
-- 두 컴퓨터 모두 macOS 또는 Linux여야 합니다.
+- 두 컴퓨터는 macOS, Linux 또는 Windows x64를 사용할 수 있습니다. Windows에는 OpenSSH Server와 Windows PowerShell이 필요하며 Git Bash와 curl은 필요하지 않습니다.
- Home에서 Child를 추가하는 대시보드는 Home에서 직접 열거나 페어링된 Hub 세션을 사용해야 합니다. 현재 컴퓨터를 Child로 전환하려면 운영자가 페어링한 대시보드 세션이 필요하며, 자격 증명 없이 발급된 로컬 세션은 이 라우팅 변경을 확정할 수 없습니다.
-비밀번호 SSH와 Windows는 현재 흐름에서 지원하지 않습니다. 링크는 어느 쪽에서든 시작할 수 있습니다. 아래 순서대로 Home에서 시작하거나, 이어지는 "이 컴퓨터를 Child로 연결하기" 절처럼 Child에서 시작합니다.
+비밀번호 SSH는 지원하지 않습니다. Home 또는 Child에서 링크를 시작할 수 있습니다. Windows는 동일하게 고정된 OpenSSH 연결을 통해 PowerShell을 실행합니다.
## `#remote`에서 Child 추가하기
@@ -99,7 +99,7 @@ ocx link reconnect --link-id [--json]
OpenCodex가 백그라운드 서비스로 실행 중이어도 기존 `ocx gui pair --origin <로컬 주소>` 명령을 사용할 수 있습니다. 서비스의 개인 OpenCodex 폴더를 소유한 계정으로 명령을 실행하고, 반환된 일회용 코드를 로컬 대시보드에 입력하세요. 서버 터미널, 추가 비밀번호, Windows Hello 또는 하드웨어 보안키가 필요하지 않습니다. CLI와 서버는 같은 최신 버전과 설정 폴더를 사용해야 하며, 업데이트 뒤에는 이전 서버를 재시작하세요. Hub 초대도 CLI와 서버의 버전이 다르면 함께 업데이트해야 합니다. 설정 폴더나 일회용 승인 폴더가 심볼릭 링크이면 페어링을 거부하므로, `OPENCODEX_HOME`에는 보호된 실제 폴더 경로를 지정하세요.
-CLI는 짧게 유효한 일회용 승인 정보를 만들고, 사용하거나 실패하면 정리를 시도합니다. 실행 정보를 읽는 것만으로는 승인받을 수 없고, 검증에 필요한 원래 비밀값은 파일에 저장하지 않습니다. 이 방식은 개인 폴더의 쓰기 권한을 확인하는 것으로, 생체 인증이나 사람의 직접 승인을 증명하지는 않습니다. 사용자 파일을 자유롭게 수정할 수 있는 프로그램은 여전히 신뢰 범위 안에 있습니다. 기존 Hub 초대의 권한 정책은 바뀌지 않습니다. 페어링 요청이 실패하면 안내된 문제를 해결한 뒤 새 명령을 실행하세요. Windows의 SSH Remote Link 지원을 추가하는 변경은 아닙니다.
+CLI는 짧게 유효한 일회용 승인 정보를 만들고, 사용하거나 실패하면 정리를 시도합니다. 실행 정보를 읽는 것만으로는 승인받을 수 없고, 검증에 필요한 원래 비밀값은 파일에 저장하지 않습니다. 이 방식은 개인 폴더의 쓰기 권한을 확인하는 것으로, 생체 인증이나 사람의 직접 승인을 증명하지는 않습니다. 사용자 파일을 자유롭게 수정할 수 있는 프로그램은 여전히 신뢰 범위 안에 있습니다. 기존 Hub 초대의 권한 정책은 바뀌지 않습니다. 페어링 요청이 실패하면 안내된 문제를 해결한 뒤 새 명령을 실행하세요. Windows의 SSH Remote Link는 위에서 설명한 별도의 PowerShell 실행 경로를 사용합니다.
페어링 도중 프로세스가 중단되면 명령을 다시 실행해 새로운 승인을 받으세요. 서비스나 페어링 명령이 실행 중일 때는 페어링 상태 파일을 수동으로 제거하지 마세요.
diff --git a/docs-site/src/content/docs/ko/guides/sidecars.md b/docs-site/src/content/docs/ko/guides/sidecars.md
index 9716fc27937..eb6ec6a6630 100644
--- a/docs-site/src/content/docs/ko/guides/sidecars.md
+++ b/docs-site/src/content/docs/ko/guides/sidecars.md
@@ -158,3 +158,9 @@ OpenCodex가 `web_search` 가로채기를 멈추고 Codex 통합이 `~/.codex/co
파일을 직접 고치고 싶다면 이전처럼 `config.json`에서 `enabled`를 `false`로 두면 됩니다. Anthropic OAuth 검색과 이미지 설명은 기존 Claude Code OAuth
fingerprint 선례를 따르지만, 실제 계정과 작업량으로 충분히 soak test하는 편이 좋습니다. 전체
필드는 [설정 레퍼런스](/ko/reference/configuration/server/#sidecars)를 참고하세요.
+
+검색 응답의 선택 필드 형식이 잘못되어도 유효한 답변과 출처는 유지합니다. 오류 메시지 값의 형식이 잘못되면 일반 실패 메시지를 표시합니다.
+
+검색 답변의 Sources 목록에서 URL과 짝을 이루지 못한 글머리표는 출처로 삭제하지 않고 답변에 남깁니다. 제목 다음 줄에 해당 URL이 있으면 하나의 출처로 처리합니다. `mode:fast`처럼 인라인 코드 안의 비 HTTP URI 형태 텍스트는 답변에 남기며, 코드로 표시한 HTTP(S) URL은 출처로 처리합니다.
+
+긴 검색어와 답변은 UTF-16 서로게이트 쌍을 나누지 않는 위치에서 줄이므로, 결과 길이 제한 때문에 😀 같은 한 글자가 반으로 잘리지 않습니다. 다만 제로 폭 결합자(ZWJ)로 이어진 이모지처럼 여러 글자로 이루어진 조합은 중간에서 잘릴 수 있습니다.
diff --git a/docs-site/src/content/docs/ko/guides/sub-agent-surface.md b/docs-site/src/content/docs/ko/guides/sub-agent-surface.md
index 3b1fb5ab0c5..baece5604ce 100644
--- a/docs-site/src/content/docs/ko/guides/sub-agent-surface.md
+++ b/docs-site/src/content/docs/ko/guides/sub-agent-surface.md
@@ -57,7 +57,7 @@ Codex는 `spawn_agent` 도구 설명에 모델을 최대 5개까지 나열하며
| 3 | `deepseek/deepseek-v4-flash` | `5` | 알파벳 순으로 첫 번째인 남은 라우팅 행 |
| 4 | `google/gemini-3-pro` | `5` | 다음 라우팅 행 |
| 5 | `moonshot/kimi-k3` | `5` | 다음 라우팅 행 |
-| 6+ | `zai/glm-5.2`, 그다음 네이티브 GPT 행 | `5`, 그다음 `102` | 5개 밖 |
+| 6+ | `zai/glm-5.2`, 그다음 네이티브 GPT 행 | `5`, 그다음 `102 + sourcePriority` | 5개 밖 |
로스터가 비어 있으면 opencodex는 아무것도 고정하지 않고, 5개는 카탈로그의 원래 priority를 따릅니다.
diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md
index d014bf6f192..d97002b856b 100644
--- a/docs-site/src/content/docs/ko/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md
@@ -13,7 +13,7 @@ opencodex는 프록시가 제공하는 로컬 웹 대시보드(`gui/` 아래의
ocx gui
```
-브라우저에서 `http://localhost:`를 엽니다. 허브에서 관리 리스너를 켜 두면 `http://127.0.0.1:<관리 포트>`를 엽니다. 프록시가 꺼져 있으면 먼저 자동으로 시작합니다.
+브라우저에서 실행 중인 프록시의 주소를 엽니다. 기본값은 `http://127.0.0.1:`이고, IPv6 루프백 `::1`에 바인딩했다면 `http://[::1]:`입니다. 이 리터럴 주소는 로컬 브라우저 페어링에 필요한 설정된 origin을 유지합니다. 허브에서 관리 리스너를 켜 두면 `http://127.0.0.1:<관리 포트>`를 엽니다. 프록시가 꺼져 있으면 먼저 자동으로 시작합니다.
개발 중에는 실행 중인 프록시와 GUI 개발 서버를 따로 띄울 수 있습니다.
```bash
@@ -27,6 +27,12 @@ bun run dev:gui
원격 대시보드는 표준 비밀번호 폼을 표시하므로 브라우저 비밀번호 관리자가 토큰 저장과 자동 완성을 제안할 수 있습니다. 대시보드 자체는 토큰을 메모리에만 보관하며 `localStorage`나 `sessionStorage`에 쓰지 않습니다. 저장 여부는 전적으로 브라우저 또는 비밀번호 관리자가 결정합니다.
+**이 기기 기억하기**는 기본적으로 꺼져 있습니다. 직접 HTTPS 대시보드(또는 로컬 루프백)에서 관리자 토큰을 서버 발급 HttpOnly, SameSite=Strict 기기 쿠키와 이 사이트의 localStorage에 저장하는 별도의 기기 증명으로 교환합니다. HTTPS 쿠키는 Secure이며 해당 호스트에만 적용됩니다. 쿠키와 증명은 모두 관리자 토큰이 아니며, 어느 쪽도 단독으로 작동하지 않습니다. 같은 호스트명의 여러 포트는 쿠키를 공유하므로, 오리진별 저장소의 증명이 기억한 기기를 이 대시보드 오리진에 묶습니다. 그래도 전용 호스트명을 권장합니다. 관리자 토큰은 localStorage나 sessionStorage에 저장하지 않습니다. 브라우저 저장소를 사용할 수 없거나 증명 저장에 실패하면 기억하기는 사용 불가로 표시됩니다.
+
+비공개 dashboard-sessions.json에는 쿠키·증명·관리자 인증 정보·브라우저 식별 해시, 서버 오리진과 고정 30일 만료를 저장합니다. Safari 홈 화면 앱을 다시 열면 유효한 기기 기록으로 단기 세션을 재개합니다. 기억한 세션으로는 GitHub 스타, 리셋 크레딧 소비, 프롬프트 계층 편집, Meta Muse 로그인, 데스크톱 호환성 변경, 할당량 검증에 동의할 수 없습니다. 이 작업에는 새 로컬 세션이나 페어링한 세션을 사용하세요. 릴레이·다른 오리진·원격 평문 HTTP에서는 기억하기를 지원하지 않습니다. 만료, 브라우저 식별 변경, 관리자 토큰 교체 후 서버 재시작 시 다시 로그인해야 합니다.
+
+사이드바나 모바일 작업의 **이 기기 잊기** 또는 로그아웃이 성공하면 기기 기록과 모든 파생 세션을 즉시 철회하고 브라우저의 증명을 삭제합니다. 로그아웃 실패 시에는 재시도할 수 있도록 증명을 유지합니다. 서버는 변경을 게시하기 전에 기록 내용을 디스크에 동기화하지만, 충돌 후 디렉터리 항목의 내구성은 최선 노력 수준입니다. 이전 토큰 저장 키는 읽거나 마이그레이션하지 않고 삭제합니다. 신뢰하는 기기에서만 사용하세요. Safari 자동 완성을 복원하는 기능은 아닙니다.
+
## 사용량 요약 바
시작 안전성 페이지를 제외한 모든 페이지 상단의 한 줄 요약에 프로바이더별 현재 할당량 사용률이 표시됩니다. 예: `OpenAI 31% | Claude 54% | xAI 12% | Google 8%`. 프로바이더 작업 화면과 같은 할당량 보고(`GET /api/provider-quotas`)를 탭이 보이는 동안 60초마다 읽으며, 업스트림 강제 새로고침은 하지 않습니다.
diff --git a/docs-site/src/content/docs/ko/index.mdx b/docs-site/src/content/docs/ko/index.mdx
index f1182e8c0df..471cafd7070 100644
--- a/docs-site/src/content/docs/ko/index.mdx
+++ b/docs-site/src/content/docs/ko/index.mdx
@@ -1,10 +1,10 @@
---
-title: "opencodex — Codex를 어떤 LLM 위에서든"
-description: OpenAI Codex & Claude Code를 위한 범용 프로바이더 프록시 — Codex CLI, App, SDK와 Claude Code에서 어떤 LLM이든 사용하세요.
+title: "opencodex — Codex, Claude Code 등을 잇는 로컬 게이트웨이"
+description: "AI 코딩 도구와 모델을 잇는 하나의 로컬 게이트웨이: Codex, Claude Code, Claude Desktop, Grok Build에서 원하는 프로바이더를 쓰고, 관리형 설정으로 더 많은 도구를 연결하세요."
template: splash
head:
- tag: title
- content: "opencodex — Codex를 어떤 LLM 위에서든"
+ content: "opencodex — Codex, Claude Code 등을 잇는 로컬 게이트웨이"
- tag: meta
attrs:
property: og:locale
diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md
index 546a0dea6eb..cce5366d22c 100644
--- a/docs-site/src/content/docs/ko/reference/adapters.md
+++ b/docs-site/src/content/docs/ko/reference/adapters.md
@@ -23,6 +23,13 @@ interface ProviderAdapter {
스트림으로 표현할 수 없는 전송 방식을 지원합니다. 이후
[`bridge.ts`](/ko/reference/architecture/#브리지)가 이벤트를 Responses SSE로 바꿉니다.
+## 공통 도구 인자 보정
+
+브리지는 도구 스키마에 따라 정수 필드의 정수형 실수와 문자열 전용 필드의 안전한 정수를
+보정합니다. 안전한 정수 범위를 벗어난 값, 오버플로 숫자, 보정하지 않는 정수가 아닌 소수는 원래 토큰을 정확히 유지하면서
+다른 안전한 필드는 계속 보정합니다. 중첩 배열과 중복 키의 unsafe 토큰도 모두 보존하므로,
+타임아웃 보정이 관련 없는 식별자를 조용히 바꾸지 않습니다.
+
## `openai-chat`
**대상:** OpenAI **Chat Completions**(`POST {baseUrl}/chat/completions`)와 모든 호환 프로바이더
@@ -147,6 +154,8 @@ Ollama 공급자에 `adapter: "ollama-native"`로 설정할 수도 있습니다.
block을 보냅니다.
- `application/vnd.amazon.eventstream`을 디코딩해 text/thinking/tool 이벤트를 복원하고, 잘린 툴
JSON을 감지합니다. 업스트림이 토큰 수를 반환하지 않아 사용량은 추정합니다.
+ 디코더는 종료 시 전송 계층 정리 완료를 기다리지 않고 취소를 시작하므로, 정리 작업이
+ 멈춰도 읽기를 중단한 소비자가 디코더 종료를 계속 기다리지 않습니다.
- `fetchResponse`에서 제한된 횟수만 재시도하고 오류를 분류/마스킹합니다. 비스트리밍 파서는 웹 검색
루프를 위해 같은 이벤트 스트림을 끝까지 소비합니다.
### 완료와 네이티브 stop reason
diff --git a/docs-site/src/content/docs/ko/reference/cli.md b/docs-site/src/content/docs/ko/reference/cli.md
index 3abe8fbd115..26c7c0ff0a7 100644
--- a/docs-site/src/content/docs/ko/reference/cli.md
+++ b/docs-site/src/content/docs/ko/reference/cli.md
@@ -17,7 +17,9 @@ opencodex CLI는 `ocx`입니다. 첫 번째 명령 이름으로 분기하며, `s
관리 명령은 기록된 런타임 포트와 신원 검사를 사용해 살아 있는 프록시의 management API와 왕복 통신하며, 두 번째 설정 경로를 따로 두지 않습니다. 멈췄거나 닿을 수 없는 프록시는 HTTP 503으로 표시되며 CLI는 0이 아닌 종료 코드를 반환합니다. 명시적으로 오프라인 설정 작업으로 문서화된 명령은 라이브 프록시 없이 설정 파일을 검증하고 수정할 수 있습니다.
-`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
+`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows x64는 증명에 바인딩된 절대 표준 npm `codex.cmd`와 보유 핸들로 바인딩된 Node/npm 도구체인(있으면 프리픽스 로컬, 없다고 확인되면 런처 아키텍처에 고정된 캡처된 PATH 쌍), 검증된 인프리픽스 플랫폼 백킹도 지원하며 reparse를 거부하는 보유 핸들을 사용합니다. 다른 Windows 레이아웃은 계속 연기되거나 관리되지 않습니다. Windows의 `managed` 결과는 관측된 설치 레이아웃만 분류하며, 바인딩된 Node/npm은 업데이터의 읽기 전용 메타데이터 도구체인일 뿐 이후 `codex.cmd` 호출이 어떤 인터프리터를 해석할지를 보증하지 않습니다. 검증된 ACL 경로와 관찰 한도 안의 공유 prefix에서는 명시적 apply를 허용하고, 무관한 패키지·shim의 바이트와 신원 보존을 확인하며, prefix 관찰 한도는 4,096항목·총 512 MiB·파일당 384 MiB입니다. 지원하지 않는 경로와 ARM64는 거절합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. 적용 조건을 충족하지 않는 Windows 설치는 `applicable: false`와 `refusal: windows_apply_disabled`를 가진 결정적 읽기 전용 plan을 생성하며, PATH 전용 선택·OpenCodex 자동 시작 래퍼·global/vendor 폴백은 계속 연기됩니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
+
+Windows plan은 npm 패키지 전체와 모듈 조회 경로의 쓰기 권한도 검증합니다. 선택된 Node는 동기 모듈 훅을 지원해야 하며, npm은 검증된 트리 밖의 의존성을 실행할 수 없습니다. Apply는 설치 프로세스 종료와 readback까지 선택 변경과 소유 세션 시작을 busy로 거절하고, 자식 수명이 불명확하면 이 배제를 유지합니다.
Windows x64 설치 관측은 [`attest` 명령](/ko/reference/cli/agents/)을 참조하세요. 명시적 경로 없이 증명에 바인딩된 런처 스냅샷이 식별한 선택 후보를 관측하며, 업데이트 권한이나 런타임 선택을 증명하지 않습니다.
diff --git a/docs-site/src/content/docs/ko/reference/cli/agents.md b/docs-site/src/content/docs/ko/reference/cli/agents.md
index c8bb7e53731..e6186944fb4 100644
--- a/docs-site/src/content/docs/ko/reference/cli/agents.md
+++ b/docs-site/src/content/docs/ko/reference/cli/agents.md
@@ -162,6 +162,39 @@ ocx claude desktop import [--apply] Validate and import JSON
family는 `opus`, `fable`, `sonnet`, `haiku`이며, 새 route는 `opus`에서 시작합니다. `none`은 해당 family가 비어 있을 때만 유효합니다. 레거시 apply 플래그인 `--static`, `--hybrid`, `--discovery-only`도 계속 지원합니다. Claude Code 설정은 `ocx claude config ...`를 사용하십시오.
+### `ocx codex [codex args...]`
+
+키는 설정과 해당 경로가 다른 보안 주체의 변경으로부터 보호될 때만 전달합니다. 현재 사용자와 권한 있는 OS 주체는 신뢰합니다. 같은 사용자로 실행되는 프로세스는 신뢰 대상입니다. 사용자의 쓰기 권한을 가진 행위자는 사용자의 OpenCodex 키 파일도 이미 읽을 수 있습니다. 검증과 네이티브 시작 사이에 같은 사용자가 설정을 교체하는 시나리오는 이 위협 모델의 범위 밖이며, 런처는 인계 시 설정을 고정하지 않습니다.
+
+일반적인 Windows 설치의 기본 ProgramData 권한은 다른 사용자에게 디렉터리 생성 권한을 줍니다. 따라서 `ProgramData\OpenAI\Codex`가 없어도 자동 자격 증명을 보류합니다. ProgramData 아래의 시스템 Codex 설정 폴더는 존재해야 하고 Administrators/SYSTEM만 쓸 수 있어야 하며, 다른 사용자가 상위 디렉터리를 교체할 수 없어야 합니다. 관리자에게 디렉터리 정책 검토를 요청하세요. opencodex는 직접 ACL을 변경하지 않으며 프로비저닝 명령을 제공하지 않습니다.
+
+대신 사용자가 직접 키를 제공할 수 있습니다. 예를 들어 `OPENCODEX_API_AUTH_TOKEN` 환경 변수를 내보내면 `ocx codex`는 그 값을 그대로 유지합니다. 기존 시스템 설정 파일은 여전히 자동 자격 증명을 보류하며 별도 검토가 필요합니다. 각 보류 사유는 키 값, 키 파일 내용이나 경로 없이 짧고 구체적인 stderr 진단 한 줄로 표시됩니다.
+
+한 단어 프롬프트 앞에는 `--`를 넣어 알 수 없는 Codex 하위 명령 선택을 막으세요.
+
+설정 파일이 없어도 machine 설정 경로에 다른 사용자가 파일을 만들 수 없어야 합니다. Windows ProgramData/OpenAI/Codex 경로의 보호를 증명할 수 없으면 자동 토큰을 보류하며 디렉터리를 만들지 않습니다. macOS는 거부 전용 ACL과 읽기 전용 허용 항목을 수락합니다. 쓰기·추가·삭제·속성·보안·소유자 변경을 허용하는 항목은 앞에 거부 항목이 있어도 거부합니다. 알 수 없는 태그나 검사 오류도 거부합니다.
+
+새 서비스 토큰은 argv가 아래 허용 목록 안에 있고, 비어 있지 않은 HTTP(S)/ALL proxy 및 `CODEX_*`/`OPENAI_*`/`AZURE_OPENAI_*` 환경 재정의가 없으며(정규 `CODEX_HOME`은 허용), 홈·설정·시스템 경로가 소유권 검사를 통과할 때만 읽습니다. 설정에는 검증된 키와 정확한 관리 loopback provider만 있어야 하며 추가 자격 증명 소비자나 설정층이 없어야 합니다. `-c`/`--config`, `-p`/`--profile`, 알 수 없는 플래그, `resume`/`fork` 등 허용 목록 밖의 인자는 토큰을 보류하고 값 없는 진단을 출력합니다. 명시적인 사용자 자격 증명과 원래 인자는 유지합니다.
+
+정규 홈 디렉터리와 `config.toml`은 현재 사용자 소유여야 합니다. POSIX에서는 group/other 쓰기를, Windows에서는 현재 사용자·SYSTEM·Administrators 이외의 DACL 쓰기 권한을 거부하고 소유자 SID를 확인합니다. 상위 경로 교체 권한도 확인합니다. macOS에서는 강제 Codex 설정 preference가 없어야 합니다. 같은 사용자나 관리자의 악성 프로세스로부터 격리하는 기능은 아닙니다.
+
+| Allowlist | Flags |
+| --- | --- |
+| Switches | `--no-daemon`, `--json`, `--ephemeral`, `--skip-git-repo-check`, `--no-alt-screen`, `--full-auto`, `--dangerously-bypass-approvals-and-sandbox`, `--search` |
+| One value | `--model`/`-m`, `--sandbox`/`-s`, `--ask-for-approval`/`-a`, `--image`/`-i` |
+
+
+
+자동 자격 증명 전달은 `CODEX_HOME`의 `~`와 공백을 리터럴 경로로 해석하고 검증한 뒤, 동일한 정규 절대 경로를 자식에게 전달합니다. 홈을 읽을 수 없으면 토큰을 읽지 않습니다. 대소문자와 관계없이 `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` 중 비어 있지 않은 값이 있으면 `NO_PROXY`와 무관하게 자동 로딩을 건너뜁니다. 거부된 실행과 사용자가 내보낸 자격 증명은 변경하지 않습니다. 새 토큰을 전달할 때 자식의 `NO_PROXY`와 `no_proxy`에 기존 우회 항목을 보존하면서 `127.0.0.1`과 `::1`을 추가합니다. `features.respect_system_proxy`를 켜거나 `--enable`/`--disable`을 전달하면 자동 로딩을 건너뜁니다.
+
+원래 인자로 Codex를 실행하기 전에 프록시 시작을 시도합니다. 라우팅은 Codex 설정이 소유하며 런처가 재작성하지 않습니다. 네이티브 및 다른 게이트웨이 경로에서는 시작을 생략합니다. 알 수 없는 라우팅은 경고 후 시작을 시도하며, 시작 실패 시에도 경고 후 실행합니다. 도움말·버전 및 `login`, `doctor`, `help` 같은 내부 명령은 시작과 자동 토큰 읽기를 생략합니다.
+
+선택한 provider의 전체 전송 설정이 이 설치의 로컬 리스너 또는 저장된 로컬 클라이언트 연결에 대해 생성한 관리 블록과 일치할 때만, 누락된 `OPENCODEX_API_AUTH_TOKEN`을 이 설치의 `service-api-token`에서 읽습니다. 이름이나 주입 주석만으로는 허용하지 않습니다. 알 수 없거나 원격인 목적지, 기본 profile, 프로젝트 설정, `OPENAI_BASE_URL`, 전달된 `-c`/`--config`, `-p`/`--profile`, `-C`/`--cd`, `--remote`는 자동 읽기를 생략합니다. 사용자가 export한 자격 증명과 인자는 보존하며, 토큰 파일 경로 재정의와 관리 토큰 충돌은 거부합니다.
+
+일치가 확인된(`matched`) autostart wrapper만 backing Codex를 직접 실행합니다. 검사 예외를 포함한 알 수 없는 상태는 검증된 bypass가 필요하며, 관리되는 Windows CMD wrapper는 현재 생성 템플릿 전체와 일치해야 `OCX_SHIM_BYPASS=1`로 실행합니다. 일반 npm 런처는 지원합니다. 검증할 수 없거나 오래된 wrapper는 종료 코드 1로 거부하며 Codex를 시작하지 않습니다. `ocx codex-shim install`로 갱신하세요. 이 실행은 shim을 수리하지 않으며, 내용 검사는 Windows backing 파일 출처나 동시 경로 교체를 보장하지 않습니다.
+
+런처 도움말은 `ocx help codex`, Codex 도움말은 `ocx codex --help`입니다. 이후 plain `codex`의 동작은 기존과 같습니다.
+
### `ocx opencode [opencode args...]`
프록시가 실행 중인지 확인한 뒤, OpenCode의 인라인 런타임 계층(`OPENCODE_CONFIG_CONTENT`)에 생성된 `provider.opencodex` 및 `providers.opencodex` 블록을 넣어 opencode를 실행합니다. 기존 인라인 config는 유지되고, 이번 실행에서는 이 두 키만 교체됩니다. 전역 또는 프로젝트 `opencode.json` 파일은 기존 override가 있는지 경고하기 위해 읽을 수 있지만, 디스크상의 파일은 절대 수정하지 않습니다. 라우팅된 model은 `opencodex//`로 나타납니다. 이후 plain `opencode`를 실행하면 이전과 정확히 같은 방식으로 동작합니다.
@@ -256,7 +289,7 @@ ocx system settings --stream-mode eager-relay
ocx system codex-cli-update check --json
```
-`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows는 후보나 설정 파일을 읽지 않고 검사를 연기합니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
+`check`는 레지스트리를 조회하거나 Codex를 실행하지 않습니다. POSIX에서는 배포 런처가 캡처한 환경 후보와 런타임 리졸버가 선택한 정규 경로가 같으면 `selectionAttested: true`가 될 수 있습니다. 연결된 npm-global 패키지 매니페스트가 소유권을 증명하고, 후보 버전이 보고된 경우 그 버전이 매니페스트 버전과 일치하면 `managed: true`가 될 수 있으며 저장된 후보는 인증되지 않습니다. Windows x64는 증명에 바인딩된 절대 표준 npm `codex.cmd`와 보유 핸들로 바인딩된 Node/npm 도구체인(있으면 프리픽스 로컬, 없다고 확인되면 런처 아키텍처에 고정된 캡처된 PATH 쌍), 검증된 인프리픽스 플랫폼 백킹도 지원하며 reparse를 거부하는 보유 핸들을 사용합니다. 다른 Windows 레이아웃은 계속 연기되거나 관리되지 않습니다. Windows의 `managed` 결과는 관측된 설치 레이아웃만 분류하며, 바인딩된 Node/npm은 업데이터의 읽기 전용 메타데이터 도구체인일 뿐 이후 `codex.cmd` 호출이 어떤 인터프리터를 해석할지를 보증하지 않습니다. 직접 Bun/소스 실행은 런처 증거가 없어 후보를 신뢰하지 않습니다. `plan`은 격리된 임시 npm 설정·캐시·로그로 공식 레지스트리를 조회하고 활성 세션이나 불완전한 프로세스 관측을 거절합니다. 설치하거나 프로세스에 신호를 보내지 않습니다. Windows x64 apply는 검증된 개인 쓰기 권한을 가진 제한된 npm prefix를 요구하고 무관한 패키지·shim을 보존하며, 적용 조건을 충족하지 않는 관측 레이아웃은 `applicable: false`와 `refusal: windows_apply_disabled`를 가진 결정적 읽기 전용 plan을 생성합니다. PATH 전용 선택·OpenCodex 자동 시작 래퍼·global/vendor 폴백은 계속 연기됩니다. [명령과 거절 사유의 전체 설명(영문)](/reference/cli/agents/#read-only-codex-cli-update-plan)을 참고하세요.
Windows에서 `CODEX_CLI_PATH=codex` 같은 단순 명령 이름이나 원격 경로·장치 경로가 후보로 캡처되면 `candidate_path_unavailable`을 보고합니다. 후보는 캡처됐지만 해당 경로가 이 검사 대상에 적합하지 않은 경우입니다.
@@ -306,3 +339,19 @@ This takes effect on the **next routed `ocx claude` launch**, injecting `CLAUDE_
Claude Code **2.1.257 or newer** is required for FORCE. Plugin and built-in agents (including Explore/Plan) and per-call model arguments are overridden. Forks and subagent skills with `model: inherit` keep the main conversation model. The main loop and Haiku/small-fast sidecars are unaffected. Existing roster files remain available.
The dashboard warns about old or unknown CLI versions, unavailable targets, and either variable already present in `settings.json` → `env` (which overrides launch env). Detection is read-only and server-local: it cannot inspect another launch shell, another machine, or project-local settings. An unknown result is not proof of force support.
+
+## Hermes / Command Code: model names
+
+Hermes and Command Code support `--model-names alias|routed` (API: `modelNames`).
+The default is `routed`, independent of `preferModelAliasIds`. A connection
+stores its mode; later Apply without the option preserves it. Alias mode uses
+only provider-qualified aliases that the router proves equivalent; missing,
+unsafe or colliding aliases fall back to routed keys. Combos keep their keys.
+Removed/renamed aliases migrate on the next successful managed refresh, with
+foreign-edit refusal. Named Hermes profiles need explicit Apply.
+
+`ocx export --client hermes --model-names alias --json` previews a current
+already loaded catalog without writing client files or managed state; a cold or
+stale catalog returns unavailable. Explicit `--out` writes an export artifact.
+See the [English integration guide](/guides/integrations/#opt-in-alias-model-names)
+for the canonical workflow.
diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
index 23105712070..f1467d731dc 100644
--- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
+++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md
@@ -54,9 +54,13 @@ ocx start --socks5-off
넘어가지 않습니다. 영수증에 근거한 지연 복원도 최종 복원과 영수증 정리를 부모가 담당하며,
부모의 공유 설정 복원이 실패하면 종료 실패로 남기고 미완료 영수증을 보존합니다.
-연결된 hub 클라이언트나 Remote Link Child에서는 성공한 API 종료를 포함해 `ocx stop`이 연결 소유의 Codex 라우팅, 모델 카탈로그와 Grok 설정을 보존합니다. `ocx start`는 라우팅을 다시 주입하지 않고 보존한 설정으로 서비스를 재개합니다. Codex를 자체 계정으로 돌리려면 `ocx disconnect`를, 이미 기본 라우팅으로 복원했다면 다시 연결하려고 `ocx sync --restart-codex`를 실행하세요.
+연결된 hub 클라이언트에서는 성공한 API 종료를 포함해 `ocx stop`이 연결 소유의 Codex 라우팅, 모델 카탈로그와 Grok 설정을 보존합니다. `ocx start`는 라우팅을 다시 주입하지 않고 보존한 설정으로 서비스를 재개합니다. Codex를 자체 계정으로 돌리려면 `ocx disconnect`를, 이미 기본 라우팅으로 복원했다면 다시 연결하려고 `ocx sync --restart-codex`를 실행하세요.
-보존 기능을 확인할 수 없는 오래된 일반 프록시는 서비스나 프로세스를 중지하기 전에 거절합니다. 런타임을 관리하는 앱이나 서비스에서 업데이트·재시작한 뒤 다시 시도하세요.
+Remote Link Child를 명시적으로 중지하면 공유 설정 복원에 성공한 경우 기본 Codex 라우팅, 카탈로그와 백업된 재개 이력 메타데이터를 복원하고 관리형 Grok 설정을 제거합니다. 저장된 Home 연결은 유지하며 `ocx disconnect`로 제거할 수 있습니다. Codex에서 Home을 다시 쓰려면 `ocx start`(서비스가 설치되어 있으면 `ocx service start`) 후 `ocx sync --restart-codex`를 실행하세요. 시작만으로 라우팅을 다시 적용하지 않으며, sync는 Grok 블록을 다시 만들지 않습니다.
+
+서비스 중지 전에 연결 소유권을 확인할 수 없으면 종료 API는 HTTP 409와 `sharedTeardown: "refused"`를 반환하고 프록시와 서비스 관리자를 실행 상태로 둡니다. 진행 중인 연결 또는 연결 해제가 끝난 뒤 다시 시도하세요.
+
+연결된 클라이언트를 중지할 때는 보호된 로컬 런타임 기록과 리스너의 신원 증명을 대조합니다. 클라이언트 리스너와 일반 프록시 모두 이 검증이 필요하며, 일반 프록시는 인증된 조회로 설정 보존 기능도 확인합니다. 오래되었거나 검증할 수 없는 런타임은 서비스나 프로세스를 중지하기 전에 거절합니다. 런타임을 관리하는 앱이나 서비스에서 업데이트·재시작한 뒤 다시 시도하세요.
이미 중지된 클라이언트는 서비스 관리자가 비활성이고 알려진 로컬 주소에 런타임이 없음을 확인한 뒤 정상적인 미실행 결과를 반환합니다.
@@ -443,7 +447,7 @@ Windows 상태 트레이 아이콘을 설치하고 제어합니다. Windows 로
### `ocx gui`
프록시가 실행 중이 아니면 자동으로 시작하면서 [웹 대시보드](/guides/web-dashboard/)를
-`http://localhost:`에서 엽니다. 허브에서 관리 리스너를 켜 두면 `http://127.0.0.1:<관리 포트>`에서 엽니다.
+`http://127.0.0.1:`에서 엽니다. 허브에서 관리 리스너를 켜 두면 `http://127.0.0.1:<관리 포트>`에서 엽니다.
## 업데이트
diff --git a/docs-site/src/content/docs/ko/reference/client-support.md b/docs-site/src/content/docs/ko/reference/client-support.md
new file mode 100644
index 00000000000..a6730e905f0
--- /dev/null
+++ b/docs-site/src/content/docs/ko/reference/client-support.md
@@ -0,0 +1,60 @@
+---
+title: "클라이언트 지원"
+description: "opencodex가 연결하는 코딩 도구, 각 도구의 연결 방식, 사용하는 프로토콜."
+---
+
+
+
+
+opencodex는 AI 코딩 도구와 모델을 잇는 하나의 로컬 게이트웨이입니다. 일부 도구는 직접(네이티브로) 설정하고, 나머지는 로컬 프록시를 가리키는 설정 파일을 내보내거나 관리합니다. 이 페이지는 코드 레지스트리에서 생성되므로 현재 릴리스가 제공하는 내용과 정확히 일치합니다.
+
+## 지원 수준
+
+- **implemented**: 레지스트리에 등록된 클라이언트입니다. 내보내는 설정의 경우 표의 프로토콜은 opencodex가 생성하는 설정에서 읽으며, 바뀌면 생성이 실패합니다. *검토됨* 표시는 설정 자체가 아니라 리뷰에서 나온 특정 엔드포인트를 뜻합니다(네이티브 클라이언트, 설정값을 특정 엔드포인트로 바꾸는 클라이언트). 생성기는 리뷰한 코드가 그대로 있는지 확인합니다. 'OpenAI 호환' 행은 엔드포인트를 단정하지 않습니다.
+- **test-covered**: implemented이면서, 리뷰로 고른 이 클라이언트의 테스트 케이스가 링크되어 있습니다. 생성기는 테스트가 존재하고 비활성화되지 않았는지만 확인하므로, 무엇을 검증하는지는 테스트를 직접 읽어 보세요.
+- **실행 검증**: 특정 시점의 관찰 기록입니다. 표에 적힌 클라이언트 제품·버전을 표에 적힌 opencodex 버전과 함께, 명시한 시나리오로, 그 날짜에 사람이 실행하고 증거를 링크했습니다. 다른 버전이나 제품에 대해서는 아무것도 보장하지 않습니다. 증거는 결과를 추가할 때 리뷰하며, 생성기가 다시 실행하지 않습니다. 대시(—)는 기록된 결과가 없다는 뜻입니다.
+
+## 클라이언트
+
+| 클라이언트 | 네이티브 | 설정 내보내기 | 관리형 파일 연동 | 수신 프로토콜 | 지원 수준 | 실행 검증 | 참고 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| Codex (CLI, app, SDK) | 예 | — | — | Responses (검토됨) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/codex-integration/codex-inject-missing-config.test.ts)) | — | opencodex가 직접 설정하며 내보내는 설정 파일은 없습니다. [가이드](/ko/guides/codex-integration/) |
+| Claude Code | 예 | — | — | Anthropic Messages (검토됨) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/claude-integration/claude-cli.test.ts)) | — | opencodex가 직접 설정하며 내보내는 설정 파일은 없습니다. [가이드](/ko/guides/claude-code/) |
+| Claude Desktop | 예 | — | — | Anthropic Messages (검토됨) | implemented | — | opencodex가 직접 설정하며 내보내는 설정 파일은 없습니다. |
+| Grok Build | 예 | — | — | Responses (검토됨) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/providers/xai/grok-config-inject.test.ts)) | — | opencodex가 직접 설정하며 내보내는 설정 파일은 없습니다. [가이드](/ko/guides/grok-build/) |
+| Qoder | — | JSON | 예 | OpenAI 호환(엔드포인트는 클라이언트가 선택) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/qoder-client.test.ts)) | — | 로컬 리스너 전용. |
+| Qoder CN | — | JSON | 예 | OpenAI 호환(엔드포인트는 클라이언트가 선택) | implemented | — | 로컬 리스너 전용. |
+| WorkBuddy | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/agent-clients.test.ts)) | — | 로컬 리스너 전용. 적용 전에 클라이언트별 검사를 실행하며 거부할 수 있습니다. |
+| WorkBuddy AI | — | JSON | 예 | Chat Completions | implemented | — | 로컬 리스너 전용. 적용 전에 클라이언트별 검사를 실행하며 거부할 수 있습니다. |
+| OpenCode | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | 원격 리스너도 지원합니다(API 키 헤더를 추가). [가이드](/ko/guides/opencode/) |
+| Pi | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | 로컬 리스너 전용. [가이드](/ko/guides/pi/) |
+| Oh My Pi (omp) | — | YAML | 예 | Chat Completions; Responses (네이티브 OpenAI 모델만) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | 로컬 리스너 전용. 자기 YAML 블록만 수정합니다. |
+| Hermes | — | YAML | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/hermes-catalog-refresh.test.ts)) | — | 원격 리스너도 지원합니다(API 키 헤더를 추가). 자기 YAML 블록만 수정합니다. |
+| OpenClaw | — | JSON5 | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/gui/integrations-invariants.test.ts)) | — | 원격 리스너도 지원합니다(API 키 헤더를 추가). |
+| Kimi CLI | — | TOML | 예 | OpenAI 호환(엔드포인트는 클라이언트가 선택) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export-new-clients.test.ts)) | — | 로컬 리스너 전용. |
+| Gajae | — | YAML | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-modality-enum.test.ts)) | — | 로컬 리스너 전용. |
+| DSH | — | YAML | 예 | Responses | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | 로컬 리스너 전용. 클라이언트의 현재 provider 저장소에 씁니다. 자기 YAML 블록만 수정합니다. 설정 파일 옆 잠금 파일로 쓰기를 조율합니다. |
+| MCode | — | YAML | 예 | Anthropic Messages | implemented | — | 로컬 리스너 전용. 설정 파일 옆 잠금 파일로 쓰기를 조율합니다. |
+| ZCode | — | JSON | 예 | Responses (검토됨) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/integrations-current-store.test.ts)) | — | 로컬 리스너 전용. 클라이언트의 현재 provider 저장소에 씁니다. 클라이언트가 이 설정값으로 엔드포인트를 고릅니다(클라이언트 문서 동작 기준으로 리뷰함). |
+| Command Code | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/command-code-client.test.ts)) | — | 로컬 리스너 전용. 설정 파일 옆 잠금 파일로 쓰기를 조율합니다. |
+| Prime | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/prime-client.test.ts)) | — | 로컬 리스너 전용. |
+| Aside | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/aside-profiles-catalog-refresh.test.ts)) | — | 로컬 리스너 전용. |
+| Raycast | — | YAML | 예 | Chat Completions (검토됨) | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/raycast-client.test.ts)) | — | 로컬 리스너 전용. 클라이언트가 이 설정값으로 엔드포인트를 고릅니다(클라이언트 문서 동작 기준으로 리뷰함). |
+| omo | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/omo-client.test.ts)) | — | 로컬 리스너 전용. |
+| Cline | — | JSON | 예 | Responses | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/cline-client.test.ts)) | — | 로컬 리스너 전용. 설정 파일 옆 잠금 파일로 쓰기를 조율합니다. |
+| Kilo Code | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-reasoning-controls.test.ts)) | — | 원격 리스너도 지원합니다(API 키 헤더를 추가). |
+| Factory Droid | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/droid-client.test.ts)) | — | 로컬 리스너 전용. [가이드](/ko/guides/factory-droid/) |
+| Gentle Shell | — | JSON | 예 | Chat Completions | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/gentle-shell-client.test.ts)) | — | 로컬 리스너 전용. |
+| T3 Code | — | JSON | 예 | Anthropic Messages | test-covered ([테스트](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/t3code-client.test.ts)) | — | 로컬 리스너 전용. 적용 전에 클라이언트별 검사를 실행하며 거부할 수 있습니다. |
+
+## 수신 엔드포인트
+
+프록시는 아래 프로토콜을 받습니다. 위 목록에 없는 도구도 이 중 하나로 사용자 지정 엔드포인트를 설정할 수 있으면 연결할 수 있습니다.
+
+- `POST /v1/responses`: Responses
+- `POST /v1/chat/completions`: Chat Completions
+- `POST /v1/messages`: Anthropic Messages
+
+## 이 페이지 최신 상태 유지
+
+이 페이지는 `scripts/generate-client-support-matrix.ts`가 내보내기·관리형 연동·네이티브 연동 레지스트리로부터 생성합니다. 클라이언트를 추가하려면 `scripts/client-support-matrix/metadata.ts`에 항목을 넣고 `bun run docs:clients`를 실행하세요. 커밋된 페이지가 오래되면 `bun run docs:clients:check`가 실패합니다.
diff --git a/docs-site/src/content/docs/ko/reference/configuration.md b/docs-site/src/content/docs/ko/reference/configuration.md
index 4f80b5136a9..3d7a67be175 100644
--- a/docs-site/src/content/docs/ko/reference/configuration.md
+++ b/docs-site/src/content/docs/ko/reference/configuration.md
@@ -27,6 +27,11 @@ opencodex는 지속 설정을 `$OPENCODEX_HOME/config.json`에 저장합니다.
남긴 뒤 기본값으로 시작합니다. 파일이 없어도 새로 설치한 경우의 기본값을 그대로 사용하며, 그것은 단일
`openai` forward 프로바이더입니다.
+로컬 `ocx config set`, `unset`, `import`, 오프라인 `ocx effort`, 커스텀 모델 추가·삭제,
+프로바이더 추가·삭제·기본값 변경은 기존 `config.json`이 유효하지 않거나 읽을 수 없으면
+원본 바이트를 바꾸지 않고 거부합니다. `ocx config validate`로 확인하고 파일을 고친 뒤
+다시 실행하세요. import도 기존 설정 파일이 있다면 읽을 수 있고 유효한 대상 파일을 요구합니다.
+
## 우선순위와 기본값
`config.json`의 유효한 값은 내장 기본값보다 우선합니다. 선택 사항으로 비어 있는 필드는 각 도메인 페이지에
diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md
index 6a0a43ef73b..af503a86dc5 100644
--- a/docs-site/src/content/docs/ko/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md
@@ -62,6 +62,8 @@ GUI에서 등록이나 OAuth 로그인을 마치면 Models 페이지로 이동
| `codexPinnedTransientPolicy?` | `"hold" \| "detour-new-threads"` | `"hold"` | 수동 핀이 저하되면 `hold`는 경고를 남기고 계속 사용하고, `detour-new-threads`는 새 스레드만 다른 계정에 둡니다. 이미 보낸 턴은 다시 보내지 않습니다. |
| `codexWsReuseAcrossTurns?` | `boolean` | `false` | 같은 계정과 스레드의 Codex WebSocket을 턴 사이에 재사용합니다. 기본값은 꺼짐입니다. 켜면 바쁜 소켓을 최대 750ms 기다리고, 모델이나 티어가 바뀌어도 소켓을 최대 2개 유지합니다. |
| `upstreamHostCircuitThreshold?` | `number` | `0` | 네이티브 OpenAI forward Responses와 compact 전송에서 입증된 연결 전 DNS/TCP 실패에 적용하는 선택적 회로 차단 임계값입니다. `0`은 비활성화하며, `1`~`20`은 이 횟수만큼 최종 논리 요청이 실패하면 provider-origin을 30초 동안 차단합니다. 차단 중에는 계정 선택이나 업스트림 전송 전에 `Retry-After`가 포함된 `503`을 반환하고, 시간이 지나면 반개방 요청 하나만 허용합니다. 타임아웃과 HTTP 응답은 집계하지 않으며, HTTP 응답이 하나라도 오면 회로를 닫습니다. Codex Pool 라우팅에서 계정이 고정되지 않은 경우에만 적용되며, `codexAccountMode: "direct"` 및 계정 한정 선택자에서는 동작하지 않습니다. |
+| `maxInboundBodyBytes?` | `number` | `0` | 인바운드 JSON 본문 하나의 디코딩 후 크기를 제한하는 바이트 단위 설정입니다. 생략하거나 `0`이면 기본값 256 MiB를 유지하며, 설정값은 1~512 MiB로 제한됩니다. 변경 후 다시 시작해야 합니다. 초과 시 오류 코드가 `inbound_body_too_large`인 로컬 HTTP 413을 반환합니다. 적용된 한도가 256 MiB를 넘으면 `maxConcurrentInboundBodyBytes`의 공유 게이트도 적용되며, 요청별 한도만 높여도 공유 예산이 늘지는 않습니다. 적용 대상 경로와 자세한 내용은 [인바운드 본문 수용](/ko/reference/inbound-body-admission/)을 참고하세요. |
+| `maxConcurrentInboundBodyBytes?` | `number` | `536870912` (512 MiB) | 바이트 단위의 선택적 최상위 공유 수용 예산입니다. 적용된 `maxInboundBodyBytes`가 256 MiB를 넘을 때만 사용합니다. 생략하거나 `0` 또는 잘못된 값이면 종전 기본값 512 MiB를 사용합니다. 유효한 양의 정수는 실제 사용할 때 512 MiB~2 GiB (`536870912`~`2147483648`)로 제한합니다. 변경 후 다시 시작해야 하며, 같은 프로세스의 모든 리스너가 예산 하나를 공유합니다. 수용량 계산용이며 메모리 사전 할당, RSS 상한 또는 RAM에 따른 자동 조정 기능이 아닙니다. 2 GiB는 운영자가 명시적으로 선택해야 합니다. 설정 예제, 대기열 및 응답 수명에 따른 제약, 기본값으로 파싱 전 512 MiB 전체 업로드를 1건만 수용하는 이유는 [인바운드 본문 수용](/ko/reference/inbound-body-admission/)을 참고하세요. |
| `modelCacheTtlMs?` | `number` | `300000` | 공급자별 `/models` 캐시의 최신성 창입니다. |
| `cacheRetention?` | `"none" \| "short" \| "long"` | Anthropic 퍼스트파티: `"long"`, 그 외 Anthropic 호환 엔드포인트: `"short"` | Anthropic 프롬프트 캐시 정책입니다. 비활성, 5분짜리 임시, 1시간짜리 확장 중 하나입니다. 설정하지 않으면 `api.anthropic.com`(API 키 또는 OAuth 계정 풀) 요청은 1시간 TTL을 쓰고, 호환 게이트웨이와 벤더 엔드포인트는 5분 TTL을 유지합니다. 명시한 값이 항상 우선합니다. |
| `tokenGuardian?` | `OcxTokenGuardianConfig` | 꺼짐 | 선택적 선제 OAuth 갱신과 Codex 계정 워밍업 정책입니다. |
diff --git a/docs-site/src/content/docs/ko/reference/configuration/server.md b/docs-site/src/content/docs/ko/reference/configuration/server.md
index 145679df1e4..bedee7af881 100644
--- a/docs-site/src/content/docs/ko/reference/configuration/server.md
+++ b/docs-site/src/content/docs/ko/reference/configuration/server.md
@@ -15,7 +15,7 @@ description: 리스너, 원격 접근, admission 키, 타임아웃, 저장소,
| `emptyCompletionRetry?` | `boolean` | `false` | 텍스트나 도구 호출이 없는 Responses 턴을, 터미널 이벤트 전에 스트림이 종료된 경우를 포함해 동일한 요청으로 한 번 재시도하도록 선택합니다. 재시도에는 비용이 발생할 수 있습니다. `OCX_EMPTY_COMPLETION_RETRY=0`은 설정을 바꾸지 않고 비활성화하며, combo 및 routed-compaction turn은 제외됩니다. |
| `dropCodexSafetyBuffering?` | `boolean` | `false` | Canonical Codex Responses 응답의 선택적 safety-buffering 헤더 두 개와 SSE 힌트를 제거합니다. 공급자의 안전 정책이나 거절 응답은 바뀌지 않습니다. Native WS 메타데이터와 compact는 제외됩니다. |
| `stallTimeoutSec?` | `number` | `300`(public) / 비활성(local) | 스트림이 끊기기까지 유효한 업스트림 진행이 없는 시간(초, Responses 및 네이티브 Chat). 미설정 시 **로컬** 업스트림(loopback, private, `.local`/`.lan` 이름)은 비활성이 기본이고 공개 업스트림은 300초. 양수 값은 둘 다에 적용(최소 1초), `0`은 무응답 watchdog을 전면 비활성화한다. canonical ChatGPT SSE를 비스트리밍 JSON으로 접는 Responses 요청에는 이 watchdog이 꺼져도 별도의 15분 전체 상한이 남는다. `/v1/responses/compact`의 보류 바디 읽기도 이 예산을 공유하지만 로컬 업스트림에서도 기본은 300초. 명시 값(`0` 포함)이 우선한다. |
-| `connectTimeoutMs?` | `number` | `200000` | 시도별 DNS/TCP/TLS/최종 헤더 기한입니다. 본문 생성 전에 끝납니다. |
+| `connectTimeoutMs?` | `number` | `200000`(public) / 비활성(local) | 요청 전송부터 업스트림이 첫 응답 헤더를 반환할 때까지의 시도별 기한(DNS/TCP/TLS 및 헤더 대기). 본문 생성 전에 끝나며, 본문 스트림을 제어하는 `stallTimeoutSec`와는 무관합니다. 미설정 시 **로컬** 업스트림(loopback, private, `.local`/`.lan` 이름)은 비활성이 기본이고 공개 업스트림은 200초. 양수 값은 둘 다에 적용, `0`(또는 임의의 비양수 값)은 헤더 기한을 전면 비활성화합니다. 비활성 덕분에 로컬 모델이 큰 프롬프트에서 수 분간 prompt prefill을 해도 첫 헤더 시계가 정상 요청을 끊지 않습니다. |
| `shutdownTimeoutMs?` | `number` | `5000` | 진행 중인 turn을 중단하기 전에 허용하는 정상 종료 드레인 기한입니다. |
| `websockets?` | `boolean` | `false` | 클라이언트용 Responses WebSocket 경로를 광고하고 허용합니다. `false`이면 클라이언트는 HTTP/SSE를 사용하며, 적격 canonical ChatGPT 업스트림 WS 최적화는 비활성화하지 않습니다. |
| `corsAllowOrigins?` | `string[]` | `[]` | CORS에서 추가로 허용할 정확한 origin입니다. 루프백 origin은 항상 허용됩니다. `chrome-extension://` 같은 authority 기반 브라우저 확장 origin을 지원하며, `*`는 와일드카드가 아닙니다. Firefox와 Safari는 확장 UUID를 (설치/브라우저 실행 때마다) 새로 만드므로 origin이 바뀌면 항목을 갱신하세요. |
@@ -26,6 +26,7 @@ description: 리스너, 원격 접근, admission 키, 타임아웃, 저장소,
| `serviceHealthRecovery.enabled?` | `boolean` | `false` | 백그라운드 서비스용 선택적 복구입니다. 작은 감시 프로세스가 `/healthz`를 확인하고, 프록시가 살아 있지만 연속 프로브에 약 1분 동안 전혀 응답하지 않으면 종료해 서비스 관리자(작업 스케줄러, launchd, systemd, WinSW)가 다시 시작하게 합니다. 관리되는 서비스 자식 프로세스에서만 시작되며, 멈춘 프로세스가 처리 중이던 요청은 재전송하지 않습니다. 시간당 최대 3회. 재시작이 필요합니다. 자동 복구에는 숫자 바인드 주소(또는 기본 localhost/와일드카드 매핑)가 필요합니다. DNS 바인드 이름은 모니터링하지 않습니다. 기록된 주소 또는 포트가 변경되면 모니터링이 중지됩니다. |
| `codexAutoStart?` | `boolean` | `true` | Codex shim이 Codex를 실행하기 전에 `ocx ensure`를 돌리도록 허용합니다. `false`이면 ensure는 아무 작업도 하지 않습니다. |
| `codexShimAutoRestore?` | `boolean` | `true` | 완료된 외부 Codex 업데이트가 설치된 shim을 교체한 뒤 복원합니다. 환경 변수로 끌 수 있습니다: `OPENCODEX_CODEX_SHIM_AUTO_RESTORE=0`. |
+| `nativeMainRefresh?` | `"codex" \| "opencodex"` | auto | `CODEX_HOME`의 네이티브 메인 ChatGPT 로그인을 갱신할 주체를 고릅니다. 키링 조회 결과는 해당 조회에만 적용됩니다. 토큰이 없으면 해당 자격 증명 준비는 실패하며, 다음 조회가 같은 토큰을 반환해도 사용할 수 있습니다. 영구 거부 상태는 저장하지 않으며 이미 준비된 요청은 완료할 수 있습니다. 파일 기반 동작은 유지됩니다. 자동 선택을 포함한 전체 동작은 [Server fields(영문)](/reference/configuration/server/#server-fields)를 참조하세요. |
| `syncResumeHistory?` | `boolean` | `true` | 되돌릴 수 있는 Codex App history 호환성입니다. 원래 메타데이터는 `ocx stop` / `ocx restore`가 백업하고 복원합니다. |
| `shadowCallIntercept?` | `{ enabled?: boolean; model?: string; sourceModels?: string[] }` | off | 인식된 Codex 보조/섀도 호출을 요청에 설정된 reasoning effort를 유지한 채 선택한 모델로 다시 보냅니다. 기본 source prefix는 `gpt-6-luna`, `gpt-5.6-luna`입니다. 0.144.x 이하의 이전 클라이언트는 `gpt-5.4-mini`를 사용했으며 `sourceModels`로 복원할 수 있습니다. |
| `webSearchSidecar?` | `OcxWebSearchSidecarConfig` | on when usable | 웹 검색 사이드카 옵션입니다. |
@@ -308,3 +309,11 @@ CLI 내부 재시도·도구 턴은 요청 전체 전송 횟수 한도에 따로
압축은 유효한 잔액을 보존하며 계속할 수 있습니다. 실행 중 거부 상태는 재시작 뒤 깨끗한 저널을 읽을 때까지 남습니다.
쓰다 끊긴 마지막 JSON 줄은 기존 복구 규칙을 따릅니다. 세부 조건과 오류 코드는
[영문 원문](/reference/configuration/server/#historical-pool-continuity)을 참조하세요.
+
+## 로컬 업스트림 타임아웃 분류
+
+로컬 분류는 타임아웃 정책이며 네트워크 접근 권한을 부여하지 않습니다. DNS 조회 없이 HTTP(S) 목적지를 분류합니다. 정확한 `localhost`(대소문자 무관, 끝의 점 허용), `0.0.0.0`, IPv4 loopback(127/8), RFC 1918 private 및 link-local(169.254/16), IPv6 loopback·link-local(fe80::/10)·unique-local(fc00::/7), IPv4-mapped 주소, `.local`/`.lan` 이름이 로컬입니다. 다른 DNS 이름은 loopback으로 해석되더라도 공개 기본값을 유지합니다. 잘못된 목적지, 지원하지 않는 스킴, `localhost.example`도 공개 기본값을 유지합니다.
+
+헤더 제한을 꺼도 클라이언트 취소와 상위 전체 기한은 유지됩니다. 명시적인 양수 `stallTimeoutSec`는 로컬 바디 무응답을 제한합니다. 로컬 헤더·stall 설정이 모두 없으면 헤더 이전의 자동 제한은 없습니다. 네이티브 Chat/Messages 로컬 JSON 읽기는 stall 예산을 쓰며 공개 JSON 읽기는 기존 `max(connectTimeoutMs ?? 200000, 5000)`을 유지합니다. Compact 바디의 기본 300초와 이미지/검색 사이드카 예산도 유지됩니다.
+
+선택 설정 `opencodeClientExport`의 `headerTimeoutMs`, `chunkTimeoutMs`, `timeoutMs`는 양수 밀리초 또는 비활성화 값 `false`를 받습니다. 생략 시 OpenCode 기본값을 유지하고 잘못된 필드는 내보내지 않습니다. V1 options와 V2 settings의 provider 전체에 적용됩니다. [느린 로컬 모델](/guides/opencode/#slow-local-models-prefill-timeouts)을 참고하세요.
diff --git a/docs-site/src/content/docs/ko/reference/inbound-body-admission.md b/docs-site/src/content/docs/ko/reference/inbound-body-admission.md
index ec9d5f5f4a2..45ccb681fd7 100644
--- a/docs-site/src/content/docs/ko/reference/inbound-body-admission.md
+++ b/docs-site/src/content/docs/ko/reference/inbound-body-admission.md
@@ -5,22 +5,55 @@ description: 요청 본문 한도를 높였을 때 동시 HTTP 요청, 재시도
[프로바이더 설정 레퍼런스](/ko/reference/configuration/providers/)의 `maxInboundBodyBytes`는 인바운드 JSON 본문 하나의 디코딩 후 최대 크기를 설명합니다. 기본값은 계속 256 MiB이며, 설정 가능한 값은 1 MiB에서 512 MiB 사이입니다. 한도를 바꾼 뒤에는 리스너와 리더가 같은 한도를 사용하도록 프록시를 다시 시작하세요.
+종전 공유 예산 기본값인 512 MiB를 유지해도 아래에서 설명하는 대기열과 응답 수명 동안의 예약 유지에 따른 제약은 남습니다.
+
+## 공유 예산을 명시적으로 선택하기
+
+`maxConcurrentInboundBodyBytes`는 바이트 단위로 지정하는 `config.json`의 선택적 **최상위** 설정입니다. 기본값은 종전과 같은 **512 MiB (`536870912`)**입니다. 생략하거나 `0` 또는 잘못된 값을 지정하면 이 기본값을 사용합니다. 유효한 양의 정수는 실제 사용할 때 **512 MiB~2 GiB (`536870912`~`2147483648`)** 범위로 제한합니다. 설정을 바꾼 뒤에는 프록시를 다시 시작하세요. 같은 프로세스의 모든 리스너가 예산 하나를 공유하며, `appOwnedMemoryBudgetMb`와는 별개입니다.
+
+요청별 한도를 512 MiB로 정하고 공유 예산을 2 GiB로 명시적으로 늘리려면, 다음 필드를 기존 `config.json`의 최상위에 추가한 뒤 다시 시작하세요.
+
+```json
+{
+ "maxInboundBodyBytes": 536870912,
+ "maxConcurrentInboundBodyBytes": 2147483648
+}
+```
+
+이 설정은 순간 최대 메모리 사용량이 커질 수 있음을 고려한 운영자의 선택입니다. 프록시는 사용 가능한 RAM을 자동으로 감지하거나 그에 맞춰 예산을 정하지 않습니다.
+
## 한도를 높이면 동시 처리량이 달라집니다
-적용된 한도가 256 MiB를 넘으면 해당 HTTP 요청마다 설정된 허용량 전체를 프로세스 전체가 공유하는 512 MiB 수용 예산에서 예약합니다. 따라서 여러 리스너가 한 프로세스를 공유하더라도 한 번에 요청 하나만 실행할 수 있습니다. 본문이 작아도 허용량 전체를 예약하며, 작은 Content-Length 값이나 압축으로 수용 검사를 우회할 수 없습니다. 예약은 요청 JSON 파싱 직후가 아니라 응답 스트림이 끝나거나 실패하거나 취소가 마무리될 때까지 유지됩니다.
+적용된 한도가 256 MiB를 넘으면 해당 HTTP 요청은 **본문을 읽기 전에** 적용된 요청별 허용량 전체를 예약해야 합니다. 예약은 프로세스 전체의 `maxConcurrentInboundBodyBytes` 예산을 공유하며, 기본값은 계속 **512 MiB**입니다.
+
+크기를 제한하면서 JSON 파싱을 완료하기 전까지는 `Content-Length`가 작거나 압축된 전송 본문이 작아도 예약량을 줄이지 않습니다. 이 값들만으로 디코딩 후 크기나 정규화된 JSON 크기를 판단하지 않습니다.
+
+제한된 크기 안에서 JSON 파싱에 성공하면 리더는 실제 전송 바이트 수와 디코딩 후 바이트 수 중 큰 값으로 예약량을 줄입니다. 변환기가 정규화된 JSON 측정에 쓸 예산을 제공한 경우에는 기존 정규화 JSON 측정값까지 포함해 가장 큰 값을 사용합니다. 예약량은 처음 예약한 허용량 범위 안에서 줄어들기만 하며, 정규화 측정값이 그보다 커도 늘어나지 않습니다. 기존 UTF-8 및 JSON 재직렬화 검사는 유지되며, 입력 바이트 길이로 더 클 수 있는 정규화 JSON 크기를 대신하지 않습니다.
+
+**기본 공유 예산 512 MiB**와 **요청별 한도 512 MiB**를 사용하면, 아직 파싱하지 않은 업로드는 **1건만** 허용량 전체를 예약할 수 있습니다. 파싱 후 예약량이 **1바이트**까지 줄어도 남는 용량은 512 MiB보다 작습니다. 따라서 충분한 용량이 반환될 때까지 다음 요청은 512 MiB 전체를 예약할 수 없습니다. 기본값만으로는 요청별 한도가 512 MiB일 때의 원래 동시 처리 문제를 **해결하지 못합니다**.
+
+`maxConcurrentInboundBodyBytes`를 `2147483648`로 명시하면 **2 GiB**를 선택하며, 파싱 및 측정에 따른 예약량 축소 전에도 **512 MiB 전체를 예약하는 업로드를 최대 4건** 수용할 수 있습니다. 이 2 GiB 예산이 파싱 후까지 동시 요청 수를 4건으로 고정하지는 않습니다. 예를 들어 먼저 파싱된 요청의 예약량이 줄어 다음 요청을 수용할 여유가 생기면, 공유 예산과 다른 제한 안에서 **아주 작은 요청 8건도 동시에 응답을 스트리밍**할 수 있습니다.
+
+줄어든 예약량은 비동기 작업과 프로바이더 응답 대기 중에도 유지되며, 응답 스트림의 끝(EOF), 오류 또는 취소 완료 시점에 해제됩니다. 파싱만 끝났다고 해제하지 않습니다. 클라이언트 연결이 끊겨도 응답 생산자의 취소 처리가 끝날 때까지 유지합니다. 오류·요청 중단·취소가 겹치더라도 해제는 정확히 한 번만 일어납니다.
적용 대상 POST 엔드포인트는 `/v1/responses`, `/v1/responses/compact`, `/v1/chat/completions`, `/v1/messages`, `/v1/messages/count_tokens`, `/v1/images/generations`, `/v1/images/edits`, `/v1/alpha/search`입니다. 이미지, 검색, 토큰 계산에는 기존의 설정 가능한 본문별 한도가 계속 적용됩니다. 내부 직접 변환 호출과 콤보 호출은 HTTP 허용량을 두 번 예약하지 않습니다. 관리 API, 오디오, 컨텍스트 기록, WebSocket의 한도는 그대로입니다.
-설정을 생략하거나 0으로 두거나 적용된 한도가 256 MiB 이하라면 이 추가 동시 처리 게이트는 켜지지 않습니다. 기존 요청 수 제한과 다른 자원 제한은 계속 적용됩니다.
+이 추가 동시 처리 게이트는 적용된 `maxInboundBodyBytes`가 256 MiB를 넘을 때만 켜집니다. 이 요청별 설정을 생략하거나 `0`으로 두면 기본값인 256 MiB가 유지되어 게이트가 꺼져 있습니다. 반면 `maxConcurrentInboundBodyBytes`를 생략하거나 `0`으로 두면 공유 기본값인 512 MiB를 선택하며, 이미 켜진 게이트를 끄지는 않습니다. 기존 요청 수 제한과 다른 자원 제한은 계속 적용됩니다.
## 일시적인 거부와 크기 초과 본문의 차이
-허용량을 일시적으로 사용할 수 없으면 프로토콜 파싱이나 프로바이더 호출 전에 HTTP **503**, `Retry-After: 1`, 오류 코드 `server_busy`를 반환합니다. Messages 및 토큰 계산 클라이언트에는 유형이 `overloaded_error`인 Anthropic 형식 오류를, OpenAI 호환 클라이언트에는 `server_error`를 반환합니다. 재시도 헤더와 클라이언트의 백오프를 지키면서 현재 요청이 끝난 뒤 다시 시도하세요. 본문 한도를 더 높여도 허용량 부족은 해결되지 않습니다.
+허용량 전체를 바로 예약할 수 없으면 **최대 32건의 요청이 FIFO 대기열**에서 **최대 120초** 기다릴 수 있습니다. 대기 중에는 애플리케이션이 요청 본문을 읽지 않습니다. 측정 후 예약량이 줄거나 요청이 끝나 예산이 반환되면 대기 순서대로 요청을 수용합니다. 대기 중 요청이 중단되면 대기열에서 제거하고, 읽지 않은 본문을 소비하지 않고 취소합니다.
-요청별 한도보다 큰 본문은 기존 HTTP **413** 처리 경로를 따릅니다. 선언된 크기가 한도를 넘는 경우에도 일시적 거부가 아니라 그 경로를 사용합니다. 연결이 끊기면 기존 취소 동작을 유지합니다. 인증과 출처 검사는 이 동시 처리 게이트보다 먼저 실행됩니다.
+대기열이 가득 찼거나 대기 시간이 만료되면 본문 파싱이나 프로바이더 호출 전에 HTTP **503**, `Retry-After: 1`, 오류 코드 `server_busy`를 반환하고, 읽지 않은 본문을 취소합니다. Messages 및 토큰 계산 클라이언트에는 유형이 `overloaded_error`인 Anthropic 형식 오류를, OpenAI 호환 클라이언트에는 `server_error`를 반환합니다. 인증과 출처 검사는 예약 및 대기열 진입보다 먼저 실행됩니다.
+
+재시도할 때는 재시도 헤더와 클라이언트의 백오프를 지키세요. 대기열과 파싱 후 예약량 조정은 불필요한 용량 부족 거부를 줄이지만 모든 503을 없애지는 않습니다. 요청별 본문 한도를 더 높여도 지속적인 용량 부족은 해결되지 않습니다.
+
+요청별 한도보다 큰 본문은 기존 HTTP **413** 처리 경로를 따릅니다. 선언된 크기가 한도를 넘는 경우에도 일시적 거부가 아니라 그 경로를 사용합니다.
## 예산이 측정하는 것
-512 MiB는 수용된 요청별 허용량의 합계이며, **프로세스 메모리가 512 MiB 아래로 유지된다는 보장은 아닙니다**. 디코딩, 문자열, 객체 그래프, 요청 사본, 기타 애플리케이션 상태에 추가 메모리가 듭니다. 기존 UTF-8 및 다시 직렬화한 JSON의 측정 방식은 유지됩니다. 입력 바이트 길이로 더 클 수 있는 정규화된 JSON 크기를 대신하지 않습니다.
+설정된 예산은 요청을 수용하기 위한 계산 예산입니다. 파싱 전에는 허용량 전체를, 파싱 후에는 측정된 예약량을 합산합니다. **2 GiB나 설정된 용량을 미리 할당하지 않으며**, **프로세스 RSS나 파싱된 객체의 힙 크기 상한도 아닙니다**. 디코딩, 문자열, 객체 그래프, 요청 사본, 기타 애플리케이션 상태에 추가 메모리가 들어 실제 메모리 사용량은 예산을 넘을 수 있습니다.
+
+공유 기본값인 512 MiB 대신 2 GiB를 선택하면 예약 가능한 총량이 4배가 됩니다. 요청별 한도가 512 MiB일 때 이 명시적 선택으로 파싱 전 전체 업로드를 1건 대신 최대 4건 수용하므로, 순간 최대 메모리 사용량이 더 커질 수 있습니다. 배포 환경의 여유 메모리를 고려해 예산을 선택하세요. 요청별 한도가 256 MiB 이하라면 공유 예산 설정과 관계없이 이 추가 게이트가 꺼져 있습니다.
-작은 요청을 병렬로 처리한다면 가능한 한 기본 한도를 유지하세요. 긴 기록이 필요한 워크플로에서는 추가 동시 처리 제한을 감수하고 한도를 높이세요.
+가능한 한 요청별 기본 한도를 유지하세요. 긴 기록이 필요한 워크플로에서 한도를 높일 때는 커질 수 있는 순간 최대 메모리 사용량과 대기열의 한도도 함께 고려하세요.
diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md
index ef88ecb76b5..8b9716128bf 100644
--- a/docs-site/src/content/docs/reference/adapters.md
+++ b/docs-site/src/content/docs/reference/adapters.md
@@ -23,6 +23,13 @@ adapter own retries/timeouts, while `runTurn` supports transports that cannot be
HTTP fetch followed by one response stream. [`bridge.ts`](/reference/architecture/#the-bridge)
then turns the events into Responses SSE.
+## Shared tool argument repair
+
+The bridge repairs integral floats in integer fields and safe bare integers in string-only
+fields against the tool's schema. Unsafe integers, overflowing numbers and unrepaired non-integral
+decimals retain their exact tokens while safe siblings are still repaired. Nested arrays and duplicate keys retain every
+unsafe token, so repairing a timeout cannot silently change an unrelated identifier.
+
## External task input on translated Responses routes
Codex task coordination can deliver input as `function_call_output` with nonblank
@@ -230,6 +237,9 @@ WebSocket proxy settings fall back to HTTP/SSE. HTTP fetch-based Responses handl
`config.proxy` or a SOCKS5 `ALL_PROXY` uses the built-in tunnel when `NO_PROXY` does not exempt
the target. Scheme-specific HTTP(S) proxy variables retain their separate native handling;
non-SOCKS `ALL_PROXY` is not a native HTTP fetch route.
+Internal model decisions, compaction responses, combo failover, and native Responses delivery accept valid event-stream media
+types regardless of casing or parameters, including `Text/Event-Stream; charset=UTF-8`. Other explicit
+media types retain their original format even when a parameter mentions `text/event-stream`. Native delivery also identifies JSON by its exact media type; a JSON name inside a parameter does not select the JSON decoder. Its existing missing-header SSE fallback and plaintext V2 event validation still apply.
Noncanonical Responses gateways receive Codex's client-executed `tool_search` declaration as a
collision-safe public function tool. Matching request history and JSON/SSE function calls are
@@ -478,6 +488,8 @@ MiMo model Command Code serves.
caller expecting JSON would otherwise receive prose.
- Decodes `application/vnd.amazon.eventstream`, reconstructs text/thinking/tool events, detects
truncated tool JSON, and estimates usage because the upstream does not return token counts.
+ Decoder teardown initiates cancellation without waiting for transport cleanup to settle, so a
+ stalled cleanup promise cannot hold the decoder open after the consumer stops reading.
- Uses the configured `baseUrl` verbatim when it is custom. A canonical
`runtime.{region}.kiro.dev` URL follows the imported credential's API region; only that canonical
shape is eligible for one budgeted fallback to `q.{region}.amazonaws.com` after an endpoint,
diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md
index d6ba1d76c35..ae34dcac19d 100644
--- a/docs-site/src/content/docs/reference/cli.md
+++ b/docs-site/src/content/docs/reference/cli.md
@@ -152,18 +152,35 @@ manifest can then establish `managed: true` when it proves ownership and, if a c
The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`, and `selectionAttested`.
Inspecting the configured candidate requires a trusted published-launcher context;
a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report
-`candidate_unavailable` on POSIX or `windows_inspection_deferred` on Windows. On Windows this first slice performs no candidate or configuration filesystem I/O:
-only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels;
-every other Windows candidate fails closed. The command does not install or repair software, execute
+`candidate_unavailable` on POSIX or `windows_inspection_deferred` on Windows. Windows x64 also supports
+a proof-captured absolute standard npm `codex.cmd` with a held-bound Node/npm toolchain — prefix-local
+when present, otherwise the captured-PATH pair anchored to the launcher architecture — and a verified
+in-prefix platform backing. It uses reparse-refusing held handles; other Windows layouts remain deferred
+or unmanaged. A `managed` Windows result classifies the observed installation layout only: the bound Node/npm
+is the updater's read-only metadata toolchain, never a claim about which interpreter a later `codex.cmd`
+invocation resolves, while inspection and planning stay read-only; applicability and confirmed apply are decided separately.
+The command does not install or repair software, execute
Codex or npm, control a running process, or write configuration/cache state.
`ocx system codex-cli-update plan --json` adds a read-only update decision for an attested POSIX
npm-global installation. It resolves `latest` through the official npm registry, validates the
exact version and SHA-512 integrity, and refuses active sessions or incomplete process observation.
Registry queries use isolated temporary npm configuration, cache and logs with best-effort cleanup.
-No software is installed and no process is signalled. Windows returns `windows_inspection_deferred`.
+No software is installed and no process is signalled. Windows x64 apply requires a bounded npm
+prefix with verified private write authority and preserves unrelated packages and shims; unsupported observed layouts return
+`applicable: false` and `refusal: windows_apply_disabled`.
+PATH-only selection and OpenCodex autostart wrappers remain deferred.
+Windows planning also requires trusted write authority over npm's complete package tree and module
+lookup directories. Apply keeps selection changes and owned session starts busy through installer
+termination and readback; an unknown child lifetime keeps that exclusion in place.
+The selected Node must support synchronous module hooks; npm cannot load dependencies outside its verified tree.
See [planning details](/reference/cli/agents/#read-only-codex-cli-update-plan).
+For a reviewed applicable npm-global plan, `ocx system codex-cli-update apply --plan-id --yes`
+performs the exact update and returns a durable job. `ocx system codex-cli-update status --json`
+reads its result; use a stable `--request-id` for a lost-response retry. See
+[confirmation and job states](/reference/cli/agents/#confirmed-codex-cli-apply-and-job-status).
+
For Windows x64 installation observation, see [the `attest` command](/reference/cli/agents/#explicit-installation-observation-on-windows-x64). Without explicit paths it observes the selected candidate identified from the proof-bound launcher snapshot; it does not grant update authority or attest runtime selection.
List or status is the default where unambiguous. Use `--json` only on leaves that support it, and
diff --git a/docs-site/src/content/docs/reference/cli/agents.md b/docs-site/src/content/docs/reference/cli/agents.md
index 501f13e7bfb..fa0798d1e3d 100644
--- a/docs-site/src/content/docs/reference/cli/agents.md
+++ b/docs-site/src/content/docs/reference/cli/agents.md
@@ -963,6 +963,39 @@ native-mode flags are rejected; failed runtime save has no local fallback. Apply
is a separate existing Desktop action on its machine, so verify host/context
before choosing it. Rendered models are not proof of native application.
+### `ocx codex [codex args...]`
+
+The key is supplied only when the configuration and its namespaces are protected from other principals; the current user and privileged OS authorities are trusted. Same-user processes are trusted: an actor with your write access can already read your OpenCodex key file. Same-user configuration replacement between verification and native startup is outside this threat model; the launcher does not freeze configuration at handoff.
+
+On a normal Windows installation, default ProgramData permissions allow other users to create directories. Automatic credentials are therefore withheld even when `ProgramData\OpenAI\Codex` is absent. The system Codex configuration folder under ProgramData must exist and be writable only by Administrators/SYSTEM, and its ancestors must prevent replacement by other users. Ask an administrator to review the directory policy. opencodex never changes ACLs itself and provides no provisioning command.
+
+Alternatively, supply the key yourself, for example by exporting `OPENCODEX_API_AUTH_TOKEN`; `ocx codex` leaves it untouched. Existing system configuration files still withhold automatic credentials and require separate review. Each withholding reason produces one short, specific stderr diagnostic without key values, key-file contents or key-file paths.
+
+Use `--` before a single-word prompt so it cannot select an unknown Codex subcommand.
+
+The machine configuration namespace must also prevent other users from creating missing system files. An unprotected Windows ProgramData/OpenAI/Codex namespace withholds automatic credentials; the launcher does not create it. macOS accepts deny-only and read-only allow ACLs, but refuses any allow entry granting write, append, delete, attribute/security changes or ownership changes, even after a deny entry. Unknown tags and inspection errors refuse.
+
+A fresh service token is loaded only when argv is within the allowlist below, the environment has no nonempty HTTP(S)/ALL proxy or `CODEX_*`/`OPENAI_*`/`AZURE_OPENAI_*` overrides (canonical `CODEX_HOME` is allowed), and the home/config/system namespaces pass ownership checks. The configuration must contain only audited keys and the exact managed loopback provider, with no additional credential consumers or configuration layers. Every unsupported argument, including `-c`/`--config`, `-p`/`--profile`, unknown flags and `resume`/`fork`, withholds the token with a value-free diagnostic; explicit user credentials and original arguments are preserved.
+
+The canonical home directory and `config.toml` must belong to the current user. POSIX denies group/other writes; Windows checks owner SID and rejects DACL write grants outside the current user, SYSTEM and Administrators. Ancestor replacement permissions are checked too. macOS additionally requires no forced Codex configuration preferences. These checks exclude other-user writers; they do not isolate a malicious process running as the same user or an administrator.
+
+| Allowlist | Flags |
+| --- | --- |
+| Switches | `--no-daemon`, `--json`, `--ephemeral`, `--skip-git-repo-check`, `--no-alt-screen`, `--full-auto`, `--dangerously-bypass-approvals-and-sandbox`, `--search` |
+| One value | `--model`/`-m`, `--sandbox`/`-s`, `--ask-for-approval`/`-a`, `--image`/`-i` |
+
+
+
+Automatic credential delivery resolves `CODEX_HOME` as a literal path (including `~` and whitespace), verifies that home, and passes its canonical absolute path to the child. An unreadable home skips loading. Any nonempty `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY` variable, in any letter case, also skips loading regardless of `NO_PROXY`; denied launches and user-exported credentials remain unchanged. With a newly loaded token, both child `NO_PROXY` and `no_proxy` include `127.0.0.1` and `::1`, preserving existing bypass entries. Enabling `features.respect_system_proxy` or forwarding `--enable`/`--disable` skips automatic loading.
+
+Ensure the proxy is running, then launch Codex with the original arguments. Routing remains in Codex configuration; the launcher never rewrites it. Native and foreign routes skip startup. Unknown routing warns and attempts startup; a failed startup warns and continues. Help/version and internal commands such as `login`, `doctor`, and `help` skip startup and automatic token loading.
+
+A missing `OPENCODEX_API_AUTH_TOKEN` is read from this installation's `service-api-token` only when the selected provider's complete transport configuration matches the managed block for the configured loopback listener or persisted local client connection. Names and injection comments alone grant no credential access. Unknown or remote destinations, default profiles, project configuration, `OPENAI_BASE_URL`, and forwarded `-c`/`--config`, `-p`/`--profile`, `-C`/`--cd`, or `--remote` options skip automatic loading. User-exported credentials and arguments are preserved; token-file overrides cannot select a different credential, and management-token collisions are refused.
+
+Only a matched autostart wrapper launches its backing Codex directly. Unknown state, including an inspection exception, requires a verified bypass; Windows CMD managed wrappers must match the complete current generated template before the selected absolute command runs with `OCX_SHIM_BYPASS=1`. Ordinary npm launchers remain supported. Unverifiable or obsolete wrappers refuse with exit 1 and start no Codex; run `ocx codex-shim install` to refresh them. This launch does not repair shims. The content check does not establish Windows backing-file provenance or prevent concurrent pathname replacement.
+
+Use `ocx help codex` for launcher help and `ocx codex --help` for Codex help. Launching plain `codex` later retains its existing behavior.
+
### `ocx opencode [opencode args...]`
Ensure the proxy is running, then launch opencode with the generated `provider.opencodex` and
@@ -979,13 +1012,32 @@ Manage and apply the Grok Build model fence.
## Client config export
+Hermes and Command Code export and integration apply/overwrite accept
+`--model-names alias|routed` (JSON API field: `modelNames`). Omission on export
+means `routed`; omission on a later integration Apply preserves its stored mode.
+This is independent of `preferModelAliasIds`. Only a provider-qualified alias
+that routes to the same provider/model is emitted; unsafe, missing or colliding
+aliases fall back to routed names, and combos keep their existing keys.
+
+`ocx export --client hermes --model-names alias --json` previews an existing
+current catalog without writing client files or managed state. A cold/stale
+catalog returns unavailable; load the catalog separately and retry. Explicit
+`--out` writes only the requested export artifact and retains its existing
+exclusive-create/`--force` behavior. For managed writes, pass the same mode to
+both `integration client preview --operation apply|overwrite` and `enable` when
+confirming a plan. The field is unsupported on disable/restore and other clients.
+An alias change during Apply or refresh can cause a conflict before any write;
+retry against the current catalog. Removed/renamed aliases migrate at the next successful refresh, with foreign-edit
+refusal. See [the integration guide](/guides/integrations/#opt-in-alias-model-names).
+
### `ocx export --client `
Print a client config wired to the running proxy. The command serializes the
`opencodex` provider block — base URL, model list, and the client's credential
reference or loopback placeholder — in the selected client's native format.
-The proxy must be running; the command resolves its live port, reads `/api/models`, and emits only
+The proxy must be running; the command resolves its live port, uses the current
+catalog (`/api/models` or the server-side client export), and emits only
models Codex can currently see.
OpenCode and Kilo exports preserve effective model limits, known capabilities, declared reasoning
@@ -1163,9 +1215,12 @@ resolver selects the same canonical environment candidate without executing it.
can then establish `managed: true` when it proves ownership and, if a candidate version is reported, that version matches the manifest; persisted candidates remain unattested. The JSON report exposes `candidateAvailable`, `candidateVersion`, `candidateSource`,
and `selectionAttested`. Inspecting the configured candidate requires a trusted published-launcher context;
a direct Bun/source launch has no such proof, ignores ambient and persisted candidate state, and may report
-`candidate_unavailable` on POSIX. On Windows this first slice performs no candidate or configuration filesystem I/O:
-only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels;
-every other Windows candidate fails closed. Because that slice never consults persisted state, a Windows run
+`candidate_unavailable` on POSIX. On Windows x64, a proof-captured absolute standard npm `codex.cmd`
+can be managed when held local handles bind its package, a prefix-local Node/npm pair or the captured
+PATH metadata pair tied to the launcher architecture, and the first in-prefix platform backing.
+This metadata pair does not attest the interpreter a later CMD invocation would select.
+Plan paths and configured version-manager roots must use their canonical long DOS names;
+aliases, unavailable roots and reparse points leave the installation unmanaged. Other Windows layouts remain deferred or unmanaged. Windows never consults persisted state, so a run
with no captured environment candidate reports `windows_inspection_deferred` rather than `candidate_unavailable`:
the command cannot observe whether a Codex CLI is installed, so it reports the deferral instead of asserting
that no candidate exists. The command does not execute Codex or a package manager, repair a shim,
@@ -1182,18 +1237,86 @@ ocx system codex-cli-update plan --channel latest --json
```
Run through the published Node launcher with the intended `CODEX_CLI_PATH` in the shell environment.
-The plan requires an attested POSIX npm-global installation and a linked package manifest version.
+The plan requires an attested npm-global installation and a linked package manifest version.
+Windows supports only an explicit absolute standard npm `codex.cmd` with a held-bound Node/npm toolchain —
+prefix-local when present, otherwise the captured-PATH pair anchored to the launcher architecture;
+PATH-only selection, OpenCodex autostart wrappers and global/vendor fallback stay deferred.
It queries the official npm registry for an exact version, SHA-512 integrity and same-origin tarball,
using the launcher's captured PATH and isolated temporary npm configuration, cache and logs. Proxy
settings and `NODE_EXTRA_CA_CERTS` remain supported; cleanup is best-effort. No Codex command runs.
An applicable plan reports `applicable: true` and a deterministic `planId` binding platform,
-installation-root digest, installed version, channel and target version/integrity. The plan is never
+installation-root/content digests, installed version, channel and exact main/platform artifacts. The plan is never
persisted. It refuses downgrades, already-current versions, active Codex sessions and incomplete
-process observation. Windows returns `windows_inspection_deferred` before registry/process queries.
+process observation. Supported Windows metadata queries use the observed Node/npm under matching
+held handles. Windows x64 can be applicable only for a bounded npm prefix with verified
+write authority; other observed prefixes retain `windows_apply_disabled`. Active/unknown sessions still refuse earlier. Unsupported
+Windows layouts return `windows_inspection_deferred` before registry/process queries.
A refusal exits 0: inspect `applicable` and `refusal`. The printed install argv is indicative; this
-command installs nothing, changes no application state and never signals a session. No apply command
-is exposed, and a plan ID is not installation authorization.
+command installs nothing, changes no application state and never signals a session.
+A plan ID alone is not installation authorization: apply requires separate explicit confirmation.
+
+#### Confirmed Codex CLI apply and job status
+
+```bash
+ocx system codex-cli-update apply --plan-id <32-hex> --yes --request-id --json
+ocx system codex-cli-update status --json
+```
+
+Apply supports the selected, managed, non-shim npm-global CLI on Linux/macOS and the restricted
+Windows x64 npm layout. It revalidates the
+approved plan under an installation lease, verifies the exact main/platform archives, installs them
+offline with lifecycle scripts disabled, and checks installed files and the selected CLI's version.
+Existing Codex sessions are never stopped; active or unknown process observations refuse the update.
+Windows supports shared prefixes with verified ACLs and no reparse paths. Observation includes
+unrelated packages and shims, capped at 4,096 entries, 512 MiB total and 384 MiB per file. Their
+bytes and identities must be preserved. Larger or unsafe prefixes, ARM64, shim repair, native apps
+and other installation managers remain unsupported. Unknown child lifetime or incomplete handle cleanup retains staging and
+the installation lease; do not retry by deleting its lock. Readback includes auxiliary files,
+new/absent backing roots and npm's hidden lockfile. `not_applied` means scoped installation state
+was observed unchanged, not that npm performed no I/O anywhere.
+Installation directories and staging require trusted POSIX ownership and permissions throughout their
+ancestry. Unsafe installation paths report `installation_prefix_unsupported` or
+`installation_paths_untrusted`; unsafe temporary roots report `staging_path_untrusted`. Use a private
+temporary directory below trusted ancestors (root-owned sticky temporary roots are supported).
+Owned session starts, desktop restarts and runtime/shim selection changes report busy while installation
+and readback run. An unconfirmed installer or verification child keeps admission closed. Admission
+recovers when the owned process group is proven gone, including a verification child that closes after
+its reap deadline. Worker loss before commit also restores admission; worker close after commit alone
+does not. Durable recovery records preserve this exclusion across proxy restarts.
+On Windows, recovery checks the exact installer/verifier Job and original process creation identity;
+all Job members must have exited. Access failures keep admission closed and never stop an installer.
+Missing child identity, partial records or inaccessible liveness evidence require inspection. A complete terminal write left
+before its publication rename can restore admission after restart when its exact owner and current
+parent/group absence are verified. On Windows, a complete pending child identity can also restore
+admission when it matches the armed owner and Job, the guardian's terminal proof is valid, and the
+parent, exact child lifetime and every Job member are proven gone. Missing proof, incomplete or armed
+pending writes still require inspection; guardian death alone cannot reopen admission.
+Recovery does not change an
+ambiguous job result, retry installation or reclaim an abandoned installation lease.
+Without recovery history, a symlinked `OPENCODEX_HOME` (or home ancestor) permits these ordinary
+operations. With history, admission reads the protected canonical home target; unsafe recovery
+paths still report busy. This does not relax the updater's installation or config-path eligibility.
+Shim repair, native apps and other installation managers remain unsupported.
+
+Apply exits 0 only for an `applied` job; `not_applied`, `refused`, `ambiguous` and pending duplicate jobs
+exit 1, including with `--json`. Status reads the
+same private job record without a running proxy or GUI token. Reuse the same optional request ID after
+a lost response: the same key and plan return the original job; a changed plan with that key refuses.
+The private ledger retains up to 64 jobs, including finished jobs, without automatic pruning. At that
+limit, new request IDs are refused with `store_full`; existing matching retries still return their
+original job. Capacity and operator recovery remain a separate follow-up that must preserve this
+deduplication contract. A busy admission ledger reports `job_store_busy` (HTTP 503 with `Retry-After: 1`);
+permanent admission failures have no retry-delay header. Missing or unreadable CLI
+status returns `job_unavailable` and a nonzero exit. Completion persistence failures do not rerun the
+installer or turn an uncertain job into a confirmed success.
+An ambiguous result is never automatically retried or rolled back. After an owner restart, an unfinished
+job reports unknown ownership instead of claiming the old process stopped. Inspect the job's outcome,
+child/lease state and cleanup errors; command exit status alone does not establish an installation result.
+
+The management API provides authenticated plan/status reads and confirmed trusted-loopback GUI-session
+apply admission. A published Node `ocx start` supplies the required inspection snapshot; direct Bun or
+compiled sidecar startup does not provide that proof. The Dashboard update panel is a separate follow-up.
#### Explicit installation observation on Windows x64
@@ -1206,7 +1329,7 @@ ocx system codex-cli-update attest --candidate --npm-prefix /f`, status 1,
@@ -1107,7 +1132,7 @@ When a newer package version is known, the tray adds a blue dot to its online, w
### `ocx gui`
-Open the [web dashboard](/guides/web-dashboard/) at `http://localhost:` — or at
+Open the [web dashboard](/guides/web-dashboard/) at `http://127.0.0.1:` — or at
`http://127.0.0.1:` when hub management ingress is enabled — auto-starting the proxy
if it is not running.
@@ -1213,6 +1238,20 @@ starts and then once a minute; the Home keeps reports in memory only, so after a
fields read `null` until the next report. A Child whose runtime started before `lastApplyAt` is
running an older runtime than its last apply.
+## Remote Link Child attestations
+
+A peer that sends work through a Remote Link Child can check two more facts in the Child's
+`ocx status --json`. Both fields are omitted, never `null`, when they cannot be proven.
+
+| Field | Meaning |
+| --- | --- |
+| `localCredentials` | `{count, generation}`. `count` is the number of provider credentials this installation stores on this machine: OAuth accounts, provider API-key slots (including disabled entries and keychain or environment references), Codex account-pool records, the native Codex `auth.json` and inactive native Codex profiles. Link keys, management tokens and incoming API keys do not count, and neither do the accounts a Home serves. `generation` is opaque and changes whenever a stored credential is added, replaced or removed, even when the count ends where it started. Reported only while an OpenCodex runtime on this machine is running and every store reads completely. A native Codex configuration whose `cli_auth_credentials_store` is not `file` omits it. |
+| `link.connectionGeneration` | On a Child: opaque, and changes whenever the tunnel to its Home connects (including a reconnect that keeps the same link) and whenever either runtime restarts. Reported only while the Child's runtime is ready. For a link started from Home, the Child learns it from the Home's signed answer, so a Home on an older release leaves it out. |
+
+A Child that reports `localCredentials.count` 0, with `localCredentials.generation` and
+`link.connectionGeneration` unchanged across a request, had no local provider credential during it
+and observed no tunnel reconnect.
+
For a link the Home added, each row also carries `tunnel`: `attempt` (ssh attempts since the link
was last connected), `nextRetryAt` (ISO time of the next scheduled retry, `null` while one runs or
none is due), `lastTransition` (`{from, to, at, cause}`) and `lastFailure` (`{cause, at}`). A cause
diff --git a/docs-site/src/content/docs/reference/client-support.md b/docs-site/src/content/docs/reference/client-support.md
new file mode 100644
index 00000000000..e1522a3b0de
--- /dev/null
+++ b/docs-site/src/content/docs/reference/client-support.md
@@ -0,0 +1,60 @@
+---
+title: "Client support"
+description: "Which coding tools opencodex connects to, how each one is wired, and which protocol it speaks."
+---
+
+
+
+
+opencodex is one local gateway for your AI coding tools and models. Some tools it configures natively; for the others it exports or manages a config file that points the tool at the local proxy. This page is generated from the code registries, so it lists exactly what the current release ships.
+
+## Support levels
+
+- **implemented**: the client is in the registry. For exported configs, the protocol shown is read from the config opencodex generates, and generation fails if it changes. A protocol marked *curated* names an endpoint that comes from a review instead of the config itself (native clients, and clients that turn a setting into a specific endpoint); the generator checks that the reviewed code is still in place. "OpenAI-compatible" rows make no endpoint claim.
+- **test-covered**: implemented, and a test case chosen in review that exercises this client is linked. The generator checks only that the test still exists and is enabled; read it to see what it asserts.
+- **Runtime verified**: a historical observation: a person ran the exact client product and version shown against the opencodex version shown, for the stated scenario, on that date, and linked the evidence. It says nothing about other versions or products. The evidence is reviewed when a result is added; the generator does not re-run it. A dash means no result is recorded.
+
+## Clients
+
+| Client | Native | Config export | Managed file integration | Inbound protocol | Support level | Runtime verified | Notes |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| Codex (CLI, app, SDK) | Yes | — | — | Responses (curated) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/codex-integration/codex-inject-missing-config.test.ts)) | — | Set up by opencodex itself; no exported config file. [guide](/guides/codex-integration/) |
+| Claude Code | Yes | — | — | Anthropic Messages (curated) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/claude-integration/claude-cli.test.ts)) | — | Set up by opencodex itself; no exported config file. [guide](/guides/claude-code/) |
+| Claude Desktop | Yes | — | — | Anthropic Messages (curated) | implemented | — | Set up by opencodex itself; no exported config file. |
+| Grok Build | Yes | — | — | Responses (curated) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/providers/xai/grok-config-inject.test.ts)) | — | Set up by opencodex itself; no exported config file. [guide](/guides/grok-build/) |
+| Qoder | — | JSON | Yes | OpenAI-compatible (endpoint chosen by the client) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/qoder-client.test.ts)) | — | Local listener only. |
+| Qoder CN | — | JSON | Yes | OpenAI-compatible (endpoint chosen by the client) | implemented | — | Local listener only. |
+| WorkBuddy | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/agent-clients.test.ts)) | — | Local listener only. Apply runs a client-specific check first and can refuse. |
+| WorkBuddy AI | — | JSON | Yes | Chat Completions | implemented | — | Local listener only. Apply runs a client-specific check first and can refuse. |
+| OpenCode | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | The exporter also supports a remote listener (it adds the API key header). [guide](/guides/opencode/) |
+| Pi | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Local listener only. [guide](/guides/pi/) |
+| Oh My Pi (omp) | — | YAML | Yes | Chat Completions; Responses (native OpenAI models only) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Local listener only. Edits only its own YAML block. |
+| Hermes | — | YAML | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/hermes-catalog-refresh.test.ts)) | — | The exporter also supports a remote listener (it adds the API key header). Edits only its own YAML block. |
+| OpenClaw | — | JSON5 | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/gui/integrations-invariants.test.ts)) | — | The exporter also supports a remote listener (it adds the API key header). |
+| Kimi CLI | — | TOML | Yes | OpenAI-compatible (endpoint chosen by the client) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export-new-clients.test.ts)) | — | Local listener only. |
+| Gajae | — | YAML | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-modality-enum.test.ts)) | — | Local listener only. |
+| DSH | — | YAML | Yes | Responses | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/config/client-config-export.test.ts)) | — | Local listener only. Writes the client's current provider store. Edits only its own YAML block. Writes are coordinated through a lock file next to the config. |
+| MCode | — | YAML | Yes | Anthropic Messages | implemented | — | Local listener only. Writes are coordinated through a lock file next to the config. |
+| ZCode | — | JSON | Yes | Responses (curated) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/integrations-current-store.test.ts)) | — | Local listener only. Writes the client's current provider store. The client picks the endpoint from this setting (reviewed against the client's documented behavior). |
+| Command Code | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/command-code-client.test.ts)) | — | Local listener only. Writes are coordinated through a lock file next to the config. |
+| Prime | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/prime-client.test.ts)) | — | Local listener only. |
+| Aside | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/aside-profiles-catalog-refresh.test.ts)) | — | Local listener only. |
+| Raycast | — | YAML | Yes | Chat Completions (curated) | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/raycast-client.test.ts)) | — | Local listener only. The client picks the endpoint from this setting (reviewed against the client's documented behavior). |
+| omo | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/omo-client.test.ts)) | — | Local listener only. |
+| Cline | — | JSON | Yes | Responses | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/cline-client.test.ts)) | — | Local listener only. Writes are coordinated through a lock file next to the config. |
+| Kilo Code | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/client-export-reasoning-controls.test.ts)) | — | The exporter also supports a remote listener (it adds the API key header). |
+| Factory Droid | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/droid-client.test.ts)) | — | Local listener only. [guide](/guides/factory-droid/) |
+| Gentle Shell | — | JSON | Yes | Chat Completions | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/gentle-shell-client.test.ts)) | — | Local listener only. |
+| T3 Code | — | JSON | Yes | Anthropic Messages | test-covered ([test](https://github.com/lidge-jun/opencodex/blob/main/tests/clients/t3code-client.test.ts)) | — | Local listener only. Apply runs a client-specific check first and can refuse. |
+
+## Inbound endpoints
+
+The proxy accepts these protocols. A tool that is not listed above can still connect if it lets you set a custom endpoint for one of them.
+
+- `POST /v1/responses`: Responses
+- `POST /v1/chat/completions`: Chat Completions
+- `POST /v1/messages`: Anthropic Messages
+
+## Keeping this page current
+
+This page is generated by `scripts/generate-client-support-matrix.ts` from the export, managed-integration and native-integration registries. To add a client, add its entry in `scripts/client-support-matrix/metadata.ts` and run `bun run docs:clients`; `bun run docs:clients:check` fails while the committed page is stale.
diff --git a/docs-site/src/content/docs/reference/codex-retry-diagnostics.md b/docs-site/src/content/docs/reference/codex-retry-diagnostics.md
index 0a7112cab84..a596c37021d 100644
--- a/docs-site/src/content/docs/reference/codex-retry-diagnostics.md
+++ b/docs-site/src/content/docs/reference/codex-retry-diagnostics.md
@@ -36,6 +36,17 @@ identical at this layer and are checked differently.
| Upstream ChatGPT plan window | The account quota section shows a 5-hour or weekly window at or near its limit while the lock reports `ready` or `off`. | Wait for the window to reset, inspect any reset option ChatGPT offers ([banked reset rules](https://help.openai.com/en/articles/20001498-how-banked-codex-resets-work), [plan windows](https://help.openai.com/en/articles/11369540-using-codex-with-your-chatgpt-plan)), or serve the request on another account with `ocx account use openai `. For explicitly authorized paid-credit use after the limit, see [account credit policy](/reference/cli/providers-accounts/#pool-policy-account-thresholds-and-paid-credit-intent). The credit-balance rediscovery defect in [#6845](https://github.com/lidge-jun/opencodex/issues/6845) was fixed by [#6886](https://github.com/lidge-jun/opencodex/pull/6886). |
| Provider incident or transport failure | Quota stays low, the lock stays released, and `ocx doctor` reports transport or ChatGPT reachability problems. | Retry after the incident clears or reroute through an existing combo. Changing lock or threshold policy is not a cure for a transport failure. |
+A proxy-generated `upstream_reset_replay_refused` response also uses status 429:
+upstream execution is unknown, so OCX refuses an automatic resend. Its log cause
+is `transport-ambiguous`, not provider rate limiting. This applies to direct provider
+routes and combo child failures on client HTTP and WebSocket ingress independently
+of `upstreamWebsocket`. Synthetic refusals do not cool combo targets or count as
+account rate limits; genuine upstream 429s retain their rate-limit handling. Older builds can
+misrecord a WebSocket or combo refusal as `rate-limit` / `rate_limit_exceeded`;
+that combination alone does not establish exhausted quota. `headers-only` is a
+response-progress stage, not proof that the provider returned headers. Correct
+attribution does not diagnose or repair the original connection reset.
+
A locked main credential closes request admission; it does not reserve the
account's remaining quota, and a predicted reset never releases the block on
its own. The thresholds (by default `short` 90, `long` 98) are visible in the
diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md
index baba267dadf..f422a166b9d 100644
--- a/docs-site/src/content/docs/reference/configuration.md
+++ b/docs-site/src/content/docs/reference/configuration.md
@@ -27,6 +27,11 @@ If the file cannot be parsed, opencodex backs it up as
`config.json.invalid-`, warns on the console, and starts with defaults. A missing file also
uses the fresh-install default: one `openai` forward provider.
+Local `ocx config set`, `unset`, `import`, offline `ocx effort`, custom-model add/remove, and
+provider add/remove/set-default refuse an invalid or unreadable existing `config.json` without
+changing its bytes. Run `ocx config validate` and repair the file before retrying. Import also
+requires a readable, valid target when a config already exists.
+
## Precedence and defaults
### Provider and model aliases
diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md
index 808d56d2c23..64b316c1e0c 100644
--- a/docs-site/src/content/docs/reference/configuration/providers.md
+++ b/docs-site/src/content/docs/reference/configuration/providers.md
@@ -135,7 +135,8 @@ separate. Full request URLs such as `/api/v1/responses` are not provider base UR
| `codexWsReuseAcrossTurns?` | `boolean` | `false` | Opt-in Codex WebSocket reuse across turns of one account and thread. When on, a busy socket is waited on for at most 750ms before another dial, up to two sockets are kept when the model or service tier changes, and turn-state headers stay on each message instead of the handshake. Authorization, account, proxy, or dial-URL changes still retire the old socket. |
| `upstreamHostCircuitThreshold?` | `number` | `0` | Opt-in circuit threshold for proven pre-connection DNS/TCP failures on native OpenAI forward Responses and compact sends. `0` disables it; `1`–`20` opens a 30-second provider-origin cooldown after that many terminal logical requests. While open, requests receive `503` with `Retry-After` before account selection or upstream send; after cooldown, one half-open request is admitted. Timeouts and HTTP responses never count, and any HTTP response closes the circuit. Applies only to Codex Pool routing with no pinned account; it is inert for `codexAccountMode: "direct"` and account-qualified selectors. |
| `maxUpstreamBodyBytes?` | `number` | `0` | Opt-in ceiling, in bytes, on a serialized native Responses **passthrough** body. `0` or omitted disables it — no limit is inferred for any destination. When set, a built body above the ceiling is refused locally before the send: streaming turns receive a terminal `response.failed` / `context_length_exceeded` so the client compacts instead of resending, and non-streaming turns receive a `413` naming the size, the number of embedded `input_image` items, and roughly how many megabytes of image data they represent. Checked at every build and rebuild point, including OAuth-refresh replay and alternate-account retry. Translated adapter paths are not covered. There is deliberately no default: the only measured ceiling here belongs to the WebSocket transport, which already falls back to HTTP for oversized turns, so a default would refuse requests that currently succeed. Set it when your gateway has a known request-size limit and you would rather see an actionable local error than an opaque upstream failure. |
-| `maxInboundBodyBytes?` | `number` | `0` | Opt-in ceiling, in bytes, on a decompressed **inbound** data-plane request body — the mirror of `maxUpstreamBodyBytes` above. `0` or omitted keeps the built-in 256 MiB default. Raise it when a large-context session can no longer compact: Codex replays the whole history to the compaction model, so on the 922k-token opt-in window the compaction request is itself the one that crosses the limit, and the session is stuck at the only operation that would have shrunk it. Clamped to 1 MiB–512 MiB. The ceiling is not negotiable: the reader materializes the body several times over (wire bytes, decoded bytes, the decoded string, and the parsed object graph), so peak memory is a multiple of whatever is admitted, and an unbounded value would be a memory exhaustion lever. Applies to `/v1/responses`, `/v1/responses/compact`, `/v1/chat/completions`, and `/v1/messages`. The listener's accept size is fixed when the proxy binds, so a change takes effect on restart. A body above the limit is refused locally with HTTP 413 and `code: "inbound_body_too_large"`, which is deliberately distinct from the `context_length_exceeded` 413 a provider size refusal produces. |
+| `maxInboundBodyBytes?` | `number` | `0` | Opt-in ceiling, in bytes, on a decompressed **inbound** data-plane request body — the mirror of `maxUpstreamBodyBytes` above. `0` or omitted keeps the built-in 256 MiB default. Raise it when a large-context session can no longer compact: Codex replays the whole history to the compaction model, so on the 922k-token opt-in window the compaction request is itself the one that crosses the limit, and the session is stuck at the only operation that would have shrunk it. Clamped to 1 MiB–512 MiB. The ceiling is not negotiable: the reader materializes the body several times over (wire bytes, decoded bytes, the decoded string, and the parsed object graph), so peak memory is a multiple of whatever is admitted, and an unbounded value would be a memory exhaustion lever. Applies to `/v1/responses`, `/v1/responses/compact`, `/v1/chat/completions`, and `/v1/messages`, plus the image, search, and token-counting routes listed in [inbound body admission](/reference/inbound-body-admission/). The listener's accept size is fixed when the proxy binds, so a change takes effect on restart. A body above the limit is refused locally with HTTP 413 and `code: "inbound_body_too_large"`, which is deliberately distinct from the `context_length_exceeded` 413 a provider size refusal produces. A resolved limit above 256 MiB also activates the shared admission gate controlled by `maxConcurrentInboundBodyBytes`; raising this per-request limit alone does not raise that budget. |
+| `maxConcurrentInboundBodyBytes?` | `number` | `536870912` (512 MiB) | Optional top-level shared admission budget in bytes, active only when the resolved `maxInboundBodyBytes` exceeds 256 MiB. Omission, `0`, or an invalid value uses the unchanged 512 MiB default. Valid positive integers are clamped to 512 MiB–2 GiB (`536870912`–`2147483648`) when consumed. Restart required; all listeners in one process share one budget. This is admission accounting, not preallocated memory, an RSS cap, or automatic RAM sizing. A 2 GiB budget requires an explicit operator choice. See [inbound body admission](/reference/inbound-body-admission/) for the configuration example, queue and response-lifetime tradeoffs, and why the default still permits only one full 512 MiB upload before parsing. |
| `upstreamRequestCompression?` | `"off" \| "gzip" \| "zstd"` | `"off"` | Opt-in compression of native ChatGPT Responses HTTP request bodies. Codex re-sends the whole conversation on every turn, and when that turn goes over HTTP OpenCodex otherwise forwards it to `chatgpt.com` uncompressed; on a metered or long-distance egress that upload is the dominant cost. When set, string bodies from 1 KiB to 16 MiB sent to `https://chatgpt.com/backend-api/codex/responses` and `/compact` are compressed at the final send and labelled with `Content-Encoding`. WebSocket turns, every other destination, bodies that already declare a `Content-Encoding`, and bodies outside that size window are sent unchanged, and a codec failure falls back to the uncompressed send. `zstd` is the encoding Codex CLI itself uses with this backend and is the faster choice for image-heavy turns; `gzip` (level 1) is also available. In a local measurement, code-heavy history shrank about 2–4×, while embedded images barely shrink. Compression runs synchronously on the proxy's event loop, so each eligible send briefly delays other in-flight requests — on the order of 2–3 ms per MiB for text on a recent laptop CPU, more for incompressible data with `gzip`. Byte limits such as `maxUpstreamBodyBytes` still measure the uncompressed body. Set it with `ocx config set upstreamRequestCompression zstd` (an unknown value is refused) or by editing `config.json`, where an unknown value is ignored, then restart the proxy to apply. |
| `modelCacheTtlMs?` | `number` | `300000` | Freshness window for the per-provider `/models` cache. |
| `cacheRetention?` | `"none" \| "short" \| "long"` | first-party Anthropic: `"long"`; other Anthropic-compatible endpoints: `"short"` | Anthropic prompt-cache policy: disabled, 5-minute ephemeral, or 1-hour extended. When unset, requests to `api.anthropic.com` (API key or the OAuth account pools) use the 1-hour TTL, because agentic turns often pause longer than five minutes and a 1-hour write (2x base input instead of 1.25x) pays for itself on the first reuse; Anthropic-compatible gateways and vendor endpoints keep the 5-minute TTL because their 1-hour support is undocumented. On native `/v1/messages` passthrough, caller breakpoints without a `ttl` get `"1h"` unless the request pins any breakpoint to `"5m"`, in which case it is forwarded unchanged. On a translated `/v1/messages` route, caller breakpoints that all ask for `ttl: "1h"` mean `"long"` and any explicit `"5m"` means `"short"`. An explicit value always wins. Changing the TTL makes each open conversation write its cache once more, since 5-minute and 1-hour entries are separate. Cost estimates price writes reported as 1-hour at the 1-hour rate. |
@@ -335,7 +336,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. |
| `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. |
| `modelDisplayNames?` | `Record` | Durable labels used only for display, keyed by this provider's exact upstream model id. Labels win over provider catalog metadata, survive discovery refreshes and provider edits, and never change authentication, adapter behavior, routing, billing, upstream request construction, the routed `provider/model` selector, or the upstream wire model. Keys are exact and case sensitive. Unknown model ids are kept so a temporarily missing model receives its label when it returns. The map accepts at most 2,000 entries, matching the discovery limit. |
-| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. The Models dashboard exposes this separately from `providerContextCaps`. |
+| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. Registry defaults cannot override a lower configured limit; explicit `modelContextWindows` entries take priority. The Models dashboard exposes this separately from `providerContextCaps`. |
| `modelContextWindows?` | `Record` | Per-model context fallbacks/caps. These override `contextWindow`: an unknown window uses the configured value, while smaller live metadata remains authoritative. |
| `inferContextWindow?` | `boolean` | Custom providers only, default off. `true` fills a missing window from bundled model-ID metadata ([details](#context-windows-on-custom-providers)); the value is an estimate, not verified relay capacity. |
| `modelContextTiers?` | `Record` | Per-model upstream tier for `github-copilot`. `long_context` sends `contextTier: "long_context"`. Catalog metadata grows only with an exact per-model `modelContextWindows` value that documents the supported window; that value wins before the provider cap. An unknown model keeps its observed window. Unconfigured passthrough requests retain a caller-supplied `contextTier` field on any provider. |
@@ -346,7 +347,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `defaultMaxOutputTokens?` | `number` | Provider-wide `openai-chat` fallback when the client omits `max_output_tokens`. |
| `modelMaxOutputTokens?` | `Record` | Positive per-model `openai-chat` fallback budgets; exact/pattern matches beat the provider default. |
| `modelCosts?` | `Record` | Per-model display prices (USD per 1M tokens), keyed by that provider's exact upstream model id — not a provider identifier or a routed `provider/model` label, e.g. `{ "deepseek-v4-flash": { "input": 0.14, "output": 0.28, "cacheRead": 0.0028, "cacheWrite": 0 } }`. Any model id is a valid key — custom providers may target any OpenAI-compatible endpoint through the `openai-chat` adapter, and local or internal provider ids work even when they are absent from the built-in catalogs. User-configured prices win over the built-in catalogs in the Logs `~$` and Usage estimates; historical entries are repriced from the current overlay, so editing a price can move past totals. The fallback order is user `modelCosts` → exact official correction → jawcode catalog → expected-price overlay → model-level vendor fallback, and an explicit all-zero user entry means a known-zero estimate; delete that model entry to restore automatic pricing. All-zero catalog metadata still falls through. Each rate must be a non-negative finite number at most 1,000,000 (USD per 1M tokens); out-of-range rows are rejected by the management boundary and dropped on load. Display-time estimation only: overlays never affect routing, account selection, quotas, or billing. |
-| `headers?` | `Record` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. Requests that ride the provider outbound wrapper — model discovery, connection tests, and other proxy-originated diagnostics such as the Ollama show probe — send `User-Agent: opencodex` unless a User-Agent is set here or by the provider preset. |
+| `headers?` | `Record` | Extra upstream headers. Authorization, cookies, API-key headers, embedded newlines, and invalid names are rejected. Requests that ride the provider outbound wrapper — model discovery, connection tests, and other proxy-originated diagnostics such as the Ollama show probe — send `User-Agent: opencodex` unless a User-Agent is set here or by the provider preset. An experimental relay may negotiate earlier completed-tool delivery with `x-opencodex-tool-completion: block-stop-v1`; the embedding shell must supply authenticated durable turn membership, and the relay must acknowledge the same response header and implement exact call-bound completion markers. Leave it unset for other providers. Early delivery applies only to eligible structured calls; later errors or cancellation cannot retract a call already delivered. |
| `forwardClientHeaders?` | `string[]` | `openai-responses` only: opt in to copying named inbound client metadata headers onto inference requests when the provider has not already set that header. Credential and transport-owned names such as `Authorization`, cookies, API-key headers, `Content-Type`, `Content-Length`, `Host`, and `x-oai-attestation` are rejected and also filtered at runtime. Header names are case-insensitive and may not repeat. `headers` remains authoritative; the existing caller `User-Agent` fallback still applies when neither surface sets one. Supported names: `originator`, `x-client-request-id`, `x-codex-app-version`, `user-agent`; all other names are refused. |
| `supportsNativeCompactionTrigger?` | `boolean` | `openai-responses` only: declare support for native v2 `compaction_trigger` input items. Defaults off for noncanonical gateways; preserves native compaction rather than requesting a portable text summary. Independent of the v1 endpoint and blob replay capabilities. |
| `supportsNativeCompactEndpoint?` | `boolean` | `openai-responses` only: declare support for native v1 `POST /responses/compact`. Defaults off for custom gateways. Key/local/forward routes use the provider's Responses URL plus `/compact` and its normal transport authentication; OAuth keeps routed compaction and its existing credential lifecycle. Does not enable v2 triggers or grant official OpenAI identity. |
@@ -392,7 +393,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `transientRetryOn5xx?` | `{ enabled?: boolean; attempts?: number }` | Key-auth `openai-chat` and `openai-responses` providers only. `authMode: "forward"` providers (the ChatGPT account pool) never read this option and keep the default ladder. Opt-in retry for pre-stream transient upstream statuses (500, 502, 503, 504, 520, 521, 522): absent means off, object presence enables it unless `enabled: false`. Covers the initial Responses request, the Responses passthrough lane and each of its recovery legs (OAuth-401 replay, same-target 429 replay, validated rebuild), the terminal-guard continuation, and native `/v1/chat/completions`. `attempts` is the TOTAL number of upstream sends allowed for one request including the first (1..10, default 3) — it is one budget shared with connection-reset recovery, so `3` means at most three real requests reach the provider. On the Responses passthrough lane the configured value is additionally intersected with the request-wide send allowance, so a value below that allowance narrows the ladder exactly while a value above it does not raise the bound. Each Combo target includes its prepaid first send in that target's configured total; `attempts: 1` sends once, without refunding the reservation or widening the shared Combo ceiling. A local refusal or cancellation before dispatch releases an unsent booking. Waits use a fixed 400 ms exponential backoff capped at 5 s and honor `Retry-After`. Separate from `retryOn429`, which handles rate limiting; mid-stream failures are never replayed. |
| `retryOnReset?` | `{ enabled?: boolean; replacements?: number }` | Native `openai-responses` providers, including `authMode: "forward"`, plus generic translated Responses dispatch before upstream headers. Opt-in replacement of a send that failed while the caller had observed nothing: absent means off, object presence enables it unless `enabled: false`. Covers both ambiguous stages — a connection that died before any response header, and an SSE body that died after the header while carrying only control events. A canonical ChatGPT upstream WebSocket that closed or errored after its create frame left, before any Responses event, is covered the same way, and its replacement is sent over HTTP. Only a self-contained request is ever replaced: `store: false`, complete `input`, no `previous_response_id`, `conversation` or `stream_id`, and only client-executed tools. `replacements` is the number of replacement sends ONE logical request may make across every leg and every combo child (1..2, default 1) — not a per-leg retry count and not a send budget, so a replacement still has to fit inside the send allowance the leg already had. A request that already emitted output or a tool call is never replaced, whatever this is set to. The replacement inference may still be billed if the origin had already started the first one, which is why this is off by default. Generic translated dispatch supports pre-header reset replacement for initial and rebuilt sends under the same grant and send budget. Adapter-owned transports and translated stream failures after upstream headers are excluded. |
| `autoToolChoiceOnlyModels?` | `string[]` | Models whose `tool_choice` accepts only `auto` or `none`; forced choices are downgraded. |
-| `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. A provider save that keeps the destination keeps the stored list, including `[]`; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH /api/providers?name=` accepts an array or `null` to clear it. |
+| `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. Translated Chat requests to `api.mistral.ai` instead reconstruct native thinking chunks before assistant text; supplied thinking replays by default for all hosted families, and an explicit list (including `[]`) overrides enrollment. No placeholder thinking or signatures are invented. Native Chat passthrough is unchanged. A provider save that keeps the destination keeps the stored list, including `[]`; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH /api/providers?name=` accepts an array or `null` to clear it. |
| `inlineThinkTagModels?` | `string[]` | Opt-in recovery for `openai-chat` gateways without a server-side reasoning parser. A leading `` / `` / `` block (optionally after whitespace) activates splitting in streamed and buffered replies. All answer whitespace is preserved. Subsequent tags are delimiters anywhere, including same-line interleaving and code fences; this mode does not interpret Markdown. Ordinary text or a code fence before the first tag keeps the whole reply untouched. Off by default; prefer structured upstream reasoning or `reasoningSplitModels` where supported. |
| `reasoningDetailsModels?` | `string[]` | Models whose endpoint returns thinking as a structured `reasoning_details` array (MiniMax M-series with `reasoning_split`); stream deltas are cumulative snapshots that are prefix-diffed, and preserved reasoning replays as a `reasoning_details` array instead of a `reasoning_content` string. |
| `requiresReasoningPlaceholderModels?` | `string[]` | Models whose upstream rejects a tool_call continuation missing `reasoning_content` (DeepSeek thinking mode); a minimal placeholder is injected when the replay cache misses. Defaults to `preserveReasoningContentModels`; set `[]` to opt out. A provider save that keeps the destination keeps the stored list, including `[]`; see [What a provider save keeps](#what-a-provider-save-keeps). `PATCH /api/providers?name=` accepts an array or `null` to clear it. |
@@ -408,8 +409,9 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `directGeminiWireRenames?` | `boolean` | Google only. Applies in AI Studio mode, including custom Google-compatible URLs. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex and canonical keyed OpenCode Zen routes preserve the requested model ID regardless of this setting; Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. |
| `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. Vertex API keys use this project endpoint when set; without it they use express mode, even if `GOOGLE_CLOUD_PROJECT` is set. ADC falls back to `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT`. `"auto"` makes each Vertex API key resolve its own project (a successful lookup is cached per key; a key whose lookup fails is probed again on a later request), so one key pool can span projects; ADC rejects `"auto"`. |
| — | — | Antigravity account quota probes (`retrieveUserQuota` and `retrieveUserQuotaSummary`) always go to Google's own Cloud Code host through the pinned outbound transport, regardless of a configured `baseUrl`; the account bearer is never sent to an operator-configured endpoint and a redirect aborts the probe. Only the model-list fallback still honors `baseUrl`. |
-| — | — | If Antigravity quota summary returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Inference and model discovery retain the IDE User-Agent. |
+| — | — | Antigravity quota summary is requested with the Antigravity CLI User-Agent, the only client family for which Google includes the Gemini 4 Argon quota group. If it returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Non-Argon inference and the model-list quota fallback keep the IDE User-Agent. |
| — | — | Antigravity quota shows Gemini 4 Argon as separate `Argon` / `Argon (Weekly)` windows next to Gem and Cla. They are display-only: account ranking, quota exhaustion, reset ordering and reset notifications keep using the Gemini and Claude windows, and Argon requests are still judged by the Gemini window. |
+| — | — | Refreshing Antigravity discovery unions usable accounts' enabled models and retains each account's own roster for six hours. With account failover active, a CCA 404 tries another eligible account only when its complete original error is at most 4 KiB and contains a model ResourceInfo whose resourceName exactly matches `models/`. Message-only errors, other resources and oversized, incomplete or erroring bodies keep the original HTTP status without a model denial or rotation. A confirmed denial lasts up to 24 hours, until positive discovery started after that request or credential replacement; older discovery cannot clear it or replace a newer positive roster. Exhaustion names the model and distinguishes unavailable accounts from credential, admission, or send-budget failure. No paid/trial classification is inferred. |
| `location?` | `string` | Vertex location. API-key project endpoints default to `global` and ignore `GOOGLE_CLOUD_LOCATION`; ADC requires this field or the `GOOGLE_CLOUD_LOCATION` fallback. Gemini 4 Argon supports `global`, `us`, and `eu`. |
| `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. |
| `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. |
@@ -736,6 +738,8 @@ it does not restore the old name. Requests have a 60-second deadline covering th
follow-up list refresh. A timeout does not undo a write: use **Retry** to check the current name
before making another change.
+Claude Code's `/model` picker (Anthropic-flavor `GET /v1/models`) shows a routed row with an exact label as `