diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index fb302d3695..988a43ee1d 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -364,7 +364,9 @@ Claude Code's `/effort` setting is preserved across the adapter: | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, above→`high` | | `thinking.type: "disabled"` | Reasoning parameters omitted entirely | -The resolved value appears in the request log's **Reasoning effort** column. +The request log shows the parsed request and, when the routed adapter exposes an outbound +diagnostic, the exact serialized value under **Requested → sent**. “Sent” confirms what +CodexCommander put on the wire, not that the upstream provider applied it. ## Inbound translation (Messages → Responses) diff --git a/docs-site/src/content/docs/guides/macos-menu-bar.md b/docs-site/src/content/docs/guides/macos-menu-bar.md index 9b9826f08e..551823d873 100644 --- a/docs-site/src/content/docs/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/guides/macos-menu-bar.md @@ -179,11 +179,17 @@ the token in a browser URL. When the companion opens the dashboard, it asks that verified local proxy for a short-lived, single-use launch ticket. The ticket appears only in the URL fragment and is removed during its -one-time exchange; the durable admin token never enters the URL or web storage. The resulting -full-featured session is process-memory-only, lasts up to eight hours, and is never renewed. Expiry -or proxy restart makes the next API request return `401`, and the page tells the user to reopen -through the companion or `ccx gui`. A manually opened loopback dashboard receives no API session and -never prompts for or sends the durable admin token. +one-time exchange. The server keeps the resulting full-featured session in process memory for up to +eight hours. The browser mirrors only its session token, CSRF token, origin, and absolute expiry in +`sessionStorage`, so a refresh works while that server session remains valid. It is never +renewed. Expiry, proxy restart, or a rejecting `401` clears the browser record and tells the user to +reopen through the companion or `ccx gui`. Neither the durable admin token nor the launch ticket enters +browser storage, and authentication never uses `localStorage`. Same-origin script can read +`sessionStorage`, so this reload convenience is not OS-user isolation. Browsers may copy the record +into duplicated or opener-created tabs, or restore it with a restored tab; every copy remains bound +to the exact origin and CSRF token and is usable only until the fixed server expiry, a proxy restart, +or a rejecting `401`. A manually opened loopback +dashboard receives no API session and never prompts for or sends the durable admin token. Provider credentials remain owned by CodexCommander. The companion never reads ChatGPT, Kimi, Grok, Anthropic, or other provider tokens and never calls provider login endpoints directly. diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 7e9afc11a3..4830332c91 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -32,10 +32,18 @@ session; loopback is not an authentication bypass. For the full dashboard, open it with `ccx gui` or the macOS menu app. The launcher uses admin authority to mint a short-lived, single-use ticket, puts only that ticket in the URL fragment, and the dashboard -removes it immediately as it exchanges it for a confirmed session. A confirmed session lives only in -the proxy and browser process for up to eight hours and is never renewed. Expiry or a proxy restart -makes the next API request return `401`, and the loopback page requires a new launcher handoff. The -durable admin token never enters the URL or browser storage. +removes it immediately as it exchanges it for a confirmed session. The server keeps that session in +process memory for up to eight hours. The browser mirrors only its session token, CSRF token, origin, +and absolute expiry in `sessionStorage`, so refreshing keeps working while +the server session remains valid. The session is never renewed. Expiry, a proxy restart, or a rejecting +`401` clears the browser record and requires a new launcher handoff. Neither the durable admin token +nor the launch ticket enters browser storage, and authentication never uses `localStorage`. + +Same-origin script can read `sessionStorage`. This small reload convenience is therefore not OS-user +isolation; it does not replace the launcher's listener check or the server's origin and CSRF checks. +Browsers may copy the record into duplicated or opener-created tabs, or restore it with a restored +tab; every copy remains bound to the exact origin and CSRF token and is usable only until the fixed +server expiry, a proxy restart, or a rejecting `401`. A dashboard bound to a non-loopback hostname may use the admin token (`CODEXCOMMANDER_ADMIN_AUTH_TOKEN`, or the auto-generated `~/.codexcommander/admin-api-token` file), @@ -45,8 +53,8 @@ as loopback, then open it through `ccx gui`. Raw admin remains available to head clients, but catalog Apply is deliberately restricted to a confirmed local dashboard launch. On trusted HTTPS, a remote dashboard presents a standard password form so a browser password manager -can offer to save and autofill the credential. The dashboard itself still keeps the token only in -memory and does not write it to `localStorage` or `sessionStorage`; whether it is saved is entirely +can offer to save and autofill the credential. The dashboard itself still keeps that raw admin token +only in memory and does not write it to `localStorage` or `sessionStorage`; whether it is saved is entirely the browser or password manager's decision. ## What you can do @@ -67,7 +75,7 @@ the browser or password manager's decision. | **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose **Reliable v1**, **Codex native**, or **Concurrent v2**, and configure the v2 thread limit. The Current behavior card reports context as **Uncapped**, **Limited**, or **Mixed limits**. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. Each routed-provider row reports **Auto-discovery on** or **Static catalog only** and links to the owning Provider setting. | | **Client Apps** | Inspect configured and available local clients, apply or remove managed config where supported, review backups, and reach Codex, Claude Code/Desktop, Grok Build, OpenCode and the file-managed clients without treating providers as clients. | | **API Access** | Issue and manage keys that authenticate other apps to the CodexCommander proxy. Provider credentials remain under Providers. | -| **Logs** | Auto-refresh recent requests with tokens, requested effort and (when available) effective outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact reasoning wire field when the adapter emits one. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. | +| **Logs** | Auto-refresh recent requests with tokens, requested → sent outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact sent reasoning wire field when the adapter emits one. “Sent” is what CodexCommander serialized; it does not prove that the provider accepted, honored, or applied that effort. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. | | **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. | | **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. | | **Stop** | Persist integration OFF, restore and verify native Codex, then stop an unsupervised proxy (`POST /api/stop`). If an installed supervisor owns it, the raw API refuses; use tray or CLI Stop so that flow stops the manager first. | 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 0d88ea0bd8..7211e027a7 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -243,7 +243,9 @@ Claude Code の `/effort` 設定はアダプターでも維持されます。 | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`、≤16384→`medium`、それより大→`high` | | `thinking.type: "disabled"` | 推論パラメータをすべて省略します | -解釈された値はリクエストログの **Reasoning effort** 列に表示されます。 +リクエストログでは、解釈した要求値と、ルーティング先 adapter が outbound 診断を提供する場合は +実際にシリアライズした値を **要求 → 送信** として表示します。「送信」は CodexCommander が wire に +載せた値を示すだけで、上流 provider が適用したことの確認ではありません。 ## 入力変換(Messages → Responses) diff --git a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md index 69d4f46d8c..be97225d3f 100644 --- a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md @@ -149,7 +149,7 @@ ccx sync --restart-codex ループバックの CodexCommander プロセスにのみ送信します。トークンを表示、ログ記録、コピー、保存したり、 ブラウザー URL に入れたりすることはありません。 -コンパニオンがダッシュボードを開くときは、検証済みローカルプロキシに短時間・1 回限りの起動チケットを要求します。チケットは URL フラグメントだけに入り、1 回の交換中に削除されます。永続的な管理トークンが URL や Web Storage に入ることはありません。確認済みの全機能セッションはプロセスメモリ内だけに最大 8 時間存在し、更新されません。期限切れまたはプロキシ再起動後の次の API リクエストは `401` になり、コンパニオンか `ccx gui` から開き直すよう案内されます。手動で開いたループバックダッシュボードには API セッションがなく、永続的な管理トークンを要求も送信もしません。 +コンパニオンがダッシュボードを開くときは、検証済みローカルプロキシに短時間・1 回限りの起動チケットを要求します。チケットは URL フラグメントだけに入り、1 回の交換中に削除されます。サーバーは確認済みの全機能セッションをプロセスメモリ内に最大 8 時間保持します。ブラウザーが `sessionStorage` に複製するのはセッショントークン、CSRF トークン、origin、絶対有効期限だけなので、サーバーセッションが有効な間は再読み込みできます。セッションは更新されません。期限切れ、プロキシ再起動、または拒否を示す `401` によりブラウザー側のレコードが消去され、コンパニオンか `ccx gui` から開き直すよう案内されます。永続的な管理トークンも起動チケットもブラウザーストレージには入らず、認証に `localStorage` は使いません。同一 origin のスクリプトは `sessionStorage` を読み取れるため、この再読み込み上の利便性は OS ユーザー分離ではありません。ブラウザーはレコードを複製タブや opener から開いたタブにコピーしたり、タブ復元時に復元したりする場合があります。どのコピーも正確な origin と CSRF トークンに拘束され、固定されたサーバー有効期限、プロキシ再起動、または拒否を示す `401` までしか使用できません。手動で開いたループバックダッシュボードには API セッションがなく、永続的な管理トークンを要求も送信もしません。 プロバイダー認証情報の管理は引き続き CodexCommander が担います。コンパニオンは ChatGPT、Kimi、 Grok、Anthropic、その他のプロバイダートークンを読み取らず、プロバイダーのログイン 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 12c3a94f2a..cd8515afc9 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -25,11 +25,13 @@ bun run dev:gui `localhost`、`*.localhost`、`127.0.0.0/8` 内のアドレス、`::1`、IPv4-mapped の `127/8` アドレスなど、どのループバック形式でも、手動で開いたダッシュボードには API 資格情報がありません。ページの枠は読み込めますが、API リクエストは未認証のままです。`ccx gui` または macOS メニューアプリから開き直してください。別のローカル OS ユーザーが停止中の listener を偽装できるため、ループバックページは永続的な管理トークンを要求も送信もしません。ループバックのブラウザーアクセスには確認済みランチャーセッションが必要で、認証の迂回路にはなりません。 -全機能を使うには `ccx gui` または macOS メニューアプリから開きます。ランチャーは管理者権限で短時間・1 回限りのチケットを発行し、そのチケットだけを URL フラグメントに入れます。ダッシュボードは 1 回の交換中にすぐ削除します。交換後の確認済みセッションはプロキシとブラウザープロセスのメモリ内だけに最大 8 時間存在し、更新されません。期限切れまたはプロキシ再起動後の次の API リクエストは `401` になり、ループバックページでは新しいランチャーハンドオフが必要です。永続的な管理トークンが URL やブラウザーストレージに入ることはありません。 +全機能を使うには `ccx gui` または macOS メニューアプリから開きます。ランチャーは管理者権限で短時間・1 回限りのチケットを発行し、そのチケットだけを URL フラグメントに入れます。ダッシュボードは 1 回の交換中にすぐ削除します。サーバーは確認済みセッションをプロセスメモリ内に最大 8 時間保持します。ブラウザーが `sessionStorage` に複製するのはセッショントークン、CSRF トークン、origin、絶対有効期限だけなので、サーバーセッションが有効な間は再読み込み後も利用できます。セッションは更新されません。期限切れ、プロキシ再起動、または拒否を示す `401` が発生するとブラウザー側のレコードが消去され、新しいランチャーハンドオフが必要です。永続的な管理トークンも起動チケットもブラウザーストレージには入らず、認証に `localStorage` は使いません。 + +同一 origin のスクリプトは `sessionStorage` を読み取れます。この再読み込み上の利便性は OS ユーザー分離ではなく、ランチャーによる listener 検証やサーバーの origin/CSRF 検証を置き換えるものではありません。ブラウザーはレコードを複製タブや opener から開いたタブにコピーしたり、タブ復元時に復元したりする場合があります。どのコピーも正確な origin と CSRF トークンに拘束され、固定されたサーバー有効期限、プロキシ再起動、または拒否を示す `401` までしか使用できません。 ループバック以外のホストでは `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` または `~/.codexcommander/admin-api-token` の管理トークンを使用できますが、ブラウザーの入力欄は信頼できる HTTPS origin でだけ有効です。平文のリモートページは bearer を要求も送信もしません。信頼できる HTTPS がない場合は、ダッシュボードをループバックとして提示するローカルまたは SSH tunnel を使い、`ccx gui` から開いてください。生の管理トークンは headless management API client では引き続き使用できますが、カタログ Apply は確認済みのローカルダッシュボード起動だけに制限されます。 -信頼できる HTTPS 上のリモートダッシュボードでは標準のパスワードフォームが表示され、ブラウザのパスワードマネージャーで保存・自動入力できます。ダッシュボード自体はトークンをメモリ内だけに保持し、`localStorage` や `sessionStorage` には書き込みません。保存するかどうかはブラウザまたはパスワードマネージャーだけが決定します。 +信頼できる HTTPS 上のリモートダッシュボードでは標準のパスワードフォームが表示され、ブラウザのパスワードマネージャーで保存・自動入力できます。ダッシュボード自体はこの生の管理トークンをメモリ内だけに保持し、`localStorage` や `sessionStorage` には書き込みません。保存するかどうかはブラウザまたはパスワードマネージャーだけが決定します。 ## できること @@ -49,7 +51,7 @@ bun run dev:gui | **モデル** | ネイティブ GPT とルーティングモデルをオン/オフし、プロバイダー許可リストとコンテキスト上限を設定し、**Reliable v1**、**Codex native**、**Concurrent v2** を選択して v2 スレッド数を設定します。「現在の動作」カードではコンテキストを **上限なし**、**制限あり**、**混在** として表示します。各ルーティングプロバイダーには **自動検出オン** または **静的カタログのみ** が表示され、管理元のプロバイダー設定へ移動できます。 | | **Client Apps** | 設定済み・利用可能なローカルクライアントを確認し、対応する管理設定の適用/削除とバックアップ確認を行い、プロバイダーと混同せずに Codex、Claude Code/Desktop、Grok Build、OpenCode、ファイル管理クライアントへ移動します。 | | **API Access** | 他のアプリが CodexCommander プロキシへ接続するための認証キーを発行・管理します。上流プロバイダーの認証情報は Providers に残ります。 | -| **ログ** | トークン、要求された強度と(利用可能な場合は)実際に送信された強度、実際のモデル、プロバイダー、状態、リクエスト ID、所要時間、エラー詳細を含む最近のリクエストを自動更新します。アダプターが reasoning パラメーターを送信した場合、詳細表示に正確な wire field も表示されます。 | +| **ログ** | トークン、要求 → 送信した outbound 強度、実際のモデル、プロバイダー、状態、リクエスト ID、所要時間、エラー詳細を含む最近のリクエストを自動更新します。アダプターが reasoning パラメーターを送信した場合、詳細表示に正確な送信 wire field も表示されます。「送信」は CodexCommander がシリアライズした値であり、プロバイダーがその強度を受理、尊重、適用した証拠ではありません。 | | **使用量 / デバッグ** | トークン使用量の測定範囲と推移を見るか、オプションのプロバイダートランスポート/使用量抽出診断をオンにします。 | | **ストレージ** | CODEX_HOME のディスク内訳(セッション、アーカイブ、DB、添付)を読み取り専用で表示。任意のアーカイブクリーンアップ: 最古 N% をプレビューし、既定では `CODEX_HOME/.trash` へ隔離、または明示チェックで完全削除。**自動クリーンアップ方針**はオプトインで**既定 OFF**(`storageCleanupPolicy.enabled`)。Storage ページでしきい値/目標/スケジュール/モードを設定するか **今すぐ実行**。隔離エントリは Storage ページから復元可能(JSONL + スレッド)。アクティブセッションは読み取り専用。最新/アクティブな `state_*.sqlite` がロック中はクリーンアップと復元を拒否。 | | **停止** | 統合を OFF に保存し、ネイティブ Codex を復元・検証してから、supervisor のないプロキシを停止します (`POST /api/stop`)。インストール済み supervisor が所有している場合、raw API は拒否します。manager を先に停止するトレイまたは CLI の Stop を使用してください。 | diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index 646c48472d..f9793daa40 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -43,7 +43,9 @@ interface ProviderAdapter { ## `openai-responses` -**対象:** OpenAI **Responses API**。**`passthrough: true`** — 元のリクエスト本文をそのまま渡し、レスポンスを **変換せずに** ストリーミングします。 +**対象:** OpenAI **Responses API**。**`passthrough: true`** — Responses のリクエストとレスポンスの +形を Chat Completions モデルへ変換せずに維持します。文書化された互換性正規化だけがリクエスト +形状の例外で、レスポンスは **変換せずに** ストリーミングします。 **認証:** `forward`(呼び出し元ヘッダー中継)または `key`。 `key` 認証では、[`retryOn429`](/ja/reference/configuration/) もここに適用されます: プリストリームの @@ -53,6 +55,11 @@ HTTP リトライ ループの対象外です。 - `forward` URL → `{baseUrl}/responses`。`key` provider のデフォルト URL は `{baseUrl}/v1/responses` です。 - `key` provider は検証済みの相対 `responsesPath` を設定できます。adapter は `baseUrl` 末尾の `/` を 1 つ除き、`{trimmedBaseUrl}{responsesPath}` に送信します。Ark Agent Plan では `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` と `responsesPath: "/responses"` を使います。 +- 最終送信境界では、認識済みの provider/model reasoning 契約に対して、すでに存在する + `reasoning.effort` をマッピングまたはクランプします。不明・カスタム契約では呼び出し元の値を + 変更せず、その任意の値を永続診断にもコピーしません。認識済み契約による検証済みの値だけを + 実際に送信した field/value として記録します。**送信**は CodexCommander がシリアライズした + 値を意味し、上流が適用したことの確認ではありません。 - `forward` モードでは安全なヘッダー許可リスト(`FORWARD_HEADERS`)だけを中継します。authorization、ChatGPT account id、OpenAI beta/originator/session ヘッダーが対象です。この ChatGPT ログイン経路は [サイドカー](/ja/guides/sidecars/) にも使われます。 ## `anthropic` diff --git a/docs-site/src/content/docs/ja/reference/architecture.md b/docs-site/src/content/docs/ja/reference/architecture.md index 678adfbfca..32b144cc4b 100644 --- a/docs-site/src/content/docs/ja/reference/architecture.md +++ b/docs-site/src/content/docs/ja/reference/architecture.md @@ -115,6 +115,13 @@ Codex カタログは Codex が受け入れるラベル(`low` / `medium` / `hi - カスタム wire マッピングのためのモデル別・プロバイダー別 `reasoningEffortMap` override を解釈します。 - `noReasoningModels` に列挙されたモデルについては effort を完全に削除します。 +変換型 adapter はリクエストの構築中にこの契約を適用します。Responses passthrough は、認識済みの +provider/model 契約に限り、最終送信境界で適用します。不明・カスタムの Responses 契約では +`reasoning.effort` を変更せず、その任意の値を永続診断にもコピーしません。認識済み契約が +検証済みの値を生成した場合だけ、リクエスト診断に実際の outbound field/value を記録し、 +dashboard では **要求 → 送信** と表示します。「送信」は CodexCommander がシリアライズした +値であり、上流 provider が適用した証明ではありません。 + ## コア型 内部モデルは `types.ts` にあります: `CodexCommanderParsedRequest`、`CodexCommanderContext`、`CodexCommanderMessage` ユニオン、 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 9621bcb8c0..f5cd30162a 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -211,4 +211,4 @@ Windows ステータス トレイ アイコンをインストールして制御 ### `ccx gui` -`http://localhost:` で [ウェブダッシュボード](/guides/web-dashboard/) を開き、必要ならプロキシを自動起動します。短時間・1 回限りのブラウザー起動チケットにより、確認済みの **Apply agent catalog** を含む変更操作が利用できます。チケットは URL フラグメントだけで渡され、交換中に削除されます。永続的な管理トークンが URL や Web Storage に入ることはありません。確認済みセッションはプロセスメモリ内だけに最大 8 時間存在し、更新されません。期限切れまたはプロキシ再起動後の次の API リクエストは `401` になります。`ccx gui` または macOS メニューアプリから開き直してください。ループバックページを手動で開いても API セッションは発行されず、永続的な管理トークンを要求も送信もしません。 +`http://localhost:` で [ウェブダッシュボード](/guides/web-dashboard/) を開き、必要ならプロキシを自動起動します。短時間・1 回限りのブラウザー起動チケットにより、確認済みの **Apply agent catalog** を含む変更操作が利用できます。チケットは URL フラグメントだけで渡され、交換中に削除されます。確認済みセッションはサーバーのプロセスメモリ内で最大 8 時間有効で、ブラウザーが `sessionStorage` に複製するのはそのトークン、CSRF トークン、origin、絶対有効期限だけです。そのためサーバーセッションが有効な間は再読み込みできます。セッションは更新されません。期限切れ、プロキシ再起動、または拒否を示す `401` によりブラウザー側のレコードが消去されるため、`ccx gui` または macOS メニューアプリから開き直してください。永続的な管理トークンも起動チケットもブラウザーストレージには入らず、認証に `localStorage` は使いません。同一 origin のスクリプトはセッションレコードを読み取れるため、この利便性は OS ユーザー分離ではありません。ブラウザーはレコードを複製タブや opener から開いたタブにコピーしたり、タブ復元時に復元したりする場合があります。どのコピーも正確な origin と CSRF トークンに拘束され、固定されたサーバー有効期限、プロキシ再起動、または拒否を示す `401` までしか使用できません。ループバックページを手動で開いても API セッションは発行されず、永続的な管理トークンを要求も送信もしません。 diff --git a/docs-site/src/content/docs/ja/reference/management-api.md b/docs-site/src/content/docs/ja/reference/management-api.md index 913e12d006..7b498a4c63 100644 --- a/docs-site/src/content/docs/ja/reference/management-api.md +++ b/docs-site/src/content/docs/ja/reference/management-api.md @@ -34,7 +34,7 @@ Authorization: Bearer 手動で開いたループバックダッシュボードには API 資格情報がありません。静的なページ枠は読み込めますが、`ccx gui` または macOS メニューアプリから開き直すまで、すべての `/api/*` リクエストは `401` を返します。どのループバック hostname/address でも永続的な管理トークンを要求も送信もしません。ブラウザーの origin は listener を所有するローカル OS ユーザーを証明しないため、ループバックは認証済み listener identity でも認証の迂回路でもありません。 -ランチャーは生の管理資格情報を使い、要求ルートとオリジンに結び付いた短時間・1 回限りのチケットを発行します。チケットは URL フラグメントだけで渡され、1 回の交換中にすぐ削除されます。交換後の確認済み GUI セッションは全機能を持ち、プロセスメモリ内だけに最大 8 時間存在します。更新はされず、期限切れまたはプロキシ再起動後の次の API リクエストは `401` になり、その後はローカルランチャーの流れが再度必要です。永続的な管理トークンが URL や Web Storage に入ることはありません。 +ランチャーは生の管理資格情報を使い、要求ルートとオリジンに結び付いた短時間・1 回限りのチケットを発行します。チケットは URL フラグメントだけで渡され、1 回の交換中にすぐ削除されます。交換後の確認済み GUI セッションは全機能を持ち、サーバーのプロセスメモリ内で最大 8 時間有効です。ブラウザーが `sessionStorage` に複製するのはセッショントークン、CSRF トークン、正確な origin、絶対有効期限だけなので、サーバーセッションが有効な間は再読み込み後に復元できます。セッションは更新されません。期限切れ、プロキシ再起動、または拒否を示す `401` によりブラウザー側のレコードが消去され、その後はローカルランチャーの流れが再度必要です。永続的な管理トークンも起動チケットもブラウザーストレージには入らず、認証に `localStorage` は使いません。同一 origin のスクリプトは `sessionStorage` を読み取れるため、この利便性は OS ユーザー分離ではありません。ブラウザーはレコードを複製タブや opener から開いたタブにコピーしたり、タブ復元時に復元したりする場合があります。どのコピーも正確な origin と CSRF トークンに拘束され、固定されたサーバー有効期限、プロキシ再起動、または拒否を示す `401` までしか使用できません。 生の管理トークンは通常の API 変更に引き続き有効です。カタログ Apply は意図的に厳しく、`POST /api/codex-catalog/apply` は確認済み GUI セッションだけを受け入れます。スクリプトでは `ccx sync --restart-codex` を使用します。 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 e2e6b13730..5af0c34bd0 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -269,7 +269,9 @@ Claude Code의 `/effort` 설정은 어댑터에서도 유지돼요. | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, 그보다 크면→`high` | | `thinking.type: "disabled"` | 추론 매개변수를 모두 생략해요 | -해석된 값은 요청 로그의 **Reasoning effort** 열에 표시돼요. +요청 로그에는 해석된 요청값과, 라우팅된 adapter가 outbound 진단을 제공할 때 정확히 직렬화한 값이 +**요청 → 전송**으로 표시돼요. “전송”은 CodexCommander가 wire에 넣은 값을 확인할 뿐, 업스트림 +provider가 적용했다는 뜻은 아니에요. ## 입력 변환(Messages → Responses) diff --git a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md index ed0e8fdaf2..1df4f36aec 100644 --- a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md @@ -143,7 +143,7 @@ Keychain에서 제공자 자격 증명을 읽지도 않습니다. 메모리에만 유지하며, 신원이 확인된 루프백 CodexCommander 프로세스에만 보냅니다. 토큰을 표시, 기록, 복사 또는 저장하거나 브라우저 URL에 넣지 않습니다. -컴패니언이 대시보드를 열 때는 확인된 로컬 프록시에 수명이 짧고 일회용인 시작 티켓을 요청합니다. 티켓은 URL fragment에만 들어가며 한 번의 교환 중 제거됩니다. 영구 관리자 토큰은 URL이나 Web Storage에 들어가지 않습니다. 확인된 전체 기능 세션은 프로세스 메모리에만 최대 8시간 유지되며 갱신되지 않습니다. 만료 또는 프록시 재시작 후 다음 API 요청은 `401`을 반환하고 컴패니언이나 `ccx gui`에서 다시 열라는 안내가 표시됩니다. 직접 연 loopback 대시보드에는 API 세션이 없으며 영구 관리자 토큰을 요구하거나 전송하지 않습니다. +컴패니언이 대시보드를 열 때는 확인된 로컬 프록시에 수명이 짧고 일회용인 시작 티켓을 요청합니다. 티켓은 URL fragment에만 들어가며 한 번의 교환 중 제거됩니다. 서버는 확인된 전체 기능 세션을 프로세스 메모리에 최대 8시간 보관합니다. 브라우저는 세션 토큰, CSRF 토큰, origin, 절대 만료 시각만 `sessionStorage`에 복제하므로 서버 세션이 유효한 동안 새로고침할 수 있습니다. 세션은 갱신되지 않습니다. 만료, 프록시 재시작 또는 거부하는 `401`이 발생하면 브라우저 레코드가 지워지고 컴패니언이나 `ccx gui`에서 다시 열라는 안내가 표시됩니다. 영구 관리자 토큰과 시작 티켓은 브라우저 저장소에 들어가지 않으며 인증은 `localStorage`를 사용하지 않습니다. 같은 origin의 스크립트는 `sessionStorage`를 읽을 수 있으므로 이 새로고침 편의 기능은 OS 사용자 격리가 아닙니다. 브라우저는 레코드를 복제 탭이나 opener가 만든 탭으로 복사하거나 탭 복원 시 함께 복원할 수 있습니다. 모든 복사본은 정확한 origin과 CSRF 토큰에 계속 묶이며 고정된 서버 만료 시각, 프록시 재시작 또는 거부하는 `401`까지만 사용할 수 있습니다. 직접 연 loopback 대시보드에는 API 세션이 없으며 영구 관리자 토큰을 요구하거나 전송하지 않습니다. 제공자 자격 증명은 계속 CodexCommander가 소유합니다. 컴패니언은 ChatGPT, Kimi, Grok, Anthropic 또는 기타 제공자 토큰을 읽지 않으며 제공자 로그인 엔드포인트를 직접 호출하지 않습니다. 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 73df70d595..bed20d02cb 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -25,11 +25,13 @@ bun run dev:gui `localhost`, `*.localhost`, `127.0.0.0/8`의 모든 주소, `::1`, IPv4-mapped `127/8` 주소 등 어떤 loopback 형식으로 직접 열어도 대시보드에는 API 자격 증명이 없습니다. 페이지 틀은 로드되지만 API 요청은 인증되지 않습니다. `ccx gui` 또는 macOS 메뉴 앱에서 다시 여세요. 다른 로컬 OS 사용자가 비활성 listener를 가장할 수 있으므로 loopback 페이지는 영구 관리자 토큰을 요구하거나 전송하지 않습니다. loopback 브라우저 접근에는 확인된 런처 세션이 필요하며 인증 우회가 아닙니다. -전체 기능을 사용하려면 `ccx gui` 또는 macOS 메뉴 앱에서 여세요. 런처는 관리자 권한으로 수명이 짧고 일회용인 티켓을 발급하고 티켓만 URL fragment에 넣습니다. 대시보드는 한 번의 교환 중 즉시 이를 제거합니다. 확인된 세션은 프록시와 브라우저 프로세스 메모리에만 최대 8시간 유지되며 갱신되지 않습니다. 만료 또는 프록시 재시작 후 다음 API 요청은 `401`을 반환하며 loopback 페이지에는 새 런처 handoff가 필요합니다. 영구 관리자 토큰은 URL이나 브라우저 저장소에 들어가지 않습니다. +전체 기능을 사용하려면 `ccx gui` 또는 macOS 메뉴 앱에서 여세요. 런처는 관리자 권한으로 수명이 짧고 일회용인 티켓을 발급하고 티켓만 URL fragment에 넣습니다. 대시보드는 한 번의 교환 중 즉시 이를 제거합니다. 서버는 확인된 세션을 프로세스 메모리에 최대 8시간 보관합니다. 브라우저는 세션 토큰, CSRF 토큰, origin, 절대 만료 시각만 `sessionStorage`에 복제하므로 서버 세션이 유효한 동안 새로고침 후에도 사용할 수 있습니다. 세션은 갱신되지 않습니다. 만료, 프록시 재시작 또는 거부하는 `401`이 발생하면 브라우저 레코드가 지워지고 새 런처 handoff가 필요합니다. 영구 관리자 토큰과 시작 티켓은 브라우저 저장소에 들어가지 않으며 인증은 `localStorage`를 사용하지 않습니다. + +같은 origin의 스크립트는 `sessionStorage`를 읽을 수 있습니다. 이 새로고침 편의 기능은 OS 사용자 격리가 아니며 런처의 listener 검증이나 서버의 origin/CSRF 검증을 대체하지 않습니다. 브라우저는 레코드를 복제 탭이나 opener가 만든 탭으로 복사하거나 탭 복원 시 함께 복원할 수 있습니다. 모든 복사본은 정확한 origin과 CSRF 토큰에 계속 묶이며 고정된 서버 만료 시각, 프록시 재시작 또는 거부하는 `401`까지만 사용할 수 있습니다. loopback이 아닌 호스트에서는 `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` 또는 `~/.codexcommander/admin-api-token`의 관리자 토큰을 사용할 수 있지만 브라우저 입력창은 신뢰할 수 있는 HTTPS origin에서만 활성화됩니다. 평문 원격 페이지는 bearer를 요구하거나 전송하지 않습니다. 신뢰할 수 있는 HTTPS가 없다면 대시보드를 loopback으로 보이게 하는 로컬 또는 SSH tunnel을 사용하고 `ccx gui`에서 여세요. 원시 관리자 토큰은 headless management API client에서 계속 사용할 수 있지만 카탈로그 Apply는 확인된 로컬 대시보드 시작으로만 제한됩니다. -신뢰할 수 있는 HTTPS의 원격 대시보드는 표준 비밀번호 폼을 표시하므로 브라우저 비밀번호 관리자가 토큰 저장과 자동 완성을 제안할 수 있습니다. 대시보드 자체는 토큰을 메모리에만 보관하며 `localStorage`나 `sessionStorage`에 쓰지 않습니다. 저장 여부는 전적으로 브라우저 또는 비밀번호 관리자가 결정합니다. +신뢰할 수 있는 HTTPS의 원격 대시보드는 표준 비밀번호 폼을 표시하므로 브라우저 비밀번호 관리자가 토큰 저장과 자동 완성을 제안할 수 있습니다. 대시보드 자체는 이 원시 관리자 토큰을 메모리에만 보관하며 `localStorage`나 `sessionStorage`에 쓰지 않습니다. 저장 여부는 전적으로 브라우저 또는 비밀번호 관리자가 결정합니다. ## 할 수 있는 일 @@ -49,7 +51,7 @@ loopback이 아닌 호스트에서는 `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` 또는 ` | **Models** | 네이티브 GPT와 라우팅 모델을 켜고 끄고, 프로바이더 allowlist와 컨텍스트 상한을 설정하며, **Reliable v1**, **Codex native**, **Concurrent v2**를 선택하고 v2 thread 수를 설정합니다. Current behavior 카드는 컨텍스트를 **Uncapped**, **Limited**, **Mixed limits**로 표시합니다. 각 라우팅 프로바이더에는 **자동 검색 켜짐** 또는 **정적 카탈로그만** 상태와 해당 프로바이더 설정 링크가 표시됩니다. | | **Client Apps** | 설정된 로컬 클라이언트와 연결 가능한 클라이언트를 확인하고, 지원되는 관리 설정을 적용하거나 제거하며 백업을 검토합니다. Codex, Claude Code/Desktop, Grok Build, OpenCode와 파일 관리 클라이언트를 프로바이더와 구분해 한곳에서 찾을 수 있습니다. | | **API Access** | 다른 앱이 CodexCommander 프록시에 인증할 키를 발급하고 관리합니다. 업스트림 프로바이더 자격 증명은 Providers에 남습니다. | -| **Logs** | 토큰, 요청한 강도와 (사용 가능한 경우) 실제 전송 강도, 실제 모델, 프로바이더, 상태, 요청 id, 소요 시간, 오류 상세가 포함된 최근 요청을 자동 갱신합니다. 어댑터가 reasoning 매개변수를 전송한 경우 상세 보기에 정확한 wire field도 표시됩니다. 클라이언트가 보낸 불투명 대화/세션 id로 필터하면 현재 로드된 Logs 링의 토큰·추정 정가 합계를 볼 수 있습니다. | +| **Logs** | 토큰, 요청 → 전송 outbound 강도, 실제 모델, 프로바이더, 상태, 요청 id, 소요 시간, 오류 상세가 포함된 최근 요청을 자동 갱신합니다. 어댑터가 reasoning 매개변수를 전송한 경우 상세 보기에 정확한 전송 wire field도 표시됩니다. “전송”은 CodexCommander가 직렬화한 값이며 프로바이더가 그 강도를 수락, 준수 또는 적용했다는 증거가 아닙니다. 클라이언트가 보낸 불투명 대화/세션 id로 필터하면 현재 로드된 Logs 링의 토큰·추정 정가 합계를 볼 수 있습니다. | | **Usage / Debug** | 토큰 사용량의 측정 범위와 추이를 보거나, 선택적 프로바이더 전송/사용량 추출 진단을 켭니다. | | **Storage** | CODEX_HOME 디스크 사용량(세션, 보관, DB, 첨부)을 읽기 전용으로 표시합니다. 선택적 보관 정리: 가장 오래된 N%를 미리본 뒤 기본으로 `CODEX_HOME/.trash`에 격리하거나, 명시 체크 후 영구 삭제합니다. **자동 정리 정책**은 opt-in이며 **기본 OFF**(`storageCleanupPolicy.enabled`)입니다. Storage 페이지에서 임계값/목표/일정/모드를 설정하거나 **지금 실행**하세요. Storage 페이지에서 격리 항목을 복원할 수 있습니다(JSONL + 스레드). 활성 세션은 읽기 전용입니다. Codex가 최신/활성 `state_*.sqlite`를 잠그면 정리와 복원을 거절합니다. | | **Stop** | 연동을 OFF로 저장하고 네이티브 Codex를 복원·검증한 뒤 supervisor가 없는 프록시를 중지합니다(`POST /api/stop`). 설치된 supervisor가 소유하면 raw API는 거부합니다. manager를 먼저 중지하는 트레이 또는 CLI Stop을 사용하십시오. | diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index cfb2710e09..4aec83ac07 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -49,8 +49,9 @@ interface ProviderAdapter { ## `openai-responses` -**대상:** OpenAI **Responses API**. **`passthrough: true`** — 원본 요청 본문을 전달하고 응답을 -**변환하지 않은 채** 스트리밍합니다. +**대상:** OpenAI **Responses API**. **`passthrough: true`** — Responses 요청과 응답 형태를 +Chat Completions 모델로 변환하지 않고 유지합니다. 문서화된 호환성 정규화만 요청 형태의 예외이며, +응답은 **변환하지 않은 채** 스트리밍합니다. **인증:** `forward`(호출자 헤더 중계) 또는 `key`. `key` 인증에서는 [`retryOn429`](/ko/reference/configuration/)도 여기에 적용됩니다: 사전 스트림 @@ -60,6 +61,11 @@ interface ProviderAdapter { - `forward` URL → `{baseUrl}/responses`. `key` provider의 기본 URL은 `{baseUrl}/v1/responses`입니다. - `key` provider는 검증된 상대 `responsesPath`를 설정할 수 있습니다. adapter는 `baseUrl` 끝의 `/` 하나를 제거하고 `{trimmedBaseUrl}{responsesPath}`로 전송합니다. Ark Agent Plan은 `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"`와 `responsesPath: "/responses"`를 사용합니다. +- 최종 outbound 경계에서 인식된 provider/model reasoning 계약은 이미 존재하는 + `reasoning.effort`를 매핑하거나 클램핑합니다. 알 수 없거나 커스텀인 계약은 호출자의 값을 그대로 + 유지하며 그 임의 값을 영구 진단에 복사하지 않습니다. 인식된 계약이 만든 검증된 값만 정확히 + 전송한 field/value로 기록합니다. **전송**은 CodexCommander가 직렬화했다는 뜻이지 업스트림이 이를 + 적용했다는 확인이 아닙니다. - `forward` 모드에서는 안전한 헤더 허용 목록(`FORWARD_HEADERS`)만 중계합니다. authorization, ChatGPT account id, OpenAI beta/originator/session 헤더가 대상입니다. 이 ChatGPT 로그인 경로는 [사이드카](/ko/guides/sidecars/)에도 쓰입니다. diff --git a/docs-site/src/content/docs/ko/reference/architecture.md b/docs-site/src/content/docs/ko/reference/architecture.md index f8df483744..82b7b60e26 100644 --- a/docs-site/src/content/docs/ko/reference/architecture.md +++ b/docs-site/src/content/docs/ko/reference/architecture.md @@ -142,6 +142,13 @@ Codex 카탈로그는 Codex가 수용하는 레이블(`low` / `medium` / `high` - 커스텀 와이어 매핑을 위한 모델별 및 프로바이더별 `reasoningEffortMap` 오버라이드를 해석합니다. - `noReasoningModels`에 나열된 모델에 대해서는 effort를 완전히 제거합니다. +변환형 adapter는 요청을 구성할 때 이 계약을 적용합니다. Responses passthrough는 인식된 +provider/model 계약에 한해 최종 outbound 경계에서 적용하며, 알 수 없거나 커스텀인 Responses 계약의 +`reasoning.effort`는 그대로 유지하되 그 임의 값을 영구 진단에 복사하지 않습니다. 인식된 계약이 +검증된 값을 만든 경우에만 요청 진단에 정확한 outbound field/value를 기록하고 dashboard는 +**요청 → 전송**으로 표시합니다. “전송”은 CodexCommander가 직렬화한 값이라는 뜻이며 업스트림 +provider가 이를 적용했다는 증거가 아닙니다. + ## 코어 타입 내부 모델은 `types.ts`에 있습니다: `CodexCommanderParsedRequest`, `CodexCommanderContext`, `CodexCommanderMessage` 유니온, 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 9b5a0057ec..16a95b88c1 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -294,4 +294,4 @@ Windows 상태 트레이 아이콘을 설치하고 제어합니다. Windows 로 ### `ccx gui` 프록시가 실행 중이 아니면 자동으로 시작하면서 [웹 대시보드](/guides/web-dashboard/)를 -`http://localhost:`에서 엽니다. 수명이 짧고 일회용인 브라우저 시작 티켓으로 확인된 **Apply agent catalog**를 포함한 변경 작업을 사용할 수 있습니다. 티켓은 URL fragment로만 전달되고 교환 중 제거됩니다. 영구 관리자 토큰은 URL이나 Web Storage에 들어가지 않습니다. 확인된 세션은 프로세스 메모리에만 최대 8시간 유지되며 갱신되지 않습니다. 만료 또는 프록시 재시작 후 다음 API 요청은 `401`을 반환합니다. `ccx gui` 또는 macOS 메뉴 앱에서 다시 여세요. loopback 페이지를 직접 열면 API 세션이 발급되지 않으며 영구 관리자 토큰을 요구하거나 전송하지 않습니다. +`http://localhost:`에서 엽니다. 수명이 짧고 일회용인 브라우저 시작 티켓으로 확인된 **Apply agent catalog**를 포함한 변경 작업을 사용할 수 있습니다. 티켓은 URL fragment로만 전달되고 교환 중 제거됩니다. 확인된 세션은 서버 프로세스 메모리에서 최대 8시간 유효하며 브라우저는 토큰, CSRF 토큰, origin, 절대 만료 시각만 `sessionStorage`에 복제합니다. 따라서 서버 세션이 유효한 동안 새로고침할 수 있습니다. 세션은 갱신되지 않습니다. 만료, 프록시 재시작 또는 거부하는 `401`이 발생하면 브라우저 레코드가 지워지므로 `ccx gui` 또는 macOS 메뉴 앱에서 다시 여세요. 영구 관리자 토큰과 시작 티켓은 브라우저 저장소에 들어가지 않으며 인증은 `localStorage`를 사용하지 않습니다. 같은 origin의 스크립트는 세션 레코드를 읽을 수 있으므로 이 편의 기능은 OS 사용자 격리가 아닙니다. 브라우저는 레코드를 복제 탭이나 opener가 만든 탭으로 복사하거나 탭 복원 시 함께 복원할 수 있습니다. 모든 복사본은 정확한 origin과 CSRF 토큰에 계속 묶이며 고정된 서버 만료 시각, 프록시 재시작 또는 거부하는 `401`까지만 사용할 수 있습니다. loopback 페이지를 직접 열면 API 세션이 발급되지 않으며 영구 관리자 토큰을 요구하거나 전송하지 않습니다. diff --git a/docs-site/src/content/docs/ko/reference/management-api.md b/docs-site/src/content/docs/ko/reference/management-api.md index abc78f31b0..7e0ecc0e2c 100644 --- a/docs-site/src/content/docs/ko/reference/management-api.md +++ b/docs-site/src/content/docs/ko/reference/management-api.md @@ -34,7 +34,7 @@ Authorization: Bearer 직접 연 loopback 대시보드에는 API 자격 증명이 없습니다. 정적 페이지 틀은 로드되지만 `ccx gui` 또는 macOS 메뉴 앱에서 다시 열 때까지 모든 `/api/*` 요청은 `401`을 반환합니다. 어떤 loopback hostname/address에서도 영구 관리자 토큰을 요구하거나 전송하지 않습니다. 브라우저 origin은 listener를 소유한 로컬 OS 사용자를 증명하지 않으므로 loopback은 인증된 listener identity도 인증 우회도 아닙니다. -런처는 원시 관리자 자격 증명을 사용해 요청한 route와 origin에 묶인 수명이 짧고 일회용인 티켓을 발급합니다. 티켓은 URL fragment로만 전달되고 한 번의 교환 중 즉시 제거됩니다. 확인된 GUI 세션은 전체 기능을 제공하며 프로세스 메모리에만 최대 8시간 유지됩니다. 갱신되지 않으며 만료 또는 프록시 재시작 후 다음 API 요청은 `401`을 반환하고 로컬 런처 흐름이 다시 필요합니다. 영구 관리자 토큰은 URL이나 Web Storage에 들어가지 않습니다. +런처는 원시 관리자 자격 증명을 사용해 요청한 route와 origin에 묶인 수명이 짧고 일회용인 티켓을 발급합니다. 티켓은 URL fragment로만 전달되고 한 번의 교환 중 즉시 제거됩니다. 확인된 GUI 세션은 전체 기능을 제공하며 서버 프로세스 메모리에서 최대 8시간 유효합니다. 브라우저는 세션 토큰, CSRF 토큰, 정확한 origin, 절대 만료 시각만 `sessionStorage`에 복제하므로 서버 세션이 유효한 동안 새로고침 후 복원할 수 있습니다. 세션은 갱신되지 않습니다. 만료, 프록시 재시작 또는 거부하는 `401`이 발생하면 브라우저 레코드가 지워지고 로컬 런처 흐름이 다시 필요합니다. 영구 관리자 토큰과 시작 티켓은 브라우저 저장소에 들어가지 않으며 인증은 `localStorage`를 사용하지 않습니다. 같은 origin의 스크립트는 `sessionStorage`를 읽을 수 있으므로 이 편의 기능은 OS 사용자 격리가 아닙니다. 브라우저는 레코드를 복제 탭이나 opener가 만든 탭으로 복사하거나 탭 복원 시 함께 복원할 수 있습니다. 모든 복사본은 정확한 origin과 CSRF 토큰에 계속 묶이며 고정된 서버 만료 시각, 프록시 재시작 또는 거부하는 `401`까지만 사용할 수 있습니다. 원시 관리자 토큰은 일반 API 변경에 계속 유효합니다. 카탈로그 Apply는 더 엄격하여 `POST /api/codex-catalog/apply`가 확인된 GUI 세션만 허용합니다. 스크립트는 `ccx sync --restart-codex`를 사용합니다. diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 85e35a6a56..e60daafa97 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -55,8 +55,10 @@ provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local & cloud), ## `openai-responses` -**Targets:** the OpenAI **Responses API**. **`passthrough: true`** — forwards the raw request body and -streams the response back **untranslated**. +**Targets:** the OpenAI **Responses API**. **`passthrough: true`** — preserves the Responses request +and response shapes instead of translating them through the Chat Completions model. Documented +compatibility normalizations are the only request-shape exceptions; the response streams back +untranslated. **Auth:** `forward` (relay the caller's headers) or `key`. For `key` auth, [`retryOn429`](/reference/configuration/) applies here too: a pre-stream 429 @@ -66,6 +68,11 @@ of the HTTP retry loop. - `forward` URL → `{baseUrl}/responses`. A `key` provider defaults to `{baseUrl}/v1/responses`. - A `key` provider may set a validated relative `responsesPath`; the adapter removes one trailing slash from `baseUrl` and sends `{trimmedBaseUrl}{responsesPath}`. For Ark Agent Plan, use `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` with `responsesPath: "/responses"`. +- At the final outbound boundary, a recognized provider/model reasoning contract maps or clamps an + already-present `reasoning.effort`. Unknown and custom contracts keep the caller's value + unchanged and do not copy that arbitrary value into durable diagnostics. Only a validated value + from a recognized contract is recorded as the exact sent field/value; **sent** means serialized + by CodexCommander, not confirmation that the upstream applied it. - In `forward` mode only a safe header allowlist is relayed (`FORWARD_HEADERS`): authorization, ChatGPT account id, and the OpenAI beta/originator/session headers. This is the ChatGPT-login path that also powers the [sidecars](/guides/sidecars/). diff --git a/docs-site/src/content/docs/reference/architecture.md b/docs-site/src/content/docs/reference/architecture.md index 219936f782..f00771dcf4 100644 --- a/docs-site/src/content/docs/reference/architecture.md +++ b/docs-site/src/content/docs/reference/architecture.md @@ -166,6 +166,14 @@ upstream providers may support only a smaller subset or require a real alias. Th - Resolves per-model and per-provider `reasoningEffortMap` overrides for custom wire mappings. - Drops the effort entirely for models listed in `noReasoningModels`. +Translated adapters apply this contract while constructing their requests. The Responses +passthrough applies it at the final outbound boundary only when the provider/model contract is +recognized; unknown and custom Responses contracts keep their `reasoning.effort` unchanged without +copying that arbitrary value into durable diagnostics. When a recognized contract produces a +validated value, request diagnostics record the exact outbound field/value and the dashboard shows +**Requested → sent**. “Sent” describes what CodexCommander serialized, not proof that the upstream +provider honored it. + ## Core types The internal model lives in `types.ts`: `CodexCommanderParsedRequest`, `CodexCommanderContext`, the `CodexCommanderMessage` union, diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index ed9779b716..b174a41014 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -354,8 +354,14 @@ proxy controls. `start` and `stop` control the icon only; use its menu to contro Open the [web dashboard](/guides/web-dashboard/) at `http://localhost:`, auto-starting the proxy if it is not running. The command mints a short-lived, single-use browser launch ticket so the dashboard can make changes, including confirmed **Apply agent catalog**. The ticket travels only in -the URL fragment and is removed during exchange; the durable admin token never enters the URL or web -storage. The resulting confirmed session is process-memory-only, lasts up to eight hours, and is not -renewed. Expiry or proxy restart makes the next API request return `401`; open the page through -`ccx gui` or the macOS menu app again. Opening `localhost` manually supplies no API session and never -prompts for or sends the durable admin token. +the URL fragment and is removed during exchange. The confirmed session stays in server process memory +for up to eight hours; only its token, CSRF token, origin, and absolute expiry are mirrored in the +browser's `sessionStorage`, so refresh works while the server session remains valid. It is not +renewed. Expiry, proxy restart, or a rejecting `401` clears the browser record; open the page through +`ccx gui` or the macOS menu app again. Neither the durable admin token nor the launch ticket enters +browser storage, and authentication never uses `localStorage`. Same-origin script can read the +session record, so this convenience is not OS-user isolation. Browsers may copy the record into +duplicated or opener-created tabs, or restore it with a restored tab; every copy remains bound to the +exact origin and CSRF token and is usable only until the fixed server expiry, a proxy restart, or a +rejecting `401`. Opening `localhost` manually supplies +no API session and never prompts for or sends the durable admin token. diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index d7313753e7..6da492129a 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -49,10 +49,16 @@ token; loopback is not an authenticated listener identity or an authentication b Those launchers use the raw admin credential to mint a short-lived, single-use ticket bound to the requested route and origin. The ticket travels only in the URL fragment and is removed immediately -during its one-time exchange. The resulting confirmed GUI session is full-featured, process-memory- -only, and valid for at most eight hours. It is never renewed: expiry or proxy restart makes the next -API request return `401`, after which the local launcher flow is required again. The durable admin -token never enters a URL or web storage. +during its one-time exchange. The resulting confirmed GUI session is full-featured, kept in server +process memory, and valid for at most eight hours. The browser mirrors only its session token, CSRF +token, exact origin, and absolute expiry in `sessionStorage`, so a refresh can rehydrate it +while the server session remains valid. It is never renewed: expiry, proxy restart, or a rejecting +`401` clears that browser record, after which the local launcher flow is required again. Neither the +durable admin token nor the launch ticket enters browser storage, and authentication never uses +`localStorage`. Same-origin script can read `sessionStorage`, so this convenience is not OS-user +isolation. Browsers may copy the record into duplicated or opener-created tabs, or restore it with a +restored tab; every copy remains bound to the exact origin and CSRF token and is usable only until the +fixed server expiry, a proxy restart, or a rejecting `401`. The raw admin bearer remains valid for ordinary API mutations. Catalog Apply is deliberately stricter: `POST /api/codex-catalog/apply` accepts only a confirmed GUI session, so scripts use diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index 819d1754b1..48358aed9a 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -258,7 +258,10 @@ Claude Code — это лишь учётные данные для доступ | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`, ≤16384→`medium`, выше→`high` | | `thinking.type: "disabled"` | Параметры рассуждений полностью опускаются | -Итоговое значение отображается в столбце **Reasoning effort** журнала запросов. +Журнал запросов показывает разобранное запрошенное значение и, когда маршрутизированный адаптер +предоставляет исходящую диагностику, точное сериализованное значение как +**Запрошено → отправлено**. «Отправлено» подтверждает, что CodexCommander поместил значение в wire, +но не то, что вышестоящий provider его применил. ## Входящее преобразование (Messages → Responses) diff --git a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md index 6f6c7c4328..15a1e5d913 100644 --- a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md @@ -164,7 +164,7 @@ ccx sync --restart-codex проверку идентичности. Он никогда не показывает, не журналирует, не копирует и не сохраняет токен и не помещает его в URL браузера. -Когда компаньон открывает дашборд, он запрашивает у проверенного локального прокси краткоживущий одноразовый launch-ticket. Ticket находится только во fragment URL и удаляется во время однократного обмена; постоянный admin-token не попадает в URL или Web Storage. Подтверждённая полнофункциональная сессия хранится только в памяти процессов до восьми часов и не продлевается. После истечения срока или перезапуска прокси следующий API-запрос получает `401`, и страница предлагает открыться заново через компаньон или `ccx gui`. Вручную открытый loopback-дашборд не получает API-сессию и никогда не запрашивает и не отправляет постоянный admin-token. +Когда компаньон открывает дашборд, он запрашивает у проверенного локального прокси краткоживущий одноразовый launch-ticket. Ticket находится только во fragment URL и удаляется во время однократного обмена. Сервер хранит подтверждённую полнофункциональную сессию в памяти процесса до восьми часов. Браузер копирует в `sessionStorage` только session-token, CSRF-token, origin и абсолютное время истечения, поэтому перезагрузка работает, пока серверная сессия действительна. Сессия не продлевается. Истечение срока, перезапуск прокси или отклоняющий `401` очищает browser-запись и предлагает открыться заново через компаньон или `ccx gui`. Ни постоянный admin-token, ни launch-ticket не попадают в browser storage; для аутентификации не используется `localStorage`. Скрипт того же origin может читать `sessionStorage`, поэтому это удобство перезагрузки не является изоляцией пользователей ОС. Браузер может скопировать запись в дублированную или созданную opener'ом вкладку либо восстановить её вместе с восстановленной вкладкой. Каждая копия остаётся привязанной к точным origin и CSRF-token и пригодна только до фиксированного серверного срока, перезапуска прокси или отклоняющего `401`. Вручную открытый loopback-дашборд не получает API-сессию и никогда не запрашивает и не отправляет постоянный admin-token. Учётные данные провайдеров остаются под управлением CodexCommander. Компаньон никогда не читает токены ChatGPT, Kimi, Grok, Anthropic или других провайдеров и никогда напрямую не вызывает diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index 0aa1525631..070946d342 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -25,11 +25,13 @@ bun run dev:gui Дашборд, открытый вручную через любую loopback-форму — `localhost`, `*.localhost`, любой адрес из `127.0.0.0/8`, `::1` или IPv4-mapped адрес `127/8` — не получает API-credential. Оболочка страницы загрузится, но API-запросы останутся неаутентифицированными. Откройте страницу заново через `ccx gui` либо приложение строки меню macOS. Другой локальный пользователь ОС может подменить неактивный listener, поэтому loopback-страница никогда не запрашивает и не отправляет постоянный admin-token. Для browser-доступа по loopback нужна подтверждённая launcher-сессия; это не обход аутентификации. -Для полной функциональности откройте дашборд через `ccx gui` или приложение строки меню macOS. Launcher с правами администратора выпускает краткоживущий одноразовый ticket и помещает во fragment URL только его; дашборд немедленно удаляет ticket во время однократного обмена. Подтверждённая сессия хранится только в памяти процессов прокси и браузера не более восьми часов и не продлевается. После истечения срока или перезапуска прокси следующий API-запрос получает `401`, и loopback-странице нужен новый launcher handoff. Постоянный admin-token не попадает в URL или хранилище браузера. +Для полной функциональности откройте дашборд через `ccx gui` или приложение строки меню macOS. Launcher с правами администратора выпускает краткоживущий одноразовый ticket и помещает во fragment URL только его; дашборд немедленно удаляет ticket во время однократного обмена. Сервер хранит подтверждённую сессию в памяти процесса не более восьми часов. Браузер копирует в `sessionStorage` только session-token, CSRF-token, origin и абсолютное время истечения, поэтому перезагрузка работает, пока серверная сессия действительна. Сессия не продлевается. Истечение срока, перезапуск прокси или отклоняющий `401` очищает browser-запись и требует нового launcher handoff. Ни постоянный admin-token, ни launch-ticket не попадают в browser storage; для аутентификации не используется `localStorage`. + +Скрипт того же origin может читать `sessionStorage`. Поэтому это удобство перезагрузки не является изоляцией пользователей ОС и не заменяет проверку listener'а launcher'ом либо проверки origin и CSRF на сервере. Браузер может скопировать запись в дублированную или созданную opener'ом вкладку либо восстановить её вместе с восстановленной вкладкой. Каждая копия остаётся привязанной к точным origin и CSRF-token и пригодна только до фиксированного серверного срока, перезапуска прокси или отклоняющего `401`. На не-loopback хосте можно использовать admin-token из `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` или `~/.codexcommander/admin-api-token`, но browser-форма включается только на доверенном HTTPS origin. Страница по plaintext remote никогда не запрашивает и не отправляет bearer. Если доверенного HTTPS нет, используйте локальный или SSH tunnel, который представляет дашборд как loopback, и откройте его через `ccx gui`. Сырой admin-token остаётся доступен headless-клиентам management API, но Apply каталога допускается только из подтверждённого локального запуска дашборда. -Удалённый дашборд на доверенном HTTPS показывает стандартную форму пароля, поэтому менеджер паролей браузера может предложить сохранить и автозаполнять token. Сам дашборд хранит его только в памяти и не записывает в `localStorage` или `sessionStorage`; решение о сохранении полностью остаётся за браузером или менеджером паролей. +Удалённый дашборд на доверенном HTTPS показывает стандартную форму пароля, поэтому менеджер паролей браузера может предложить сохранить и автозаполнять token. Сам дашборд хранит этот сырой admin-token только в памяти и не записывает в `localStorage` или `sessionStorage`; решение о сохранении полностью остаётся за браузером или менеджером паролей. ## Возможности @@ -49,7 +51,7 @@ bun run dev:gui | **Models** | Включение и отключение нативных GPT и маршрутизируемых моделей, настройка allowlist'ов провайдеров и лимитов контекста, выбор **Reliable v1**, **Codex native** или **Concurrent v2** и настройка лимита потоков v2. Карточка Current behavior показывает контекст как **Uncapped**, **Limited** или **Mixed limits**. Для каждого маршрутизируемого провайдера отображается **Автообнаружение включено** или **Только статический каталог** со ссылкой на соответствующую настройку провайдера. | | **Client Apps** | Просмотр настроенных и доступных локальных клиентов, применение или удаление управляемой конфигурации там, где это поддерживается, проверка резервных копий и доступ к Codex, Claude Code/Desktop, Grok Build, OpenCode и файловым клиентам без смешения клиентов с провайдерами. | | **API Access** | Выпуск и управление ключами, которыми другие приложения аутентифицируются в прокси CodexCommander. Учётные данные вышестоящих провайдеров остаются в Providers. | -| **Logs** | Автообновляемый список недавних запросов: токены, запрошенный и, когда доступен, фактически отправленный уровень рассуждений, фактическая модель, провайдер, статус, id запроса, длительность и подробности ошибок. Если адаптер отправляет параметр рассуждений, в подробностях также отображается точное wire-поле. Можно фильтровать по непрозрачному id диалога/сессии (если клиент его передаёт) и суммировать токены и оценочную стоимость по прайс-листу в пределах загруженного кольца Logs. | +| **Logs** | Автообновляемый список недавних запросов: токены, уровень рассуждений «запрошен → отправлен» во внешнем запросе, фактическая модель, провайдер, статус, id запроса, длительность и подробности ошибок. Если адаптер отправляет параметр рассуждений, в подробностях также отображается точное отправленное wire-поле. «Отправлен» означает значение, сериализованное CodexCommander, и не доказывает, что провайдер принял, соблюдал или применил этот уровень. Можно фильтровать по непрозрачному id диалога/сессии (если клиент его передаёт) и суммировать токены и оценочную стоимость по прайс-листу в пределах загруженного кольца Logs. | | **Usage / Debug** | Просмотр покрытия и трендов расхода токенов либо включение опциональной диагностики транспорта провайдеров и извлечения данных об использовании. | | **Storage** | Только чтение разбивки диска CODEX_HOME (сессии, архивы, БД, вложения). Опциональная очистка архива: предпросмотр самых старых N%, затем карантин в `CODEX_HOME/.trash` (по умолчанию) или безвозвратное удаление по явному флажку. **Политика автоочистки** — opt-in и **по умолчанию ВЫКЛ** (`storageCleanupPolicy.enabled`); порог/цель/расписание/режим на странице Storage или **Запустить сейчас**. Записи карантина можно восстановить со страницы Storage (JSONL + threads). Активные сессии только для чтения. Очистка и восстановление отклоняются, пока Codex держит блокировку новейшего/активного `state_*.sqlite`. | | **Stop** | Сохранить интеграцию OFF, восстановить и проверить native Codex, затем остановить прокси без supervisor (`POST /api/stop`). Если им владеет установленный supervisor, raw API отказывает; используйте Stop в трее или CLI, чтобы сначала остановить manager. | diff --git a/docs-site/src/content/docs/ru/reference/adapters.md b/docs-site/src/content/docs/ru/reference/adapters.md index 6daa6d8ef0..0b69970fa1 100644 --- a/docs-site/src/content/docs/ru/reference/adapters.md +++ b/docs-site/src/content/docs/ru/reference/adapters.md @@ -53,8 +53,10 @@ interface ProviderAdapter { ## `openai-responses` -**Назначение:** OpenAI **Responses API**. **`passthrough: true`** — пересылает исходное тело -запроса и стримит ответ обратно **без преобразования**. +**Назначение:** OpenAI **Responses API**. **`passthrough: true`** — сохраняет форму запроса и +ответа Responses без преобразования через модель Chat Completions. Единственные исключения для +формы запроса — документированные нормализации совместимости; ответ стримится обратно +**без преобразования**. **Аутентификация:** `forward` (ретрансляция заголовков вызывающей стороны) или `key`. При `key`-аутентификации [`retryOn429`](/ru/reference/configuration/) действует и здесь: 429 до @@ -64,6 +66,12 @@ interface ProviderAdapter { - URL для `forward` → `{baseUrl}/responses`. URL по умолчанию для провайдера с `key` — `{baseUrl}/v1/responses`. - Провайдер с `key` может задать проверенный относительный `responsesPath`: адаптер удаляет один завершающий `/` из `baseUrl` и отправляет запрос на `{trimmedBaseUrl}{responsesPath}`. Для Ark Agent Plan используйте `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` и `responsesPath: "/responses"`. +- На последней исходящей границе известный контракт reasoning для provider/model отображает или + ограничивает уже присутствующий `reasoning.effort`. Для неизвестных и пользовательских + контрактов значение вызывающей стороны остаётся неизменным и не копируется в постоянную + диагностику. Только проверенное значение известного контракта записывается как точные + отправленные field/value. **Отправлено** означает, что CodexCommander сериализовал значение, + а не подтверждает его применение вышестоящим сервисом. - В режиме `forward` ретранслируется только безопасный allowlist заголовков (`FORWARD_HEADERS`): authorization, ChatGPT account id и заголовки OpenAI beta/originator/session. Это путь входа через ChatGPT, на котором также работают [сайдкары](/ru/guides/sidecars/). diff --git a/docs-site/src/content/docs/ru/reference/architecture.md b/docs-site/src/content/docs/ru/reference/architecture.md index 22cdaf4f5c..f6ced18657 100644 --- a/docs-site/src/content/docs/ru/reference/architecture.md +++ b/docs-site/src/content/docs/ru/reference/architecture.md @@ -180,6 +180,14 @@ Compaction контекста Codex работает для маршрутизи wire-отображений. - Полностью убирает уровень для моделей, перечисленных в `noReasoningModels`. +Преобразующие адаптеры применяют этот контракт при построении запроса. Passthrough Responses +применяет его на последней исходящей границе только для известного контракта provider/model; +`reasoning.effort` неизвестного или пользовательского контракта Responses остаётся неизменным и не +копируется в постоянную диагностику. Только когда известный контракт выдаёт проверенное значение, +диагностика запроса записывает точные исходящие field/value, а dashboard показывает +**Запрошено → отправлено**. «Отправлено» описывает значение, сериализованное CodexCommander, но не +доказывает, что вышестоящий provider его применил. + ## Основные типы Внутренняя модель живёт в `types.ts`: `CodexCommanderParsedRequest`, `CodexCommanderContext`, объединение `CodexCommanderMessage`, diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index 6756826006..12e2c2a802 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -316,4 +316,4 @@ one-click управление прокси. `start` и `stop` управляю ### `ccx gui` Открыть [веб-дашборд](/guides/web-dashboard/) по адресу `http://localhost:`, автоматически -запустив прокси, если он ещё не работает. Краткоживущий одноразовый browser launch-ticket открывает изменения, включая подтверждённый **Apply agent catalog**. Ticket передаётся только во fragment URL и удаляется во время обмена; постоянный admin-token не попадает в URL или Web Storage. Подтверждённая сессия хранится только в памяти процессов до восьми часов и не продлевается. После истечения срока или перезапуска прокси следующий API-запрос получает `401`; откройте страницу снова через `ccx gui` или приложение строки меню macOS. При ручном открытии loopback-страницы API-сессия не выдаётся, и постоянный admin-token никогда не запрашивается и не отправляется. +запустив прокси, если он ещё не работает. Краткоживущий одноразовый browser launch-ticket открывает изменения, включая подтверждённый **Apply agent catalog**. Ticket передаётся только во fragment URL и удаляется во время обмена. Подтверждённая сессия хранится в памяти серверного процесса до восьми часов; браузер копирует в `sessionStorage` только её token, CSRF-token, origin и абсолютное время истечения. Поэтому перезагрузка работает, пока серверная сессия действительна. Сессия не продлевается. Истечение срока, перезапуск прокси или отклоняющий `401` очищает browser-запись; откройте страницу снова через `ccx gui` или приложение строки меню macOS. Ни постоянный admin-token, ни launch-ticket не попадают в browser storage; для аутентификации не используется `localStorage`. Скрипт того же origin может читать session-запись, поэтому это удобство не является изоляцией пользователей ОС. Браузер может скопировать запись в дублированную или созданную opener'ом вкладку либо восстановить её вместе с восстановленной вкладкой. Каждая копия остаётся привязанной к точным origin и CSRF-token и пригодна только до фиксированного серверного срока, перезапуска прокси или отклоняющего `401`. При ручном открытии loopback-страницы API-сессия не выдаётся, и постоянный admin-token никогда не запрашивается и не отправляется. diff --git a/docs-site/src/content/docs/ru/reference/management-api.md b/docs-site/src/content/docs/ru/reference/management-api.md index 6527d8d617..ea43e47d9b 100644 --- a/docs-site/src/content/docs/ru/reference/management-api.md +++ b/docs-site/src/content/docs/ru/reference/management-api.md @@ -45,7 +45,7 @@ Claude Code или любого другого клиента моделей; о Вручную открытый loopback-дашборд не получает API-credential. Статическая оболочка страницы может загрузиться, но каждый запрос `/api/*` возвращает `401`, пока страницу не откроют заново через `ccx gui` или приложение строки меню macOS. Ни на одном loopback hostname/address страница не запрашивает и не отправляет постоянный admin-token. Browser origin не доказывает, какой локальный пользователь ОС владеет listener'ом, поэтому loopback не является ни аутентифицированной listener identity, ни обходом аутентификации. -Launcher использует сырой admin credential, чтобы выпустить краткоживущий одноразовый ticket, привязанный к запрошенному route и origin. Ticket передаётся только во fragment URL и немедленно удаляется во время однократного обмена. Полученная подтверждённая GUI-сессия полнофункциональна, хранится только в памяти процессов и действует не более восьми часов. Она не продлевается: после истечения срока или перезапуска прокси следующий API-запрос получает `401`, после чего снова нужен локальный launcher flow. Постоянный admin-token не попадает в URL или Web Storage. +Launcher использует сырой admin credential, чтобы выпустить краткоживущий одноразовый ticket, привязанный к запрошенному route и origin. Ticket передаётся только во fragment URL и немедленно удаляется во время однократного обмена. Полученная подтверждённая GUI-сессия полнофункциональна, хранится в памяти серверного процесса и действует не более восьми часов. Браузер копирует в `sessionStorage` только session-token, CSRF-token, точный origin и абсолютное время истечения, поэтому может восстановить сессию после перезагрузки, пока серверная сессия действительна. Сессия не продлевается: истечение срока, перезапуск прокси или отклоняющий `401` очищает browser-запись, после чего снова нужен локальный launcher flow. Ни постоянный admin-token, ни launch-ticket не попадают в browser storage; для аутентификации не используется `localStorage`. Скрипт того же origin может читать `sessionStorage`, поэтому это удобство не является изоляцией пользователей ОС. Браузер может скопировать запись в дублированную или созданную opener'ом вкладку либо восстановить её вместе с восстановленной вкладкой. Каждая копия остаётся привязанной к точным origin и CSRF-token и пригодна только до фиксированного серверного срока, перезапуска прокси или отклоняющего `401`. Сырой admin-token сохраняет право на обычные API-мутации. Apply каталога намеренно строже: `POST /api/codex-catalog/apply` принимает только подтверждённую GUI-сессию. Для скриптов используйте `ccx sync --restart-codex`. diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 5c514cdd5b..eeeb98853f 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -230,7 +230,9 @@ Claude Code 的 `/effort` 设置会完整保留并传递给适配器: | `thinking.type: "enabled"` + `budget_tokens` | ≤4096→`low`,≤16384→`medium`,更高→`high` | | `thinking.type: "disabled"` | 完全省略推理参数 | -解析后的值会显示在请求日志的 **Reasoning effort** 列中。 +请求日志会显示解析后的请求值;如果路由 adapter 提供 outbound 诊断,还会在 +**请求 → 已发送**下显示实际序列化的值。“已发送”只确认 CodexCommander 放到了 wire 上, +并不表示上游 provider 已应用该值。 ## 入站转换(Messages → Responses) diff --git a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md index 566105d16a..4bf56419e5 100644 --- a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md @@ -124,7 +124,7 @@ ccx sync --restart-codex 验证的回环 CodexCommander 进程。它绝不会显示、记录、复制或存储该令牌,也不会将其放入浏览器 URL。 -伴侣打开仪表盘时,会向经过验证的本地代理请求一个短期、一次性启动票据。票据只出现在 URL fragment 中,并在一次性交换过程中清除;长期管理员 token 不会进入 URL 或 Web Storage。确认的完整功能 session 只存在于进程内存中,最长八小时,且不会续期。到期或代理重启后的下一个 API 请求会返回 `401`,页面会提示通过伴侣或 `ccx gui` 重新打开。手动打开的 loopback 仪表盘没有 API session,也绝不会请求或发送长期管理员 token。 +伴侣打开仪表盘时,会向经过验证的本地代理请求一个短期、一次性启动票据。票据只出现在 URL fragment 中,并在一次性交换过程中清除。服务器在进程内存中保留确认的完整功能 session,最长八小时。浏览器仅把 session token、CSRF token、origin 和绝对到期时间镜像到同一标签页的 `sessionStorage`,所以服务器 session 有效时可以刷新。session 不会续期。到期、代理重启或拒绝请求的 `401` 会清除浏览器记录,并提示通过伴侣或 `ccx gui` 重新打开。长期管理员 token 和启动票据都不会进入浏览器存储,认证也绝不使用 `localStorage`。同源脚本可以读取 `sessionStorage`,因此这项刷新便利性不构成 OS 用户隔离。手动打开的 loopback 仪表盘没有 API session,也绝不会请求或发送长期管理员 token。 提供商凭据仍由 CodexCommander 管理。伴侣绝不会读取 ChatGPT、Kimi、Grok、Anthropic 或其他 提供商令牌,也绝不会直接调用提供商登录端点。 diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 7569c0e90b..ef4ebe7d4f 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -24,11 +24,13 @@ bun run dev:gui 手动通过任何 loopback 形式打开的仪表盘都不会获得 API 凭证,包括 `localhost`、`*.localhost`、`127.0.0.0/8` 中的任意地址、`::1` 和 IPv4-mapped `127/8` 地址。页面框架可以加载,但 API 请求仍未通过身份验证。请通过 `ccx gui` 或 macOS 菜单栏应用重新打开。由于另一个本地 OS 用户可以冒充未使用端口上的 listener,loopback 页面绝不会请求或发送长期管理员 token。loopback 浏览器访问需要确认的 launcher session,不能绕过身份验证。 -要使用完整功能,请通过 `ccx gui` 或 macOS 菜单栏应用打开。launcher 使用管理员权限签发一个短期、一次性票据,只把票据放入 URL fragment;仪表盘会在一次性交换过程中立即将其清除。确认 session 只存在于代理和浏览器进程内存中,最长八小时,且不会续期。到期或代理重启后的下一个 API 请求会返回 `401`,loopback 页面需要新的 launcher handoff。长期管理员 token 绝不会进入 URL 或浏览器存储。 +要使用完整功能,请通过 `ccx gui` 或 macOS 菜单栏应用打开。launcher 使用管理员权限签发一个短期、一次性票据,只把票据放入 URL fragment;仪表盘会在一次性交换过程中立即将其清除。服务器在进程内存中保留确认 session,最长八小时。浏览器仅把 session token、CSRF token、origin 和绝对到期时间镜像到当前标签页的 `sessionStorage`,所以服务器 session 有效时,刷新后仍可继续使用。session 不会续期。到期、代理重启或拒绝请求的 `401` 会清除浏览器记录,并要求新的 launcher handoff。长期管理员 token 和启动票据都不会进入浏览器存储,认证也绝不使用 `localStorage`。 + +同源脚本可以读取 `sessionStorage`。因此,这项刷新便利性不构成 OS 用户隔离,也不能取代 launcher 的 listener 验证或服务器的 origin/CSRF 检查。 非 loopback 主机可以使用 `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` 或 `~/.codexcommander/admin-api-token` 中的管理员 token,但浏览器输入框仅在受信任的 HTTPS origin 上启用。明文远程页面绝不会请求或发送 bearer。若没有受信任的 HTTPS,请使用把仪表盘呈现为 loopback 的本地或 SSH tunnel,并通过 `ccx gui` 打开。原始管理员 token 仍可供 headless management API client 使用,但 catalog Apply 只允许来自确认的本地仪表盘启动。 -受信任 HTTPS 上的远程仪表盘会显示标准密码表单,浏览器密码管理器可以提示保存并自动填充 token。仪表盘本身只在内存中保存 token,不会写入 `localStorage` 或 `sessionStorage`;是否持久保存完全由浏览器或密码管理器决定。 +受信任 HTTPS 上的远程仪表盘会显示标准密码表单,浏览器密码管理器可以提示保存并自动填充 token。仪表盘本身只在内存中保存这个原始管理员 token,不会写入 `localStorage` 或 `sessionStorage`;是否持久保存完全由浏览器或密码管理器决定。 ## 可以完成哪些操作 @@ -48,7 +50,7 @@ bun run dev:gui | **Models** | 开关原生 GPT 与路由模型,配置 provider allowlist 和上下文上限,选择 **Reliable v1**、**Codex native** 或 **Concurrent v2**,并设置 v2 thread 数量。Current behavior 卡片会将上下文显示为 **Uncapped**、**Limited** 或 **Mixed limits**。每个路由 provider 都会显示 **自动发现已开启** 或 **仅静态目录**,并链接到对应的 provider 设置。 | | **Client Apps** | 查看已配置和可连接的本地客户端;在支持时应用或移除托管配置并检查备份;集中访问 Codex、Claude Code/Desktop、Grok Build、OpenCode 及文件托管客户端,同时避免把客户端与提供商混为一谈。 | | **API Access** | 签发和管理其他应用连接 CodexCommander 代理时使用的认证密钥。上游提供商凭据仍归 Providers 管理。 | -| **Logs** | 自动刷新近期请求,显示 token、请求强度以及(可用时)实际发送强度、实际模型、provider、状态、request id、耗时和错误详情。适配器发送 reasoning 参数时,详情中还会显示准确的 wire field。可按不透明会话/对话 ID(客户端提供时)筛选,并对当前已加载的 Logs 环形缓冲合计 token 与估算标价成本。 | +| **Logs** | 自动刷新近期请求,显示 token、请求 → 发送的 outbound 强度、实际模型、provider、状态、request id、耗时和错误详情。适配器发送 reasoning 参数时,详情中还会显示准确的已发送 wire field。“发送”表示 CodexCommander 序列化的值,不能证明 provider 已接受、遵循或应用该强度。可按不透明会话/对话 ID(客户端提供时)筛选,并对当前已加载的 Logs 环形缓冲合计 token 与估算标价成本。 | | **Usage / Debug** | 查看 token usage 覆盖率与趋势,或启用可选的 provider transport 和 usage 提取诊断。 | | **Storage** | 只读查看 CODEX_HOME 磁盘占用(会话、归档、数据库、附件)。可选归档清理:预览最旧 N%,默认隔离到 `CODEX_HOME/.trash`,或勾选后永久删除。**自动清理策略**为可选且**默认关闭**(`storageCleanupPolicy.enabled`);可在 Storage 页配置阈值/目标/计划/模式,或点「立即运行」。可在 Storage 页从隔离区恢复(JSONL + 线程)。活动会话保持只读。Codex 锁定最新/活动的 `state_*.sqlite` 时拒绝清理与恢复。 | | **Stop** | 将集成保存为 OFF,恢复并验证原生 Codex,再停止没有 supervisor 的代理(`POST /api/stop`)。若已安装 supervisor 占有代理,原始 API 会拒绝;请使用托盘或 CLI Stop,让该流程先停止 manager。 | diff --git a/docs-site/src/content/docs/zh-cn/reference/adapters.md b/docs-site/src/content/docs/zh-cn/reference/adapters.md index a6b5cb55bd..45fedead3a 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -46,7 +46,8 @@ interface ProviderAdapter { ## `openai-responses` -**目标:** OpenAI **Responses API**。**`passthrough: true`** —— 转发原始请求 body,并把响应 +**目标:** OpenAI **Responses API**。**`passthrough: true`** —— 保持 Responses 请求和响应的 +结构,不经 Chat Completions 模型转换。只有已记录的兼容性规范化会改变请求结构;响应仍 **不经转换**地流式传回。 **认证:** `forward`(转发调用方 header)或 `key`。 @@ -56,6 +57,10 @@ interface ProviderAdapter { - `forward` URL → `{baseUrl}/responses`。`key` provider 的默认 URL 是 `{baseUrl}/v1/responses`。 - `key` provider 可设置经过验证的相对 `responsesPath`;adapter 会移除 `baseUrl` 末尾的一个 `/`,并向 `{trimmedBaseUrl}{responsesPath}` 发送请求。Ark Agent Plan 使用 `baseUrl: "https://ark.cn-beijing.volces.com/api/plan/v3"` 和 `responsesPath: "/responses"`。 +- 在最终 outbound 边界,已识别的 provider/model reasoning 契约会映射或限制已存在的 + `reasoning.effort`。未知或自定义契约会原样保留调用方的值,但不会把该任意值复制到持久 + 诊断。只有已识别契约生成的验证值才会作为实际发送的 field/value 记录;**已发送**表示 + CodexCommander 完成了序列化,并不证明上游已应用该值。 - `forward` 模式只会转发安全的 header allowlist(`FORWARD_HEADERS`):authorization、ChatGPT account id 和 OpenAI beta/originator/session header。这条 ChatGPT 登录路径也为 [sidecar](/zh-cn/guides/sidecars/) 提供支持。 diff --git a/docs-site/src/content/docs/zh-cn/reference/architecture.md b/docs-site/src/content/docs/zh-cn/reference/architecture.md index 0b7ea27d11..09afd10793 100644 --- a/docs-site/src/content/docs/zh-cn/reference/architecture.md +++ b/docs-site/src/content/docs/zh-cn/reference/architecture.md @@ -156,6 +156,12 @@ Codex context compaction 同样适用于路由模型。`server/responses/compact - 解析模型级和 provider 级 `reasoningEffortMap` override,用于自定义 wire 映射。 - 对 `noReasoningModels` 中的模型完全移除 effort。 +转换型 adapter 会在构建请求时应用此契约。Responses passthrough 只会针对已识别的 +provider/model 契约,在最终 outbound 边界应用它;未知或自定义 Responses 契约的 +`reasoning.effort` 会保持不变,但不会把该任意值复制到持久诊断。只有已识别契约生成验证值时, +请求诊断才会记录准确的 outbound field/value,dashboard 则显示 **请求 → 已发送**。“已发送” +描述 CodexCommander 序列化到 wire 的值,并不证明上游 provider 已应用该值。 + ## 核心类型 内部模型位于 `types.ts`:`CodexCommanderParsedRequest`、`CodexCommanderContext`、`CodexCommanderMessage` 联合类型、 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index 74dc38829b..1a7c475e57 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -207,4 +207,4 @@ ccx codex-shim uninstall ### `ccx gui` -在 `http://localhost:` 打开 [web dashboard](/guides/web-dashboard/),如果代理未运行则会自动启动。短期、一次性的浏览器启动票据会解锁更改操作,包括确认后的 **Apply agent catalog**。票据只通过 URL fragment 传递,并在交换过程中清除;长期管理员 token 不会进入 URL 或 Web Storage。确认 session 只存在于进程内存中,最长八小时,且不会续期。到期或代理重启后的下一个 API 请求会返回 `401`;请通过 `ccx gui` 或 macOS 菜单栏应用重新打开。手动打开 loopback 页面不会获得 API session,也绝不会请求或发送长期管理员 token。 +在 `http://localhost:` 打开 [web dashboard](/guides/web-dashboard/),如果代理未运行则会自动启动。短期、一次性的浏览器启动票据会解锁更改操作,包括确认后的 **Apply agent catalog**。票据只通过 URL fragment 传递,并在交换过程中清除。确认 session 由服务器在进程内存中保留,最长八小时;浏览器仅把 token、CSRF token、origin 和绝对到期时间镜像到当前标签页的 `sessionStorage`。因此服务器 session 有效时可以刷新。session 不会续期。到期、代理重启或拒绝请求的 `401` 会清除浏览器记录;请通过 `ccx gui` 或 macOS 菜单栏应用重新打开。长期管理员 token 和启动票据都不会进入浏览器存储,认证也绝不使用 `localStorage`。同源脚本可以读取 session 记录,因此这项便利性不构成 OS 用户隔离。手动打开 loopback 页面不会获得 API session,也绝不会请求或发送长期管理员 token。 diff --git a/docs-site/src/content/docs/zh-cn/reference/management-api.md b/docs-site/src/content/docs/zh-cn/reference/management-api.md index 5ab8dcf315..1057d1381a 100644 --- a/docs-site/src/content/docs/zh-cn/reference/management-api.md +++ b/docs-site/src/content/docs/zh-cn/reference/management-api.md @@ -34,7 +34,7 @@ Authorization: Bearer 手动打开的 loopback 仪表盘不会获得 API 凭证。静态页面框架可以加载,但在通过 `ccx gui` 或 macOS 菜单栏应用重新打开之前,每个 `/api/*` 请求都会返回 `401`。任何 loopback hostname/address 都不会请求或发送长期管理员 token。浏览器 origin 无法证明哪个本地 OS 用户拥有 listener,因此 loopback 既不是经过身份验证的 listener identity,也不能绕过身份验证。 -launcher 使用原始管理员凭证签发一个绑定到请求 route 和 origin 的短期、一次性票据。票据只通过 URL fragment 传递,并在一次性交换过程中立即清除。得到的确认 GUI session 功能完整,只存在于进程内存中,最长八小时。它不会续期:到期或代理重启后的下一个 API 请求会返回 `401`,之后需要再次使用本地 launcher 流程。长期管理员 token 不会进入 URL 或 Web Storage。 +launcher 使用原始管理员凭证签发一个绑定到请求 route 和 origin 的短期、一次性票据。票据只通过 URL fragment 传递,并在一次性交换过程中立即清除。得到的确认 GUI session 功能完整,由服务器在进程内存中保留,最长八小时。浏览器仅把 session token、CSRF token、准确 origin 和绝对到期时间镜像到同一标签页的 `sessionStorage`,所以服务器 session 有效时可以在刷新后恢复。session 不会续期:到期、代理重启或拒绝请求的 `401` 会清除浏览器记录,之后需要再次使用本地 launcher 流程。长期管理员 token 和启动票据都不会进入浏览器存储,认证也绝不使用 `localStorage`。同源脚本可以读取 `sessionStorage`,因此这项便利性不构成 OS 用户隔离。 原始管理员 token 仍可执行普通 API 更改。catalog Apply 会更严格:`POST /api/codex-catalog/apply` 只接受确认 GUI session。脚本请使用 `ccx sync --restart-codex`。 diff --git a/gui/src/api.ts b/gui/src/api.ts index 8ba64c27a6..413f12e25c 100644 --- a/gui/src/api.ts +++ b/gui/src/api.ts @@ -21,6 +21,33 @@ const GUI_LAUNCH_TICKET_PARAM = "ccx-launch-ticket"; const GUI_LAUNCH_ROUTE_PARAM = "ccx-route"; /** Safe authenticated read used to validate a raw admin token before closing the sign-in form. */ const ADMIN_TOKEN_VALIDATION_PATH = "/api/settings"; +const CONFIRMED_GUI_SESSION_STORAGE_KEY = "codexcommander.confirmed-gui-session.v1"; +const CONFIRMED_GUI_SESSION_STORAGE_VERSION = 1 as const; +const CONFIRMED_GUI_SESSION_MAX_TTL_MS = 8 * 60 * 60_000; +const CONFIRMED_GUI_SESSION_EXPIRY_SKEW_MS = 60_000; +const GUI_SESSION_TOKEN_PATTERN = /^ccx_session_[A-Za-z0-9_-]{43}$/; +const GUI_SESSION_CSRF_PATTERN = /^[A-Za-z0-9_-]{43}$/; + +interface ConfirmedGuiSession { + token: string; + csrfToken: string; + origin: string; + expiresAt: number; + confirmedLaunch: true; +} + +interface StoredConfirmedGuiSession extends ConfirmedGuiSession { + version: typeof CONFIRMED_GUI_SESSION_STORAGE_VERSION; +} + +const STORED_CONFIRMED_GUI_SESSION_KEYS = new Set([ + "version", + "token", + "csrfToken", + "origin", + "expiresAt", + "confirmedLaunch", +]); /** * Loopback is not an authenticated browser origin: another local OS user can @@ -72,10 +99,15 @@ function needsApiAuth(input: RequestInfo | URL): boolean { } } -/** In-memory only — never write tokens to web storage (XSS can read sessionStorage/localStorage). */ +/** + * Raw admin credentials remain memory-only. A narrowly scoped, server-minted + * confirmed GUI session may also be mirrored to this tab's sessionStorage so + * a same-tab reload does not discard an otherwise-live eight-hour session. + */ let memoryToken: string | null = null; let memoryCsrfToken: string | null = null; let memorySessionOrigin: string | null = null; +let memorySessionExpiresAt: number | null = null; let memoryConfirmedGuiLaunch = false; let memoryAdminCredential = false; let guiLaunchCapabilityReady: Promise = Promise.resolve(false); @@ -93,12 +125,122 @@ function setAdminCredential(admin: boolean): void { for (const listener of guiLaunchCapabilityListeners) listener(); } +function pageSessionStorage(): Storage | null { + try { + return window.sessionStorage; + } catch { + return null; + } +} + +function clearStoredConfirmedGuiSession(): void { + try { + pageSessionStorage()?.removeItem(CONFIRMED_GUI_SESSION_STORAGE_KEY); + } catch { + // Disabled or policy-blocked storage keeps the dashboard memory-only. + } +} + +function isValidSessionExpiry(expiresAt: unknown, now = Date.now()): expiresAt is number { + return typeof expiresAt === "number" + && Number.isSafeInteger(expiresAt) + && expiresAt > now + && expiresAt <= now + CONFIRMED_GUI_SESSION_MAX_TTL_MS + CONFIRMED_GUI_SESSION_EXPIRY_SKEW_MS; +} + +function parseConfirmedGuiSession(value: unknown, now = Date.now()): ConfirmedGuiSession | null { + if (value === null || typeof value !== "object" || Array.isArray(value)) return null; + const record = value as Record; + if (record.confirmedLaunch !== true + || typeof record.token !== "string" + || !GUI_SESSION_TOKEN_PATTERN.test(record.token) + || typeof record.csrfToken !== "string" + || !GUI_SESSION_CSRF_PATTERN.test(record.csrfToken) + || record.origin !== window.location.origin + || !isValidSessionExpiry(record.expiresAt, now)) return null; + return { + token: record.token, + csrfToken: record.csrfToken, + origin: record.origin, + expiresAt: record.expiresAt, + confirmedLaunch: true, + }; +} + +function parseStoredConfirmedGuiSession(value: unknown, now = Date.now()): ConfirmedGuiSession | null { + if (value === null || typeof value !== "object" || Array.isArray(value)) return null; + const record = value as Record; + const keys = Object.keys(record); + if (record.version !== CONFIRMED_GUI_SESSION_STORAGE_VERSION + || keys.length !== STORED_CONFIRMED_GUI_SESSION_KEYS.size + || keys.some(key => !STORED_CONFIRMED_GUI_SESSION_KEYS.has(key as keyof StoredConfirmedGuiSession))) return null; + return parseConfirmedGuiSession(record, now); +} + +function persistConfirmedGuiSession(session: ConfirmedGuiSession): void { + const storage = pageSessionStorage(); + if (!storage) return; + const record: StoredConfirmedGuiSession = { + version: CONFIRMED_GUI_SESSION_STORAGE_VERSION, + ...session, + }; + try { + storage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, JSON.stringify(record)); + } catch { + // Do not leave an older capability behind when a fresh launch succeeded + // but storage is unavailable. The new session remains usable in memory. + try { storage.removeItem(CONFIRMED_GUI_SESSION_STORAGE_KEY); } catch { /* best effort */ } + } +} + +function activateConfirmedGuiSession(session: ConfirmedGuiSession, persist: boolean): void { + memoryToken = session.token; + memoryCsrfToken = session.csrfToken; + memorySessionOrigin = session.origin; + memorySessionExpiresAt = session.expiresAt; + setAdminCredential(false); + if (persist) persistConfirmedGuiSession(session); + setConfirmedGuiLaunch(true); +} + +function rehydrateConfirmedGuiSession(): boolean { + const storage = pageSessionStorage(); + if (!storage) return false; + let raw: string | null; + try { + raw = storage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY); + } catch { + return false; + } + if (raw === null) return false; + try { + const session = parseStoredConfirmedGuiSession(JSON.parse(raw)); + if (!session) { + clearStoredConfirmedGuiSession(); + return false; + } + activateConfirmedGuiSession(session, false); + return true; + } catch { + clearStoredConfirmedGuiSession(); + return false; + } +} + function readToken(): string | null { + if (memorySessionExpiresAt !== null && memorySessionExpiresAt <= Date.now()) { + clearToken(); + return null; + } return memoryToken; } function storeToken(token: string): void { + clearStoredConfirmedGuiSession(); memoryToken = token; + memoryCsrfToken = null; + memorySessionOrigin = null; + memorySessionExpiresAt = null; setConfirmedGuiLaunch(false); setAdminCredential(true); } @@ -107,13 +249,15 @@ function clearToken(): void { memoryToken = null; memoryCsrfToken = null; memorySessionOrigin = null; + memorySessionExpiresAt = null; + clearStoredConfirmedGuiSession(); setConfirmedGuiLaunch(false); setAdminCredential(false); } /** Clear memory only when it still holds `expected` (avoid wiping a newer concurrent store). */ function clearTokenIfCurrent(expected: string | null): void { - if (expected != null && readToken() === expected) clearToken(); + if (expected != null && memoryToken === expected) clearToken(); } /** Validate and store a server-minted GUI session; rejects anything bound to another origin. */ @@ -121,16 +265,12 @@ function storeSession( token: string | null, csrfToken: string | null, origin: string | null, + expiresAt: number | null, confirmedLaunch = false, ): boolean { - if (!token?.startsWith("ccx_session_") - || !csrfToken - || origin !== window.location.origin) return false; - memoryToken = token; - memoryCsrfToken = csrfToken; - memorySessionOrigin = origin; - setAdminCredential(false); - setConfirmedGuiLaunch(confirmedLaunch); + const session = parseConfirmedGuiSession({ token, csrfToken, origin, expiresAt, confirmedLaunch }); + if (!session) return false; + activateConfirmedGuiSession(session, true); return true; } @@ -183,6 +323,7 @@ async function exchangeGuiLaunchFragment( typeof record.token === "string" ? record.token : null, typeof record.csrfToken === "string" ? record.csrfToken : null, typeof record.origin === "string" ? record.origin : null, + typeof record.expiresAt === "number" ? record.expiresAt : null, true, ); return stored; @@ -205,7 +346,9 @@ async function verifyAdminToken(token: string): ReturnType { function withToken(input: RequestInfo | URL, init: RequestInit | undefined, token: string): [RequestInfo | URL, RequestInit | undefined] { const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); - const isSession = token.startsWith("ccx_session_"); + const isSession = token === memoryToken + && memoryConfirmedGuiLaunch + && GUI_SESSION_TOKEN_PATTERN.test(token); headers.set("X-CodexCommander-API-Key", token); if (memorySessionOrigin && memoryCsrfToken && isSession) { headers.set("X-CodexCommander-GUI-Origin", memorySessionOrigin); @@ -261,7 +404,13 @@ export function installApiAuthFetch(): void { const originalFetch = window.fetch.bind(window); rawFetch = originalFetch; const launch = takeGuiLaunchFragment(); - guiLaunchCapabilityReady = exchangeGuiLaunchFragment(launch); + // Rehydrate first even when a fresh ticket is present. A successful exchange + // atomically replaces the stored session; a transient/consumed ticket leaves + // an already-valid same-origin session usable until its own expiry. + const rehydrated = rehydrateConfirmedGuiSession(); + guiLaunchCapabilityReady = launch + ? exchangeGuiLaunchFragment(launch).then(exchanged => exchanged || rehydrated) + : Promise.resolve(rehydrated); window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { if (!needsApiAuth(input)) return originalFetch(input, init); @@ -325,6 +474,7 @@ export function resetApiAuthFetchForTests(adminTokenPrompt: AdminTokenPrompt = p memoryToken = null; memoryCsrfToken = null; memorySessionOrigin = null; + memorySessionExpiresAt = null; memoryConfirmedGuiLaunch = false; memoryAdminCredential = false; guiLaunchCapabilityReady = Promise.resolve(false); diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 18b2d29284..4ce49f0105 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -725,13 +725,14 @@ export const de: Record = { "logs.col.time": "Zeit", "logs.col.request": "Anfrage", "logs.col.model": "Modell", - "logs.col.effort": "Aufwand", + "logs.col.effort": "Angefragt → Gesendet", "logs.col.provider": "Anbieter", "logs.col.status": "Status", "logs.col.tokens": "Tokens", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "Ausgabe-Tokens pro Sekunde über die gesamte Anfragedauer", + "logs.metric.effortTitle": "Angefragter Aufwand → exakter von CodexCommander gesendeter Wert; der Anbieter kann ihn dennoch ignorieren", "logs.metric.estimatedCostTitle": "API-Listenpreis-Äquivalent, keine tatsächliche Belastung; bei fehlendem Preisabgleich nicht verfügbar", "usage.cost.total": "API-Listenpreis-Äquivalent (dieser Zeitraum)", "usage.cost.disclaimer": "Kein Abrechnungsbeleg. Stattdessen können Abonnementnutzung oder Anbieter-Guthaben gelten.", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 6b09ed57de..ca9b74fc1a 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -750,13 +750,14 @@ export const en = { "logs.col.time": "Time", "logs.col.request": "Request", "logs.col.model": "Model", - "logs.col.effort": "Effort", + "logs.col.effort": "Requested → Sent", "logs.col.provider": "Provider", "logs.col.status": "Status", "logs.col.tokens": "Tokens", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "Output tokens per second over the full request duration", + "logs.metric.effortTitle": "Requested effort → exact value sent by CodexCommander; the provider may still ignore it", "logs.metric.estimatedCostTitle": "API list-price equivalent, not an actual charge; unmatched pricing is unavailable", "usage.cost.total": "API list-price equivalent (this range)", "usage.cost.disclaimer": "Not a billing receipt. Subscription usage or provider credits may apply instead.", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index dfec725987..ee40f200f5 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -712,13 +712,14 @@ export const ja: Record = { "logs.col.time": "時刻", "logs.col.request": "リクエスト", "logs.col.model": "モデル", - "logs.col.effort": "負荷", + "logs.col.effort": "要求 → 送信", "logs.col.provider": "プロバイダー", "logs.col.status": "状態", "logs.col.tokens": "トークン", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "リクエスト全体の所要時間あたりの出力トークン数", + "logs.metric.effortTitle": "要求された負荷 → CodexCommander が送信した正確な値。プロバイダーが適用したことは確認できません", "logs.metric.estimatedCostTitle": "API 定価相当額(実際の請求ではありません); 未対応の価格は利用できません", "usage.cost.total": "API 定価相当額(この期間)", "usage.cost.disclaimer": "請求明細ではありません。サブスクリプション利用量やプロバイダークレジットが代わりに適用される場合があります。", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 15b32e3d75..0c5324c3cc 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -739,13 +739,14 @@ export const ko: Record = { "logs.col.time": "시간", "logs.col.request": "요청", "logs.col.model": "모델", - "logs.col.effort": "추론 강도", + "logs.col.effort": "요청 → 전송", "logs.col.provider": "프로바이더", "logs.col.status": "상태", "logs.col.tokens": "토큰", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "전체 요청 시간 기준 초당 출력 토큰", + "logs.metric.effortTitle": "요청한 추론 강도 → CodexCommander가 전송한 정확한 값이며, 제공자가 적용했는지는 확인되지 않습니다", "logs.metric.estimatedCostTitle": "API 정가 환산치이며 실제 청구액이 아닙니다. 가격 미매칭은 표시하지 않습니다.", "usage.cost.total": "API 정가 환산치 (이 기간)", "usage.cost.disclaimer": "결제 영수증이 아닙니다. 구독 사용량 또는 프로바이더 크레딧이 대신 적용될 수 있습니다.", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index b137cb20b9..8156e47966 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -744,13 +744,14 @@ export const ru: Record = { "logs.col.time": "Время", "logs.col.request": "Запрос", "logs.col.model": "Модель", - "logs.col.effort": "Уровень", + "logs.col.effort": "Запрошено → Отправлено", "logs.col.provider": "Провайдер", "logs.col.status": "Статус", "logs.col.tokens": "Токены", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "Выходные токены в секунду за полную длительность запроса", + "logs.metric.effortTitle": "Запрошенный уровень → точное значение, отправленное CodexCommander; применение провайдером не подтверждено", "logs.metric.estimatedCostTitle": "Эквивалент стоимости по прайс-листу API, а не фактическое списание; если цену не удалось сопоставить, значение недоступно", "usage.cost.total": "Эквивалент стоимости по прайс-листу API (за этот период)", "usage.cost.disclaimer": "Не является счётом. Расходы могут покрываться подпиской или кредитами провайдера.", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index bebc4b658d..11fcdadcfb 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -736,13 +736,14 @@ export const zh: Record = { "logs.col.time": "时间", "logs.col.request": "请求", "logs.col.model": "模型", - "logs.col.effort": "推理强度", + "logs.col.effort": "请求 → 已发送", "logs.col.provider": "提供方", "logs.col.status": "状态", "logs.col.tokens": "Token 数", "logs.col.tokPerSec": "tok/s", "logs.col.estimatedCost": "~$", "logs.metric.tokPerSecTitle": "按完整请求耗时计算的每秒输出 token", + "logs.metric.effortTitle": "请求的推理强度 → CodexCommander 发送的确切值;不代表提供商已应用", "logs.metric.estimatedCostTitle": "按 API 标价估算,并非实际扣费;价格无法匹配时不显示", "usage.cost.total": "API 标价折算(当前范围)", "usage.cost.disclaimer": "这不是账单或扣费凭证。实际可能计入订阅用量或消耗服务商额度。", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index a5279657fb..c3301adebf 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -238,12 +238,14 @@ interface ReasoningLogFields { function effortLabel(log: ReasoningLogFields): string { const requested = log.requestedEffort?.replace(/\s*->\s*/g, " → "); - const effective = log.effectiveEffort; - if (!requested) return effective ?? "-"; + // `effectiveEffort` is the durable API field name. The dashboard calls it "sent" because + // CodexCommander can prove the adapter's final wire value, not whether the upstream applied it. + const sent = log.effectiveEffort; + if (!requested) return sent ?? "-"; // requestedEffort may already contain a cap/clamp chain (for example max->high). // Only append the adapter result when it differs from that chain's terminal value. - if (!effective || requested === effective || requested.split(" → ").at(-1) === effective) return requested; - return `${requested} → ${effective}`; + if (!sent || requested === sent || requested.split(" → ").at(-1) === sent) return requested; + return `${requested} → ${sent}`; } function reasoningWireLabel(log: ReasoningLogFields): string | undefined { @@ -691,7 +693,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { {t("logs.col.tokPerSec")} {t("logs.col.estimatedCost")} {t("logs.col.model")} - {t("logs.col.effort")} + {t("logs.col.effort")} {t("logs.col.provider")} {t("logs.col.status")} {t("logs.col.request")} @@ -910,7 +912,7 @@ function LogDetailDialog({ {t("logs.col.model")}{modelLabel(detail.resolvedModel ?? detail.model)} {t("logs.col.provider")}{formatProviderDisplayName(detail.provider, t)} {(detail.requestedEffort || detail.effectiveEffort) && ( - <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} + <>{t("logs.col.effort")}{effortLabel(detail)}{reasoningWire ? ` (${reasoningWire})` : ""} )} {detail.errorCode && (<>{t("logs.col.error")}{detail.errorCode})} {detail.upstreamError && (<>{t("logs.col.upstreamReason")}{detail.upstreamError})} diff --git a/gui/tests/api-auth-memory.test.ts b/gui/tests/api-auth-memory.test.ts index c4d1dd0276..7f66a367d7 100644 --- a/gui/tests/api-auth-memory.test.ts +++ b/gui/tests/api-auth-memory.test.ts @@ -5,9 +5,13 @@ import { isBrowserLoopbackHostname, isConfirmedGuiLaunch, resetApiAuthFetchForTests, + whenGuiLaunchCapabilitySettles, } from "../src/api"; const globals = ["document", "window", "navigator", "sessionStorage", "fetch"] as const; +const CONFIRMED_GUI_SESSION_STORAGE_KEY = "codexcommander.confirmed-gui-session.v1"; +const CONFIRMED_GUI_SESSION_TOKEN = `ccx_session_${"S".repeat(43)}`; +const CONFIRMED_GUI_SESSION_CSRF = "C".repeat(43); let previousGlobals: Record<(typeof globals)[number], unknown>; let testWindow: Window; let originalPrompt: typeof window.prompt; @@ -131,6 +135,260 @@ test("prompted API tokens stay memory-only and are not written to sessionStorage expect(sessionStorage.length).toBe(0); }); +test("a confirmed launch session survives a same-tab reload and is ready before the first API request", async () => { + const launchTicket = `ccx_launch_${"A".repeat(43)}`; + const expiresAt = Date.now() + 60_000; + window.location.hash = `ccx-launch-ticket=${launchTicket}&ccx-route=dashboard`; + let exchangeCalls = 0; + const seenApiRequests: Array<{ key: string | null; origin: string | null; csrf: string | null }> = []; + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input), window.location.href); + if (url.pathname === "/api/gui-launch-exchange") { + exchangeCalls += 1; + return Response.json({ + route: "dashboard", + session: { + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt, + confirmedLaunch: true, + }, + }); + } + const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)); + seenApiRequests.push({ + key: headers.get("X-CodexCommander-API-Key"), + origin: headers.get("X-CodexCommander-GUI-Origin"), + csrf: headers.get("X-CodexCommander-CSRF-Token"), + }); + return Response.json({ ok: true }); + }) as typeof fetch; + + await installMockAuthFetch(mockFetch); + expect(await whenGuiLaunchCapabilitySettles()).toBe(true); + const stored = JSON.parse(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY) ?? "null"); + expect(stored).toEqual({ + version: 1, + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt, + confirmedLaunch: true, + }); + + // Model a module reload while preserving this tab's sessionStorage. + resetApiAuthFetchForTests(); + await installMockAuthFetch(mockFetch); + expect(isConfirmedGuiLaunch()).toBe(true); + expect(await whenGuiLaunchCapabilitySettles()).toBe(true); + expect(exchangeCalls).toBe(1); + expect((await fetch("/api/config")).status).toBe(200); + expect((await fetch("/api/settings", { method: "PUT", body: "{}" })).status).toBe(200); + expect(seenApiRequests).toEqual([ + { key: CONFIRMED_GUI_SESSION_TOKEN, origin: window.location.origin, csrf: null }, + { key: CONFIRMED_GUI_SESSION_TOKEN, origin: window.location.origin, csrf: CONFIRMED_GUI_SESSION_CSRF }, + ]); +}); + +test("rehydration rejects and removes malformed, foreign, expired, or overlong session records", async () => { + const valid = { + version: 1, + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt: Date.now() + 60_000, + confirmedLaunch: true, + }; + const invalidRecords: string[] = [ + "{not-json", + JSON.stringify({ ...valid, version: 2 }), + JSON.stringify({ ...valid, token: "ccx_session_short" }), + JSON.stringify({ ...valid, csrfToken: "short" }), + JSON.stringify({ ...valid, origin: "http://127.0.0.1" }), + JSON.stringify({ ...valid, expiresAt: Date.now() - 1 }), + JSON.stringify({ ...valid, expiresAt: Date.now() + (9 * 60 * 60_000) }), + JSON.stringify({ ...valid, confirmedLaunch: false }), + JSON.stringify({ ...valid, unexpected: true }), + ]; + const seenApiKeys: Array = []; + const mockFetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + seenApiKeys.push(new Headers(init?.headers).get("X-CodexCommander-API-Key")); + return new Response("unauthorized", { status: 401 }); + }) as typeof fetch; + + for (const raw of invalidRecords) { + resetApiAuthFetchForTests(); + sessionStorage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, raw); + await installMockAuthFetch(mockFetch); + expect(isConfirmedGuiLaunch()).toBe(false); + expect((await fetch("/api/config")).status).toBe(401); + expect(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY)).toBeNull(); + } + expect(seenApiKeys).toEqual(invalidRecords.map(() => null)); +}); + +test("a 401 clears a rehydrated confirmed session and fails closed on loopback", async () => { + sessionStorage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, JSON.stringify({ + version: 1, + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt: Date.now() + 60_000, + confirmedLaunch: true, + })); + const seenApiKeys: Array = []; + const mockFetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + seenApiKeys.push(new Headers(init?.headers).get("X-CodexCommander-API-Key")); + return new Response("unauthorized", { status: 401 }); + }) as typeof fetch; + await installMockAuthFetch(mockFetch); + + expect(isConfirmedGuiLaunch()).toBe(true); + expect((await fetch("/api/config")).status).toBe(401); + expect(seenApiKeys).toEqual([CONFIRMED_GUI_SESSION_TOKEN]); + expect(isConfirmedGuiLaunch()).toBe(false); + expect(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY)).toBeNull(); +}); + +test("an in-memory confirmed session expires before a later request and clears its stored record", async () => { + const expiresAt = Date.now() + 60_000; + sessionStorage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, JSON.stringify({ + version: 1, + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt, + confirmedLaunch: true, + })); + const seenApiKeys: Array = []; + const mockFetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + seenApiKeys.push(new Headers(init?.headers).get("X-CodexCommander-API-Key")); + return new Response("unauthorized", { status: 401 }); + }) as typeof fetch; + await installMockAuthFetch(mockFetch); + expect(isConfirmedGuiLaunch()).toBe(true); + + const originalDateNow = Date.now; + try { + Date.now = () => expiresAt; + expect((await fetch("/api/config")).status).toBe(401); + } finally { + Date.now = originalDateNow; + } + expect(seenApiKeys).toEqual([null]); + expect(isConfirmedGuiLaunch()).toBe(false); + expect(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY)).toBeNull(); +}); + +test("a successful fresh launch replaces an older stored session", async () => { + const oldToken = `ccx_session_${"O".repeat(43)}`; + sessionStorage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, JSON.stringify({ + version: 1, + token: oldToken, + csrfToken: "D".repeat(43), + origin: window.location.origin, + expiresAt: Date.now() + 60_000, + confirmedLaunch: true, + })); + window.location.hash = `ccx-launch-ticket=ccx_launch_${"N".repeat(43)}&ccx-route=logs`; + const seenApiKeys: Array = []; + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input), window.location.href); + if (url.pathname === "/api/gui-launch-exchange") { + return Response.json({ + route: "logs", + session: { + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt: Date.now() + 120_000, + confirmedLaunch: true, + }, + }); + } + seenApiKeys.push(new Headers(init?.headers).get("X-CodexCommander-API-Key")); + return Response.json({ ok: true }); + }) as typeof fetch; + await installMockAuthFetch(mockFetch); + + expect(await whenGuiLaunchCapabilitySettles()).toBe(true); + expect((await fetch("/api/config")).status).toBe(200); + expect(seenApiKeys).toEqual([CONFIRMED_GUI_SESSION_TOKEN]); + expect(JSON.parse(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY) ?? "null").token) + .toBe(CONFIRMED_GUI_SESSION_TOKEN); +}); + +test("a failed fresh launch falls back to an already-valid stored session", async () => { + const storedToken = `ccx_session_${"F".repeat(43)}`; + const storedCsrf = "G".repeat(43); + const storedRecord = { + version: 1, + token: storedToken, + csrfToken: storedCsrf, + origin: window.location.origin, + expiresAt: Date.now() + 60_000, + confirmedLaunch: true, + }; + sessionStorage.setItem(CONFIRMED_GUI_SESSION_STORAGE_KEY, JSON.stringify(storedRecord)); + window.location.hash = `ccx-launch-ticket=ccx_launch_${"X".repeat(43)}&ccx-route=logs`; + let exchangeCalls = 0; + const seenApiKeys: Array = []; + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input), window.location.href); + if (url.pathname === "/api/gui-launch-exchange") { + exchangeCalls += 1; + return new Response("expired", { status: 401 }); + } + seenApiKeys.push(new Headers(init?.headers).get("X-CodexCommander-API-Key")); + return Response.json({ ok: true }); + }) as typeof fetch; + await installMockAuthFetch(mockFetch); + + expect(await whenGuiLaunchCapabilitySettles()).toBe(true); + expect(isConfirmedGuiLaunch()).toBe(true); + expect((await fetch("/api/config")).status).toBe(200); + expect(exchangeCalls).toBe(1); + expect(seenApiKeys).toEqual([storedToken]); + expect(JSON.parse(sessionStorage.getItem(CONFIRMED_GUI_SESSION_STORAGE_KEY) ?? "null")) + .toEqual(storedRecord); +}); + +test("blocked sessionStorage falls back to a memory-only confirmed session", async () => { + const blockedStorage = { + getItem(): string | null { throw new Error("blocked"); }, + setItem(): void { throw new Error("blocked"); }, + removeItem(): void { throw new Error("blocked"); }, + clear(): void { throw new Error("blocked"); }, + key(): string | null { return null; }, + length: 0, + } satisfies Storage; + Object.defineProperty(window, "sessionStorage", { configurable: true, value: blockedStorage }); + window.location.hash = `ccx-launch-ticket=ccx_launch_${"B".repeat(43)}&ccx-route=dashboard`; + const mockFetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const url = new URL(input instanceof Request ? input.url : String(input), window.location.href); + if (url.pathname === "/api/gui-launch-exchange") { + return Response.json({ + route: "dashboard", + session: { + token: CONFIRMED_GUI_SESSION_TOKEN, + csrfToken: CONFIRMED_GUI_SESSION_CSRF, + origin: window.location.origin, + expiresAt: Date.now() + 60_000, + confirmedLaunch: true, + }, + }); + } + const key = new Headers(init?.headers).get("X-CodexCommander-API-Key"); + return new Response("{}", { status: key === CONFIRMED_GUI_SESSION_TOKEN ? 200 : 401 }); + }) as typeof fetch; + await installMockAuthFetch(mockFetch); + + expect(await whenGuiLaunchCapabilitySettles()).toBe(true); + expect(isConfirmedGuiLaunch()).toBe(true); + expect((await fetch("/api/config")).status).toBe(200); +}); + test("validates prompted tokens with a safe read before retrying the failed request", async () => { useRemoteOperatorOrigin(); const validationResults: string[] = []; @@ -411,8 +669,8 @@ test("an expired confirmed loopback session fails closed and requires relaunch", return Response.json({ route: "dashboard", session: { - token: "ccx_session_expired", - csrfToken: "expired-csrf", + token: `ccx_session_${"E".repeat(43)}`, + csrfToken: "E".repeat(43), origin: "http://localhost", expiresAt: Date.now() - 1, confirmedLaunch: true, @@ -432,7 +690,7 @@ test("an expired confirmed loopback session fails closed and requires relaunch", expect(res.status).toBe(401); expect(promptCalls).toBe(0); expect(exchangeCalls).toBe(1); - expect(seenApiKeys).toEqual(["ccx_session_expired"]); + expect(seenApiKeys).toEqual([null]); expect(isConfirmedGuiLaunch()).toBe(false); }); @@ -447,8 +705,8 @@ test("a launch exchange session for another origin is rejected without a loopbac return Response.json({ route: "dashboard", session: { - token: "ccx_session_foreign", - csrfToken: "foreign-csrf", + token: `ccx_session_${"F".repeat(43)}`, + csrfToken: "F".repeat(43), origin: "http://192.0.2.10:10100", expiresAt: Date.now() + 60_000, confirmedLaunch: true, diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 897f0ef678..eeecc2f9c7 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import type { IncomingMeta, ProviderAdapter } from "./base"; +import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base"; import { namespacedToolName, type AdapterEvent, type CodexCommanderParsedRequest, type CodexCommanderProviderConfig, type CodexCommanderUsage } from "../types"; import { catalogModelSupportsReasoningSummaries } from "../codex/catalog"; import { COMPACT_PROMPT, decodeCompactionSummary, SUMMARY_PREFIX } from "../responses/compaction"; @@ -8,7 +8,12 @@ import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy import { decodeServerSentEvents } from "../lib/sse-decoder"; import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; import { CCX_REASONING_PREFIX } from "../responses/reasoning-envelope"; -import { modelRecordValue } from "../reasoning-effort"; +import { + configuredReasoningEfforts, + mapReasoningEffort, + modelRecordValue, + reasoningEffortMapFor, +} from "../reasoning-effort"; import type { TranslatorBudget } from "../lib/translator-budget"; // Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode. @@ -1055,6 +1060,55 @@ function usageFromResponsesPayload(payload: unknown): CodexCommanderUsage | unde }; } +/** + * Apply a provider-declared reasoning ladder/map at the last Responses wire boundary. + * + * The passthrough adapter starts from the caller's raw Responses body, unlike the Chat + * Completions adapter which constructs its outbound body field-by-field. That means an effort + * override can reach this point without ever passing through `mapReasoningEffort`. Only providers + * with an explicit model/provider effort contract are safe to rewrite; custom Responses gateways + * without metadata keep their payload byte-semantics (apart from the adapter's existing + * compatibility sanitizers). + * + * `effectiveEffort` is the durable request-log schema name. Here it means the exact value + * serialized by CodexCommander, not confirmation that the upstream honored the value. + */ +function normalizeResponsesReasoningEffort( + body: unknown, + provider: CodexCommanderProviderConfig, + modelId: string, +): { body: unknown; reasoningLog?: AdapterRequest["reasoningLog"] } { + if (!isPlainObject(body) || !isPlainObject(body.reasoning)) return { body }; + const requested = body.reasoning.effort; + if (typeof requested !== "string" || requested.length === 0) return { body }; + + const hasDeclaredContract = !isCanonicalOpenAiForwardProvider(provider) + && (configuredReasoningEfforts(provider, modelId) !== undefined + || reasoningEffortMapFor(provider, modelId) !== undefined); + const mapped = hasDeclaredContract + ? mapReasoningEffort(provider, modelId, requested) + : undefined; + const sent = mapped ?? requested; + const normalizedBody = mapped !== undefined && mapped !== requested + ? { ...body, reasoning: { ...body.reasoning, effort: mapped } } + : body; + + return { + body: normalizedBody, + // Unknown/custom Responses contracts remain wire-transparent, but their arbitrary + // caller-controlled strings must not cross into durable request diagnostics. + ...(hasDeclaredContract && mapped !== undefined + ? { + reasoningLog: { + effectiveEffort: sent, + wireField: "reasoning.effort" as const, + wireValue: sent, + }, + } + : {}), + }; +} + function responsesPayloadText(response: unknown): string { if (!isPlainObject(response) || !Array.isArray(response.output)) return ""; return response.output @@ -1161,11 +1215,17 @@ export function createResponsesPassthroughAdapter(provider: CodexCommanderProvid outBody = buildRoutedCompactionBody(outBody); } const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubCodexCommanderCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true }))))))); - const body = JSON.stringify(stripDisabledReasoningSummaries( + const finalBody = stripDisabledReasoningSummaries( normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId), provider, parsed.modelId, - )); + ); + const normalizedReasoning = normalizeResponsesReasoningEffort( + finalBody, + provider, + parsed.modelId, + ); + const body = JSON.stringify(normalizedReasoning.body); const releaseBodyObservation = translatorBudget.observeExternallyCapped( "passthrough_serialization", new TextEncoder().encode(body).byteLength, @@ -1176,6 +1236,9 @@ export function createResponsesPassthroughAdapter(provider: CodexCommanderProvid headers, body, releaseBodyObservation, + ...(normalizedReasoning.reasoningLog + ? { reasoningLog: normalizedReasoning.reasoningLog } + : {}), }; }, diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index fee4d52dc1..d39c0f136b 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -115,10 +115,17 @@ A manually opened loopback dashboard receives no API credential. On every loopba address, the browser never prompts for or transmits the raw admin token; it requires a confirmed GUI session instead. `ccx gui` and the macOS companion can use the raw admin credential outside the browser to mint a short-lived, single-use launch ticket carried only in the URL fragment. Its one-time -exact-origin, exact-route exchange creates a process-memory-only confirmed GUI session with an -eight-hour absolute lifetime. It is never renewed: expiry or proxy restart makes the next API request -return `401`, after which the dashboard directs the user back to the launcher. The durable admin token -never enters the URL or web storage. The raw admin principal remains capable of ordinary headless API +exact-origin, exact-route exchange creates a confirmed GUI session with an eight-hour absolute +lifetime. The server keeps the session in process memory; the browser mirrors only its session token, +CSRF token, origin, and absolute expiry in `sessionStorage`, so a reload can rehydrate it while +the server session remains valid. It is never renewed: expiry, proxy restart, or a rejecting `401` +invalidates the client copy, which is then cleared before the dashboard directs the user back to the +launcher. Neither the durable admin token nor the launch ticket enters browser storage, and the +dashboard never uses `localStorage` for authentication. Because same-origin script can read +`sessionStorage`, this reload convenience is not OS-user isolation. Browsers may copy the record into +duplicated or opener-created tabs, or restore it with a restored tab; every copy remains bound to the +exact origin and CSRF token and is usable only until the fixed server expiry, a proxy restart, or a +rejecting `401`. The raw admin principal remains capable of ordinary headless API mutations but is deliberately not accepted by the browser Apply endpoint; the companion and CLI keep their existing narrow non-browser flows. diff --git a/structure/04_transports-and-sidecars.md b/structure/04_transports-and-sidecars.md index e919d0b668..637be9e325 100644 --- a/structure/04_transports-and-sidecars.md +++ b/structure/04_transports-and-sidecars.md @@ -33,6 +33,21 @@ within their route; neither route falls through to the other. See and before the `/v1/*` guard. Unknown `/v1/*` paths return JSON 404 errors instead of falling through to GUI static serving. +### Responses reasoning effort boundary + +`openai-responses` preserves the caller's Responses request shape and the upstream response shape; +it does not translate them through CodexCommander's internal Chat Completions model. Documented +compatibility normalizations are the exception to that passthrough contract. At the final outbound +boundary, a recognized provider/model reasoning contract maps or clamps an already-present +`reasoning.effort` to the value that contract accepts. An unknown or custom contract is left +unchanged rather than guessed. + +When a recognized contract produces a validated mapped value, the adapter records the exact +outbound field and serialized value for request diagnostics. The dashboard presents this as +**Requested → sent**. Unknown/custom values remain wire-transparent but are not copied into durable +diagnostics. “Sent” proves what CodexCommander put on the wire; it is not evidence that the upstream +provider applied or surfaced that effort internally. + ### Passthrough SSE stream shapes (#314) Native passthrough SSE has TWO shapes, selected per request in diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index 4a3bc06728..14954c54d0 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -18,7 +18,7 @@ CodexCommander uses three mutually exclusive admission credential classes: | --- | --- | --- | | Data plane | `CODEXCOMMANDER_API_AUTH_TOKEN`, the `service-api-token` file loaded through `CCX_API_TOKEN_FILE`, and `config.apiKeys` | `/v1/*` HTTP endpoints and new data-plane WebSocket handshakes only | | Management plane | `CODEXCOMMANDER_ADMIN_AUTH_TOKEN` or the independent protected `admin-api-token` file | `/api/*` only | -| GUI session | A confirmed local-app launch, process-memory-only and origin-bound | Full dashboard methods for up to eight hours; catalog Apply remains confirmed-session-only | +| GUI session | A confirmed local-app launch, server-memory-backed and origin-bound; its token, CSRF token, origin, and absolute expiry are mirrored in browser `sessionStorage` | Full dashboard methods for up to eight hours; catalog Apply remains confirmed-session-only | The service token file remains a delivery mechanism for the data-plane environment token; it is not a fourth credential class. A management credential that equals any configured data-plane credential @@ -50,8 +50,8 @@ management token creation, validation, or permission hardening fails, every `/ap must be checked explicitly because an `icacls` timeout is a soft failure in the shared secret helper. Opening a local dashboard page directly does not mint an API credential. The static shell may load, -but every `/api/*` request remains behind management authentication; a fresh `ccx gui`/macOS -companion launch is required. A loopback page never prompts for or transmits the durable admin token, +but every `/api/*` request remains behind management authentication; unless that same tab can +rehydrate a still-valid confirmed session, a fresh `ccx gui`/macOS companion launch is required. A loopback page never prompts for or transmits the durable admin token, because the browser origin does not prove which local OS user owns the listener. There is no lower-privilege loopback session, implicit renewal, or loopback authentication bypass. The dashboard never attaches a management session to `/v1/*`. @@ -64,10 +64,15 @@ headless management API clients using a trusted transport. `ccx gui` or the macOS companion may use the durable admin credential to mint a short-lived, single-use launch ticket bound to the exact route and origin. Only the ticket enters the URL fragment, which the dashboard removes immediately during its one-time exchange. The exchange creates a -confirmed, CSRF-protected GUI session with an eight-hour absolute lifetime. It is process-memory-only -and never renewed. Expiry or proxy restart makes the next API request return `401`; the browser then -directs the user to relaunch. The durable admin token never enters the URL, `localStorage`, or -`sessionStorage`. The +confirmed, CSRF-protected GUI session with an eight-hour absolute lifetime. It remains in server +process memory and is never renewed. The browser mirrors only the confirmed session token, CSRF token, +exact origin, and absolute expiry in `sessionStorage`; this allows reloads while +the server session is still valid. Expiry, proxy restart, or a rejecting `401` invalidates that copy; +the client clears it and directs the user to relaunch. Neither the durable admin token nor the launch +ticket enters browser storage, and authentication never uses `localStorage`. Browsers may copy the +record into duplicated or opener-created tabs, or restore it with a restored tab; every copy remains +bound to the exact origin and CSRF token and is usable only until the fixed server expiry, a proxy +restart, or a rejecting `401`. The ticket is a transient capability, not a fourth durable credential class or a general management bypass. Its exchange endpoint is the narrow pre-authenticated exception to the `/api/*` gate: the single-use ticket itself is the bearer and is bound to the exact origin and route. @@ -76,7 +81,9 @@ Confirmed launch mitigates cross-OS-user loopback listener spoofing, remote driv accidental clients; it is not proof of user presence and is not stronger than raw admin against a malicious process already running as the same trusted OS account described in [`02_config-and-codex-home.md`](02_config-and-codex-home.md). In particular, it must not be described -as blocking a coding agent that already holds the raw admin token. +as blocking a coding agent that already holds the raw admin token. Same-origin script can read the +confirmed session record from `sessionStorage`, so that record must not be described as OS-user +isolation either. The raw admin principal remains capable of ordinary management API mutations. Catalog Apply is the narrow exception: its browser endpoint accepts only a confirmed GUI session, while the native diff --git a/tests/gui-management-session.test.ts b/tests/gui-management-session.test.ts index ad89e65b60..c1754b9394 100644 --- a/tests/gui-management-session.test.ts +++ b/tests/gui-management-session.test.ts @@ -20,8 +20,11 @@ afterEach(() => { }); describe("GUI confirmed launch exchange", () => { - test("scrubs the ticket, exchanges once in memory, and leaves data requests untouched", async () => { + test("scrubs the ticket, stores only the confirmed session per tab, and leaves data requests untouched", async () => { const ticket = `ccx_launch_${"A".repeat(43)}`; + const sessionToken = `ccx_session_${"S".repeat(43)}`; + const csrfToken = "C".repeat(43); + const expiresAt = Date.now() + 60_000; const location = new URL(`http://localhost:10100/#ccx-launch-ticket=${ticket}&ccx-route=subagents`); const seen: Array<{ url: string; method: string; headers: Headers; body?: BodyInit | null; hash: string }> = []; const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit): Promise => { @@ -36,23 +39,34 @@ describe("GUI confirmed launch exchange", () => { return Response.json({ route: "subagents", session: { - token: "ccx_session_browser-secret", - csrfToken: "csrf-browser-secret", + token: sessionToken, + csrfToken, origin: "http://localhost:10100", - expiresAt: Date.now() + 60_000, + expiresAt, confirmedLaunch: true, }, }); } return Response.json({ ok: true }); }; - let durableWrites = 0; - const storage = { getItem: () => null, setItem: () => { durableWrites += 1; }, removeItem: () => { durableWrites += 1; } }; + const sessionValues = new Map(); + let localStorageWrites = 0; + const localStorage = { + getItem: () => null, + setItem: () => { localStorageWrites += 1; }, + removeItem: () => { localStorageWrites += 1; }, + }; + const sessionStorage = { + getItem: (key: string) => sessionValues.get(key) ?? null, + setItem: (key: string, value: string) => { sessionValues.set(key, value); }, + removeItem: (key: string) => { sessionValues.delete(key); }, + }; Object.assign(globalThis, { - localStorage: storage, - sessionStorage: storage, + localStorage, + sessionStorage, window: { location, + sessionStorage, history: { state: null, replaceState(_state: unknown, _title: string, next: string) { @@ -77,13 +91,21 @@ describe("GUI confirmed launch exchange", () => { expect(seen[0]?.hash).toBe("#subagents"); expect(seen[0]?.headers.get("x-codexcommander-api-key")).toBeNull(); expect(JSON.parse(String(seen[0]?.body))).toEqual({ ticket, route: "subagents" }); - expect(seen[1]?.headers.get("x-codexcommander-api-key")).toBe("ccx_session_browser-secret"); + expect(seen[1]?.headers.get("x-codexcommander-api-key")).toBe(sessionToken); expect(seen[1]?.headers.get("x-codexcommander-gui-origin")).toBe("http://localhost:10100"); expect(seen[1]?.headers.get("x-codexcommander-csrf-token")).toBeNull(); - expect(seen[2]?.headers.get("x-codexcommander-api-key")).toBe("ccx_session_browser-secret"); - expect(seen[2]?.headers.get("x-codexcommander-csrf-token")).toBe("csrf-browser-secret"); + expect(seen[2]?.headers.get("x-codexcommander-api-key")).toBe(sessionToken); + expect(seen[2]?.headers.get("x-codexcommander-csrf-token")).toBe(csrfToken); expect(seen[3]?.headers.get("x-codexcommander-api-key")).toBeNull(); expect(seen[3]?.headers.get("x-codexcommander-gui-origin")).toBeNull(); - expect(durableWrites).toBe(0); + expect(localStorageWrites).toBe(0); + expect([...sessionValues.values()].map(value => JSON.parse(value))).toEqual([{ + version: 1, + token: sessionToken, + csrfToken, + origin: "http://localhost:10100", + expiresAt, + confirmedLaunch: true, + }]); }); }); diff --git a/tests/openai-responses-passthrough.test.ts b/tests/openai-responses-passthrough.test.ts index c3da87b25a..fd0a56f784 100644 --- a/tests/openai-responses-passthrough.test.ts +++ b/tests/openai-responses-passthrough.test.ts @@ -104,6 +104,105 @@ describe("DeepSeek Responses endpoint contract", () => { }); }); +describe("OpenAI Responses reasoning effort wire contract", () => { + function buildEffortRequest( + upstream: Parameters[0], + modelId: string, + effort?: string, + ) { + return createResponsesPassthroughAdapter(upstream).buildRequest({ + modelId, + context: { messages: [] }, + stream: true, + options: effort ? { reasoning: effort } : {}, + _rawBody: { + model: modelId, + input: "ping", + ...(effort ? { reasoning: { effort } } : {}), + }, + }, { headers: new Headers({ authorization: "Bearer token" }) }); + } + + test("OpenCode Go clamps a stale Grok max request to its declared high ceiling", () => { + const opencodeGo = { + ...providerConfigSeed(getProviderRegistryEntry("opencode-go")!), + apiKey: "sk-test", + }; + const request = buildEffortRequest(opencodeGo, "grok-4.5", "max"); + + expect(JSON.parse(request.body).reasoning).toEqual({ effort: "high" }); + expect(request.reasoningLog).toEqual({ + effectiveEffort: "high", + wireField: "reasoning.effort", + wireValue: "high", + }); + }); + + test("DeepSeek Responses applies its model aliases and logs the exact sent effort", () => { + const deepseek = { + ...providerConfigSeed(getProviderRegistryEntry("deepseek")!), + apiKey: "sk-test", + }; + + for (const requested of ["medium", "xhigh"] as const) { + const request = buildEffortRequest(deepseek, "deepseek-v4-flash", requested); + expect(JSON.parse(request.body).reasoning).toEqual({ effort: "high" }); + expect(request.reasoningLog).toEqual({ + effectiveEffort: "high", + wireField: "reasoning.effort", + wireValue: "high", + }); + } + + const maxRequest = buildEffortRequest(deepseek, "deepseek-v4-flash", "max"); + expect(JSON.parse(maxRequest.body).reasoning).toEqual({ effort: "max" }); + expect(maxRequest.reasoningLog).toEqual({ + effectiveEffort: "max", + wireField: "reasoning.effort", + wireValue: "max", + }); + }); + + test("canonical OpenAI forwarding preserves the native effort without logging raw passthrough input", () => { + const openai = { + ...providerConfigSeed(getProviderRegistryEntry("openai")!), + // Defense-in-depth control: even if future enrichment grows model metadata, + // ChatGPT-native forwarding stays byte-semantic rather than adapter-mapped. + modelReasoningEfforts: { "gpt-5.6-sol": ["low"] }, + modelReasoningEffortMap: { "gpt-5.6-sol": { max: "low" } }, + }; + const request = buildEffortRequest(openai, "gpt-5.6-sol", "max"); + + expect(JSON.parse(request.body).reasoning).toEqual({ effort: "max" }); + expect(request.reasoningLog).toBeUndefined(); + }); + + test("metadata-free custom gateways preserve effort verbatim without logging caller-controlled text", () => { + const custom = { + adapter: "openai-responses", + baseUrl: "https://responses.example/v1", + authMode: "key" as const, + apiKey: "sk-test", + }; + const request = buildEffortRequest(custom, "custom-reasoner", "vendor-max"); + + expect(JSON.parse(request.body).reasoning).toEqual({ effort: "vendor-max" }); + expect(request.reasoningLog).toBeUndefined(); + }); + + test("omitting reasoning effort emits no reasoning diagnostic", () => { + const request = buildEffortRequest({ + adapter: "openai-responses", + baseUrl: "https://responses.example/v1", + authMode: "key", + apiKey: "sk-test", + }, "custom-reasoner"); + + expect(JSON.parse(request.body)).not.toHaveProperty("reasoning"); + expect(request.reasoningLog).toBeUndefined(); + }); +}); + describe("OpenAI Responses passthrough sanitization", () => { test("normalizes top-level function schemas in the serialized raw body (#745)", () => { const validParameters = {