diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index f3a9d5220fe..de536c78e80 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -161,7 +161,7 @@ modèles n'écrivent jamais un profil passerelle sur une installation first-part ### First-party de la CLI Claude Code -Activez l’interrupteur dans Connexion → Claude ou lancez `ocx claude config set --first-party on` ; utilisez `off` pour désactiver. L’activation est refusée si le proxy local est indisponible, si l’autorité ne peut être préparée, si les réglages sont illisibles ou si des clés appartiennent à un autre programme. La désactivation reste enregistrable. Avec le seul first-party Desktop actif, définissez `NO_PROXY='*'` dans le shell pour un terminal entièrement natif. Le risque pour le compte décrit ci-dessus s’applique aussi à la CLI. +Activez l’interrupteur dans Connexion → Claude ou lancez `ocx claude config set --first-party on` ; utilisez `off` pour désactiver. L’activation est refusée si le proxy local est indisponible, si l’autorité ne peut être préparée, si les réglages sont illisibles ou si des clés appartiennent à un autre programme. La désactivation reste enregistrable. Avec le seul first-party Desktop actif, définissez `NO_PROXY='*'` dans le shell pour un terminal entièrement natif. Le risque pour le compte décrit ci-dessus s’applique aussi à la CLI. Tant que le first-party est actif, OpenCodex ajoute aussi `127.0.0.1`, `localhost`, `::1` et `0.0.0.0` à `NO_PROXY` dans `~/.claude/settings.json` afin que les serveurs MCP locaux (y compris les noms `*.localhost`) contournent le proxy ; ajoutez vous-même toute autre adresse `127.x`. Une valeur `NO_PROXY` existante n’est complétée que des entrées manquantes ; à la désactivation, elle retrouve sa valeur d’origine si OpenCodex a pu l’enregistrer et que vous ne l’avez pas modifiée depuis, sinon la valeur actuelle, entrées ajoutées comprises, est conservée. Désactiver le routage Claude conserve les variables de proxy gérées. Tant que le listener fonctionne, toutes les requêtes Messages sont relayées sans modification ; après son arrêt, `claude` ne peut plus se connecter avant le lancement d’OpenCodex ou la désactivation du first-party Desktop/CLI. Le lancement natif via `ocx claude` ne définit `NO_PROXY=*` que pour un environnement géré sans proxy HTTPS hérité d’un autre programme. Sinon il conserve ce proxy et avertit que l’interception définie dans les réglages reste active ; désactivez le first-party ou retirez ce réglage. L’interface distingue les réglages illisibles (unknown), une URL opencodex avec jeton mais une AC étrangère (foreign : corrigez HTTPS_PROXY / NODE_EXTRA_CA_CERTS à la main) et le routage Claude désactivé avec un listener encore actif qui relaie sans modification (disabled : désactivez first-party avant le redémarrage). Sans listener, l’état est stopped ; avec une AC gérée mais un port ou jeton incorrect, il est broken. Avec le first-party actif et une interception indisponible, stopped et broken affichent routingOff : le routage Claude ou l’interception est désactivé, ou cette machine est cliente d’un autre hub opencodex ; réactivez l’interception ici ou désactivez first-party pour supprimer les réglages. Si l’interception est disponible, stopped demande de lancer opencodex et broken conseille `ocx ensure` ou un redémarrage. La CLI activée sans proxy est non appliquée ; un seul client activé avec un proxy opérationnel partage le relais ; aucun client activé avec un proxy restant produit un avertissement de réglage résiduel. unknown signifie qu’opencodex ne peut pas déterminer si les réglages pointent encore vers son proxy. Un proxy sans jeton sur 127.0.0.1 avec une AC étrangère est local : sa propriété est incertaine ; supprimez HTTPS_PROXY de ~/.claude/settings.json si vous ne l’utilisez plus. disabled exige des réglages appliqués correspondant au listener ; un port ou jeton différent donne broken même si le routage est désactivé. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 1043ba864b6..b65634cc2c3 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -252,8 +252,9 @@ a `*.localhost` name, `::1` or `0.0.0.0`, off the proxy, which only tunnels HTTP hosts. A server on another `127.x` address needs that address added to `NO_PROXY` yourself. If you already set `NO_PROXY`, OpenCodex appends only the missing loopback entries and records its change in `opencodex-no-proxy.json` next to -`settings.json`; turning first-party off puts your original value back, and a value you edited -in the meantime is left as it is. +`settings.json`. Turning first-party off puts your original value back when OpenCodex could +record it and you have not changed it since; otherwise the current value, including the added +entries, is kept. Claude Desktop first-party routes its Code tab and subagents through OpenCodex. The standalone Claude Code CLI has a separate first-party switch. Both clients read the same `~/.claude/settings.json` proxy and CA settings: if only one switch is on, the other client still transits the local proxy, where TLS terminates, but its Messages requests relay to Anthropic unchanged. Other Anthropic paths relay unchanged and unrelated hosts remain blind tunnels. With the CLI switch on, the standalone `claude` also lists routed models in `/model`; see [Model picker in the CLI](#model-picker-in-the-cli). 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 e755758e452..4784ea11acb 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -138,7 +138,7 @@ OpenCodex 所有の選択済みゲートウェイ行、保存済みのゲート ### Claude Code CLI の 1P -接続 → Claude で CLI の 1P をオンにするか、`ocx claude config set --first-party on` を実行します。オフには `off` を使います。プロキシの停止、CA の準備失敗、設定の読み取り失敗、他プログラムが所有するキーがある場合、オンへの切り替えは拒否されます。オフはプロキシが使えなくても保存できます。Desktop の 1P だけがオンのとき、ターミナルから完全に直接接続するにはシェルで `NO_PROXY='*'` を設定してください。上記のアカウントリスクは CLI にも適用されます。 +接続 → Claude で CLI の 1P をオンにするか、`ocx claude config set --first-party on` を実行します。オフには `off` を使います。プロキシの停止、CA の準備失敗、設定の読み取り失敗、他プログラムが所有するキーがある場合、オンへの切り替えは拒否されます。オフはプロキシが使えなくても保存できます。Desktop の 1P だけがオンのとき、ターミナルから完全に直接接続するにはシェルで `NO_PROXY='*'` を設定してください。上記のアカウントリスクは CLI にも適用されます。 1P がオンの間、OpenCodex は `~/.claude/settings.json` の `NO_PROXY` に `127.0.0.1`、`localhost`、`::1`、`0.0.0.0` も追加し、ローカルの MCP サーバー(`*.localhost` 名を含む)がプロキシを経由しないようにします。その他の `127.x` アドレスは自分で追加してください。既存の `NO_PROXY` には不足分だけを追記し、オフにすると、OpenCodex が元の値を記録でき、その後変更していなければ元の値に戻します。それ以外の場合は、追加した項目を含む現在の値をそのまま残します。 Claude のルーティングを無効にしても管理対象の設定環境変数は残ります。リスナーが動いている間はすべての Messages 要求を変更せず中継しますが、停止後は OpenCodex を起動するか Desktop/CLI の 1P をオフにするまで通常の `claude` は接続できません。`ocx claude` は管理対象の設定があり、外部の HTTPS プロキシを継承していない場合だけ `NO_PROXY=*` を設定します。外部プロキシは保持し、設定側の傍受が続くため 1P をオフにするか設定を解除するよう警告します。 画面では設定を読めない unknown、opencodex のトークン付き URL と管理外 CA が組み合わさった foreign、Claude ルーティングがオフでも動作中のリスナーが要求をそのまま中継する disabled を区別します。foreign は HTTPS_PROXY / NODE_EXTRA_CA_CERTS を手動で直し、disabled は再起動前に 1P をオフにして設定を消してください。リスナーがなければ stopped、管理対象 CA でもポートやトークンが違えば broken です。1P がオンでもインターセプトを提供できなければ、stopped と broken は routingOff を表示します。Claude ルーティングまたはインターセプトがオフか、この端末が別の opencodex ハブのクライアントであるため、この端末で再度有効にするか 1P をオフにして設定を削除するよう案内します。インターセプトを提供できる設定の場合だけ、stopped は opencodex の起動、broken は `ocx ensure` または再起動を案内します。CLI だけオンでプロキシ設定がなければ未適用、片方だけオンで正常なら共有中継、両方オフでも設定が残れば残留設定を表示します。 unknown は設定がまだ opencodex のプロキシを指すか判断できない状態です。外部 CA とトークンなしの 127.0.0.1 プロキシがある場合は local と表示します。所有者を確認できないため、使っていなければ ~/.claude/settings.json から HTTPS_PROXY を削除してください。disabled は設定と動作中のリスナーが一致する場合だけで、ポートやトークンが違えばルーティングがオフでも broken です。 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 0bde291bc38..3ee9bf2ca3b 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -161,7 +161,7 @@ OpenCodex 소유의 선택된 게이트웨이 항목, 저장된 게이트웨이 ### Claude Code CLI 1P -연결 → Claude에서 CLI 1P를 켜거나 `ocx claude config set --first-party on`을 실행하세요. 끌 때는 `off`를 사용해요. 프록시가 꺼져 있거나 실행 중이지 않거나, CA·설정 파일을 준비할 수 없거나, 다른 프로그램이 프록시 설정을 소유하면 켜기 요청은 거절돼요. 끄기는 프록시 상태와 관계없이 저장돼요. Desktop 1P만 켜진 상태에서 터미널을 완전히 직접 연결하려면 셸에 `NO_PROXY='*'`를 설정하세요. 위의 계정 위험은 CLI 1P에도 적용돼요. +연결 → Claude에서 CLI 1P를 켜거나 `ocx claude config set --first-party on`을 실행하세요. 끌 때는 `off`를 사용해요. 프록시가 꺼져 있거나 실행 중이지 않거나, CA·설정 파일을 준비할 수 없거나, 다른 프로그램이 프록시 설정을 소유하면 켜기 요청은 거절돼요. 끄기는 프록시 상태와 관계없이 저장돼요. Desktop 1P만 켜진 상태에서 터미널을 완전히 직접 연결하려면 셸에 `NO_PROXY='*'`를 설정하세요. 위의 계정 위험은 CLI 1P에도 적용돼요. 1P가 켜져 있는 동안 OpenCodex는 `~/.claude/settings.json`의 `NO_PROXY`에 `127.0.0.1`, `localhost`, `::1`, `0.0.0.0`도 추가해서 로컬 MCP 서버(`*.localhost` 이름 포함)가 프록시를 거치지 않게 해요. 그 밖의 `127.x` 주소는 직접 추가하세요. 기존 `NO_PROXY` 값에는 빠진 항목만 덧붙이고, 끄면 OpenCodex가 원래 값을 기록할 수 있었고 그 뒤로 값을 바꾸지 않았을 때 원래 값으로 되돌려요. 그렇지 않으면 추가된 항목을 포함한 현재 값을 그대로 둬요. Claude 라우팅을 꺼도 소유한 설정 환경 변수는 남아요. 리스너가 실행 중이면 모든 Messages 요청을 그대로 전달하지만, 프록시가 멈추면 OpenCodex를 실행하거나 Desktop/CLI 1P를 끄기 전까지 일반 `claude`는 연결할 수 없어요. `ocx claude`는 소유한 설정 환경 변수가 있고 상속된 외부 HTTPS 프록시가 없을 때만 `NO_PROXY=*`를 설정해요. 외부 프록시가 있으면 보존하고, 설정의 인터셉트가 계속 적용되므로 1P를 끄거나 설정을 해제하라는 경고를 표시해요. 화면은 설정을 읽지 못한 상태(unknown), opencodex 토큰이 있는 프록시에 관리 대상이 아닌 CA가 붙은 상태(foreign), Claude 라우팅이 꺼졌지만 리스너는 살아 있어 요청을 그대로 전달하는 상태(disabled)를 구분해요. foreign이면 HTTPS_PROXY / NODE_EXTRA_CA_CERTS를 직접 고치고, disabled이면 재시작 전에 1P를 꺼서 설정을 지우세요. 리스너가 없으면 stopped, 관리 대상 CA를 쓰지만 포트·토큰이 다르면 broken이에요. 1P가 켜진 상태에서 인터셉트를 제공할 수 없으면 stopped와 broken 모두 routingOff 경고를 보여 줘요. Claude 라우팅이나 인터셉트가 꺼졌거나 이 기기가 다른 opencodex 허브의 클라이언트일 수 있으므로, 이 기기에서 다시 켜거나 1P를 꺼서 설정을 지우라고 안내해요. 인터셉트가 가능한 설정일 때만 stopped는 opencodex 실행, broken은 `ocx ensure` 또는 재시작을 안내해요. CLI 1P만 켰는데 프록시 설정이 없으면 미적용, 한쪽만 켜고 프록시가 정상이면 공유 전달, 둘 다 껐는데 설정이 남으면 잔여 설정으로 표시해요. unknown은 설정이 아직 opencodex 프록시를 가리키는지 판단할 수 없다는 뜻이에요. 외부 CA와 토큰 없는 127.0.0.1 프록시가 함께 있으면 local로 표시해요. opencodex 소유인지 확인할 수 없으므로 더 이상 사용하지 않는다면 ~/.claude/settings.json에서 HTTPS_PROXY를 지우세요. disabled는 현재 리스너와 설정이 정확히 맞을 때만 나타나고, 포트나 토큰이 어긋나면 라우팅이 꺼져 있어도 broken이에요. 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 fc663709e87..589ed001a9b 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -146,7 +146,7 @@ OpenCodex настройки first-party в `settings.json`; при отсутс ### First-party для Claude Code CLI -Включите переключатель в Подключение → Claude или выполните `ocx claude config set --first-party on`; для выключения используйте `off`. Включение отклоняется при недоступном прокси, ошибке подготовки CA, нечитаемых настройках или чужих ключах прокси. Выключение сохраняется и при недоступном прокси. Если активен только first-party Desktop, задайте `NO_PROXY='*'` в оболочке для полностью нативного терминала. Описанный выше риск для аккаунта относится и к CLI. +Включите переключатель в Подключение → Claude или выполните `ocx claude config set --first-party on`; для выключения используйте `off`. Включение отклоняется при недоступном прокси, ошибке подготовки CA, нечитаемых настройках или чужих ключах прокси. Выключение сохраняется и при недоступном прокси. Если активен только first-party Desktop, задайте `NO_PROXY='*'` в оболочке для полностью нативного терминала. Описанный выше риск для аккаунта относится и к CLI. Пока first-party включён, OpenCodex также добавляет `127.0.0.1`, `localhost`, `::1` и `0.0.0.0` в `NO_PROXY` в `~/.claude/settings.json`, чтобы локальные MCP-серверы (включая имена `*.localhost`) обходили прокси; другие адреса `127.x` добавьте сами. К существующему значению `NO_PROXY` дописываются только недостающие записи, а при выключении восстанавливается прежнее значение, если OpenCodex смог его сохранить и вы его с тех пор не меняли; иначе текущее значение, включая добавленные записи, остаётся без изменений. Отключение маршрутизации Claude сохраняет управляемые настройки прокси. Пока listener работает, все запросы Messages пересылаются без изменений; после его остановки обычный `claude` не подключится до запуска OpenCodex или выключения first-party Desktop/CLI. Нативный запуск через `ocx claude` задаёт `NO_PROXY=*` только при управляемых настройках без унаследованного чужого HTTPS-прокси. Иначе он сохраняет чужой прокси и предупреждает, что перехват в настройках остаётся активным: выключите first-party или удалите настройку. Интерфейс различает нечитаемые настройки (unknown), URL opencodex с токеном и чужим CA (foreign: исправьте HTTPS_PROXY / NODE_EXTRA_CA_CERTS вручную) и выключенную маршрутизацию Claude при ещё работающем listener, который передаёт запросы без изменений (disabled: выключите first-party до перезапуска). Без listener состояние stopped; с управляемым CA, но неверным портом или токеном — broken. При включённом first-party и недоступном перехвате stopped и broken показывают routingOff: маршрутизация Claude или перехват отключены либо этот компьютер является клиентом другого хаба opencodex; включите перехват здесь или выключите first-party, чтобы удалить настройки. Только если перехват доступен, stopped предлагает запустить opencodex, а broken — выполнить `ocx ensure` или перезапустить его. Включённая CLI без прокси показывает отсутствие применения, один включённый клиент с работающим прокси — общий ретранслятор, оба выключенных при оставшемся прокси — остаточную настройку. unknown означает, что opencodex не может определить, указывают ли настройки на его прокси. Прокси без токена на 127.0.0.1 вместе с чужим CA получает состояние local: принадлежность не подтверждена; если он больше не нужен, удалите HTTPS_PROXY из ~/.claude/settings.json. disabled возможно только при совпадении настроек с работающим listener; другой порт или токен даёт broken даже при выключенной маршрутизации. diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 8f1942959eb..2cf7faf839b 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -183,7 +183,7 @@ vekilin ayarları üzerine yazılmaz. Mod değiştirince Desktop'ı tamamen kapa ### Claude Code CLI first-party -Bağlantı → Claude bölümünde CLI anahtarını açın veya `ocx claude config set --first-party on` çalıştırın; kapatmak için `off` kullanın. Yerel vekil kullanılamıyorsa, CA hazırlanamazsa, ayarlar okunamazsa veya anahtarlar başka bir programa aitse açma isteği reddedilir. Kapatma yine de kaydedilir. Yalnız Desktop first-party açıkken terminalde tamamen yerel bağlantı için kabukta `NO_PROXY='*'` ayarlayın. Yukarıdaki hesap riski CLI için de geçerlidir. +Bağlantı → Claude bölümünde CLI anahtarını açın veya `ocx claude config set --first-party on` çalıştırın; kapatmak için `off` kullanın. Yerel vekil kullanılamıyorsa, CA hazırlanamazsa, ayarlar okunamazsa veya anahtarlar başka bir programa aitse açma isteği reddedilir. Kapatma yine de kaydedilir. Yalnız Desktop first-party açıkken terminalde tamamen yerel bağlantı için kabukta `NO_PROXY='*'` ayarlayın. Yukarıdaki hesap riski CLI için de geçerlidir. First-party açıkken OpenCodex, yerel MCP sunucularının (`*.localhost` adları dahil) vekili atlaması için `~/.claude/settings.json` içindeki `NO_PROXY` değerine `127.0.0.1`, `localhost`, `::1` ve `0.0.0.0` girdilerini de ekler; diğer `127.x` adreslerini kendiniz ekleyin. Mevcut bir `NO_PROXY` değerine yalnızca eksik girdiler eklenir; kapatıldığında önceki değer, OpenCodex onu kaydedebildiyse ve siz o zamandan beri değiştirmediyseniz geri yüklenir, aksi halde eklenen girdiler dahil mevcut değer korunur. Claude yönlendirmesini kapatmak yönetilen vekil ayarlarını korur. Dinleyici çalışırken tüm Messages istekleri değiştirilmeden iletilir; durduğunda OpenCodex çalışana veya Desktop/CLI first-party kapatılana kadar doğrudan `claude` bağlanamaz. `ocx claude` yerel başlatması `NO_PROXY=*` değerini yalnızca yönetilen ayarlar varken ve yabancı bir HTTPS vekili miras alınmamışken ayarlar. Aksi halde yabancı vekili korur ve ayarlardaki kesmenin sürdüğünü bildirir: first-party özelliğini kapatın veya ayarı kaldırın. Arayüz okunamayan ayarları (unknown), opencodex belirteçli URL ile yabancı CA birleşimini (foreign: HTTPS_PROXY / NODE_EXTRA_CA_CERTS değerlerini elle düzeltin) ve Claude yönlendirmesi kapalıyken dinleyicinin istekleri değiştirmeden iletmeye devam etmesini (disabled: yeniden başlatmadan önce first-party modunu kapatın) ayırt eder. Dinleyici yoksa stopped; yönetilen CA ile port veya belirteç uyuşmuyorsa broken durumudur. First-party açıkken kesme kullanılamıyorsa stopped ve broken, routingOff uyarısını gösterir: Claude yönlendirmesi veya kesme kapalı ya da bu makine başka bir opencodex merkezinin istemcisidir; bu makinede yeniden etkinleştirin veya ayarları kaldırmak için first-party modunu kapatın. Kesme kullanılabilirken stopped opencodex uygulamasını başlatmayı, broken ise `ocx ensure` ya da yeniden başlatmayı önerir. Yalnız CLI açık ama vekil yoksa uygulanmadı, tek istemci açık ve vekil çalışıyorsa paylaşılan iletim, iki istemci kapalıyken vekil kalmışsa artık ayar uyarısı görünür. unknown, ayarların hâlâ opencodex vekiline işaret edip etmediğinin belirlenemediği anlamına gelir. Yabancı CA ile birlikte 127.0.0.1 üzerindeki belirteçsiz vekil local durumudur: sahipliği doğrulanamaz; artık kullanmıyorsanız ~/.claude/settings.json içindeki HTTPS_PROXY değerini kaldırın. disabled yalnızca ayarlar çalışan dinleyiciyle eşleşiyorsa geçerlidir; port veya belirteç farklıysa yönlendirme kapalı olsa bile broken görünür. 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 79a3af88efd..bc45c7bd082 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 @@ -131,7 +131,7 @@ OpenCodex 拥有的已选网关条目、保存的网关指纹、`settings.json` ### Claude Code CLI 第一方模式 -在 连接 → Claude 中开启 CLI 开关,或运行 `ocx claude config set --first-party on`;关闭时用 `off`。若本地代理不可用、CA 无法准备、设置无法读取,或代理键由其他程序所有,开启请求会被拒绝。关闭操作始终可以保存。仅开启 Desktop 第一方模式时,要让终端完全原生直连,请在 shell 中设置 `NO_PROXY='*'`。上述账户风险也适用于 CLI。 +在 连接 → Claude 中开启 CLI 开关,或运行 `ocx claude config set --first-party on`;关闭时用 `off`。若本地代理不可用、CA 无法准备、设置无法读取,或代理键由其他程序所有,开启请求会被拒绝。关闭操作始终可以保存。仅开启 Desktop 第一方模式时,要让终端完全原生直连,请在 shell 中设置 `NO_PROXY='*'`。上述账户风险也适用于 CLI。 第一方模式开启期间,OpenCodex 还会在 `~/.claude/settings.json` 的 `NO_PROXY` 中加入 `127.0.0.1`、`localhost`、`::1` 和 `0.0.0.0`,让本地 MCP 服务器(包括 `*.localhost` 名称)绕过代理;其他 `127.x` 地址需自行添加。已有的 `NO_PROXY` 只会补上缺少的条目,关闭后,若 OpenCodex 已成功记录原值且你之后未修改该值,则恢复原值;否则保留当前值(包括新增的条目)。 关闭 Claude 路由会保留由 OpenCodex 管理的代理设置。监听器仍运行时,所有 Messages 请求原样转发;停止后,运行 OpenCodex 或关闭 Desktop/CLI 第一方模式之前,直接运行 `claude` 无法连接。`ocx claude` 原生启动仅在有自有设置且未继承外部 HTTPS 代理时设置 `NO_PROXY=*`。否则保留外部代理,并警告设置中的拦截仍生效;请关闭第一方模式或取消该设置。 界面区分设置不可读(unknown)、带 opencodex 令牌的代理 URL 却搭配外部 CA(foreign:手动修正 HTTPS_PROXY / NODE_EXTRA_CA_CERTS),以及 Claude 路由已关闭但仍有监听器原样转发请求(disabled:重启前关闭第一方模式以清除设置)。没有监听器时为 stopped;使用受管理的 CA 但端口或令牌不匹配时为 broken。第一方模式开启但无法提供拦截服务时,stopped 和 broken 都显示 routingOff:Claude 路由或拦截功能已关闭,或此设备是另一台 opencodex 中枢的客户端;请在此设备上重新启用拦截服务,或关闭第一方模式以移除设置。仅在拦截服务可用时,stopped 才提示启动 opencodex,broken 才提示运行 `ocx ensure` 或重启。CLI 已开启但没有代理设置为未应用;仅开启一个客户端且代理正常时提示共享转发;两个客户端都关闭但代理设置仍在时提示残留。 unknown 表示 opencodex 无法确定设置是否仍指向自己的代理。外部 CA 搭配 127.0.0.1 上无令牌的代理时显示 local:归属无法确认;如果不再使用,请从 ~/.claude/settings.json 中删除 HTTPS_PROXY。disabled 仅在设置与运行中的监听器匹配时出现;端口或令牌不匹配时,即使路由关闭也显示 broken。 diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index f01884006f4..7aedc87dc6d 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -137,7 +137,7 @@ Desktop 第一方模式透過 OpenCodex 處理 Code 分頁及其子代理。獨 ### Claude Code CLI 第一方模式 -在 連線 → Claude 開啟 CLI 開關,或執行 `ocx claude config set --first-party on`;關閉時使用 `off`。若本機代理無法使用、CA 無法準備、設定無法讀取,或代理鍵由其他程式擁有,開啟要求會被拒絕。關閉仍可儲存。只有 Desktop 第一方模式開啟時,若要讓終端機完全原生直連,請在 shell 設定 `NO_PROXY='*'`。上述帳號風險也適用於 CLI。 +在 連線 → Claude 開啟 CLI 開關,或執行 `ocx claude config set --first-party on`;關閉時使用 `off`。若本機代理無法使用、CA 無法準備、設定無法讀取,或代理鍵由其他程式擁有,開啟要求會被拒絕。關閉仍可儲存。只有 Desktop 第一方模式開啟時,若要讓終端機完全原生直連,請在 shell 設定 `NO_PROXY='*'`。上述帳號風險也適用於 CLI。 第一方模式開啟期間,OpenCodex 也會在 `~/.claude/settings.json` 的 `NO_PROXY` 加入 `127.0.0.1`、`localhost`、`::1` 和 `0.0.0.0`,讓本機 MCP 伺服器(包括 `*.localhost` 名稱)略過代理;其他 `127.x` 位址請自行加入。既有的 `NO_PROXY` 只會補上缺少的項目,關閉後,若 OpenCodex 已成功記錄原本的值且你之後未修改,就會還原原本的值;否則保留目前的值(包括新增的項目)。 關閉 Claude 路由會保留由 OpenCodex 管理的代理設定。監聽器仍執行時,所有 Messages 請求原樣轉送;停止後,執行 OpenCodex 或關閉 Desktop/CLI 第一方模式前,直接執行 `claude` 無法連線。`ocx claude` 原生啟動只在有自有設定且未繼承外部 HTTPS 代理時設定 `NO_PROXY=*`。否則保留外部代理,並警告設定中的攔截仍生效;請關閉第一方模式或取消該設定。 介面會區分設定無法讀取(unknown)、帶有 opencodex 權杖的代理 URL 卻搭配外部 CA(foreign:手動修正 HTTPS_PROXY / NODE_EXTRA_CA_CERTS),以及 Claude 路由已關閉但監聽器仍原樣轉送要求(disabled:重新啟動前關閉第一方模式以移除設定)。沒有監聽器時為 stopped;使用受管理的 CA 但連接埠或權杖不符時為 broken。第一方模式開啟但無法提供攔截服務時,stopped 和 broken 都顯示 routingOff:Claude 路由或攔截功能已關閉,或這台裝置是另一個 opencodex 中樞的用戶端;請在這台裝置上重新啟用攔截服務,或關閉第一方模式以移除設定。只有攔截服務可用時,stopped 才提示啟動 opencodex,broken 才提示執行 `ocx ensure` 或重新啟動。CLI 已開啟但沒有代理設定時為未套用;只開啟一個用戶端且代理正常時提示共享轉送;兩者皆關閉但代理設定仍在時提示殘留。 unknown 表示 opencodex 無法確定設定是否仍指向自己的代理。外部 CA 搭配 127.0.0.1 上沒有權杖的代理時顯示 local:無法確認歸屬;若不再使用,請從 ~/.claude/settings.json 移除 HTTPS_PROXY。disabled 僅在設定與執行中的監聽器相符時出現;連接埠或權杖不相符時,即使路由關閉也顯示 broken。 diff --git a/src/claude/intercept/connect-proxy.ts b/src/claude/intercept/connect-proxy.ts index 81c8575bdcd..a3845d57d59 100644 --- a/src/claude/intercept/connect-proxy.ts +++ b/src/claude/intercept/connect-proxy.ts @@ -124,7 +124,7 @@ function respond( // Plain proxied HTTP (often loopback MCP traffic missing a NO_PROXY bypass) lands here; say why. const CONNECT_ONLY_BODY = "OpenCodex Claude intercept proxy accepts only CONNECT tunnels. " - + "Exclude this host via NO_PROXY (loopback: 127.0.0.1,localhost,::1,0.0.0.0).\n"; + + "Exclude this host via NO_PROXY (loopback and unspecified hosts: 127.0.0.1,localhost,::1,0.0.0.0).\n"; function splice(client: Socket, upstream: Socket, pending: Uint8Array): void { const teardown = () => { diff --git a/src/claude/intercept/settings.ts b/src/claude/intercept/settings.ts index e0e087fe7b9..bb4371bcf3c 100644 --- a/src/claude/intercept/settings.ts +++ b/src/claude/intercept/settings.ts @@ -146,13 +146,28 @@ export function inspectClaudeInterceptSettings( }; } -function writeSettings(path: string, doc: SettingsDoc): void { +const defaultPersist = (path: string, content: string): void => atomicWriteFile(path, content); +let persistFile = defaultPersist; + +/** Replace the settings.json write in tests; the real-home guard still runs first. Returns a restore. */ +export function setClaudeSettingsPersistForTests(persist: (path: string, content: string) => void): () => void { + const before = persistFile; + persistFile = persist; + return () => { persistFile = before; }; +} + +/** Commit settings.json. Once this returns, the file on disk holds the new document. */ +function persistSettings(path: string, doc: SettingsDoc): void { // An armed test process must not rewrite the real Claude settings.json (#6775). assertNotRealClaudeConfigUnderTest(dirname(path), path); mkdirSync(dirname(path), { recursive: true }); // The managed env embeds the proxy token, so the file must stay owner-only: // atomicWriteFile applies the real NTFS ACL on Windows where chmod is a no-op. - atomicWriteFile(path, `${JSON.stringify(doc, null, 2)}\n`); + persistFile(path, `${JSON.stringify(doc, null, 2)}\n`); +} + +function writeSettings(path: string, doc: SettingsDoc): void { + persistSettings(path, doc); // Whether Claude Code reaches the proxy just changed, so its cached /model list is stale. invalidateClaudeCodeServedCatalog(dirname(path)); } @@ -271,12 +286,23 @@ export function removeClaudeInterceptSettings( const env = envRecord(read.doc); if (!isOwnedCaPath(env.NODE_EXTRA_CA_CERTS, ownedCaPath)) return { ok: true, changed: false, path }; const receipt = readNoProxyReceipt(configDir); + const receiptBytes = readNoProxyReceiptBytes(configDir); delete env.NODE_EXTRA_CA_CERTS; if (isClaudeInterceptProxyUrl(env.HTTPS_PROXY)) delete env.HTTPS_PROXY; if (receipt && env.NO_PROXY === receipt.written) setOrDelete(env, "NO_PROXY", receipt.hadKey ? receipt.previous : undefined); if (!invalidateNoProxyReceipt(configDir)) return { ok: false, reason: "unreadable", path: noProxyReceiptPath(configDir) }; if (Object.keys(env).length === 0) delete read.doc.env; else read.doc.env = env; - writeSettings(path, read.doc); + try { + persistSettings(path, read.doc); + } catch (error) { + // A removal that was not committed has not ended the episode, so its receipt stays valid. Only the + // persist step is covered: once settings.json changed, the receipt must not come back. + if (receiptBytes !== null) { + try { writeNoProxyReceiptBytes(configDir, receiptBytes); } catch { /* keep the original error */ } + } + throw error; + } + invalidateClaudeCodeServedCatalog(dirname(path)); return { ok: true, changed: true, path }; } diff --git a/structure/runtime.md b/structure/runtime.md index 8eedcc9c94c..5ffc96aa130 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -249,7 +249,7 @@ under `/claude-intercept/` with a 0600 key; never installed into Claude Code reaches the pair through an authenticated `HTTPS_PROXY` URL plus `NODE_EXTRA_CA_CERTS` in its settings env (`src/claude/intercept/settings.ts`), so no `ANTHROPIC_BASE_URL` rewrite is involved and the client still believes it talks to Anthropic. The proxy splices `CONNECT api.anthropic.com:443` onto the TLS -listener, relays every other CONNECT target blind, and refuses unauthenticated clients, plain proxied HTTP (a 405 whose body names the NO_PROXY fix), and loopback targets. Because it can never serve loopback, the same env carries a `NO_PROXY` loopback bypass (`127.0.0.1,localhost,::1,0.0.0.0`); without it Claude Code sent local HTTP MCP traffic to the proxy and every such server failed (#6936). Apply appends only the missing entries to a user value and leaves a covering value (including `*`) alone; an owned block without the bypass classifies stale so the startup migrate repairs it, and a non-string value other than null is refused as `foreign_env` rather than rewritten. Because the key is shared, apply records `{ hadKey, previous, written }` in `/opencodex-no-proxy.json` (`src/claude/intercept/settings-no-proxy.ts`, no token), and removal restores `previous` only while NO_PROXY still equals `written`, preserving anything else. The receipt lives exactly as long as its ownership episode: an apply from an absent block and every removal invalidate it first and fail closed (`unreadable`) when they cannot, and rollback restores it with the captured value. The per-install proxy token is stored owner-only under `/claude-intercept/` (0600 plus a real per-user NTFS ACL on Windows, via `src/lib/windows-secret-acl.ts`, and re-pinned on every read-through `ensure`), and the settings file carrying it is written through the same hardened atomic writer. Every start runs `migrateClaudeInterceptSettings` (`src/claude/intercept/settings.ts`), which rewrites an owned env that no longer matches — e.g. a pre-auth URL left by an upgrade — while never creating an absent env or touching a foreign one, so a service restart cannot strand clients on 407s. Status/inspection reads the token without minting it; only apply and intercept startup create it. +listener, relays every other CONNECT target blind, and refuses unauthenticated clients, plain proxied HTTP (a 405 whose body names the NO_PROXY fix), and loopback targets. Because it can never serve loopback, the same env carries a `NO_PROXY` loopback bypass (`127.0.0.1,localhost,::1,0.0.0.0`); without it Claude Code sent local HTTP MCP traffic to the proxy and every such server failed (#6936). Apply appends only the missing entries to a user value and leaves a covering value (including `*`) alone; an owned block without the bypass classifies stale so the startup migrate repairs it, and a non-string value other than null is refused as `foreign_env` rather than rewritten. Because the key is shared, apply records `{ hadKey, previous, written }` in `/opencodex-no-proxy.json` (`src/claude/intercept/settings-no-proxy.ts`, no token), and removal restores `previous` only while NO_PROXY still equals `written`, preserving anything else. Restoration needs that receipt: one that could not be written leaves none, and removal then keeps the appended entries (the harmless direction); a removal whose settings write fails before committing tries to put the receipt back. The receipt lives exactly as long as its ownership episode: an apply from an absent block and every removal invalidate it first and fail closed (`unreadable`) when they cannot, and rollback restores it with the captured value. The per-install proxy token is stored owner-only under `/claude-intercept/` (0600 plus a real per-user NTFS ACL on Windows, via `src/lib/windows-secret-acl.ts`, and re-pinned on every read-through `ensure`), and the settings file carrying it is written through the same hardened atomic writer. Every start runs `migrateClaudeInterceptSettings` (`src/claude/intercept/settings.ts`), which rewrites an owned env that no longer matches — e.g. a pre-auth URL left by an upgrade — while never creating an absent env or touching a foreign one, so a service restart cannot strand clients on 407s. Status/inspection reads the token without minting it; only apply and intercept startup create it. The TLS listener rewrites `POST /v1/messages` and `POST /v1/messages/count_tokens` to a loopback origin and dispatches them under the `claude-intercept` ingress; other paths relay to the configured upstream. One relayed path is extended: for the standalone CLI's picker catalog, the listener's `cliCatalog` hook (`src/claude/intercept/cli-catalog.ts`) merges opencodex rows into `GET /api/organizations//model_selector/cc` (or `/api/model_selector/cc`), touching only the `cc` surface and leaving Desktop's `ccd`/`code` surfaces to picker mode, and into the fallback `additional_model_options` of `GET /api/claude_cli/bootstrap`. Eligibility: the `cc` catalog is rewritten only for a CLI-classified `User-Agent` with `claudeCode.cliFirstParty` on, because Claude Code caches it on disk for every process and a Desktop-spawned process must not seed rows a plain `claude` cannot route; the bootstrap request carries no entrypoint and follows CLI intent alone. Rows come from `buildCliPickerModels` (`src/claude/intercept/picker-models.ts`), which uses Desktop 3P registry aliases because the CLI offers only Claude-shaped ids (an `ocx-claude-*` row renders "Update Claude Code"), and advertises only an alias the registry decodes back to the same route. `src/claude/intercept/cli-picker.ts` supplies them lazily on the first eligible request, persists the snapshot at `/claude-intercept/cli-picker-models.json`, re-checks each persisted row against the live registry (its alias must still decode to its recorded route; a cold registry after a restart is awaited first, and a snapshot whose rows still do not decode, for example after a provider change, is rebuilt within the same bound), and bounds a cold build at 2.5 s while awaiting the shared registry build in `src/claude/desktop-3p-startup.ts` that startup also uses (a failed or empty on-demand build is not retried for 30 s). Both CLI catalogs obey the shared [picker rewrite bounds](clients/claude-desktop.md#picker-catalog-rewrite-bounds), preflighting row growth before copying and keeping the final JSON within 16 MiB. The rewrite fails open: a provider error, non-2xx status, unparsable body, or no added row relays the upstream bytes unchanged; a body over the 16 MiB cap is cut off while streaming and answered 502 rather than buffered. Claude Code trusts its cached `/cache/model-catalog/*-cc.json` for about an hour, so `writeSettings` in `src/claude/intercept/settings.ts` and every successful `reconcileClaudeFirstPartySettings` drop those files (leaving Desktop's `*-ccd.json`); running sessions keep their list until restarted. The pair is on by default on a hub (`claudeCode.intercept.enabled`); its proxy port defaults to public port + 100 (`claudeCode.intercept.port`). The lifecycle records bind failure, and stop joins both sockets. Its serialized `ensure()` joins pending startup, reuses the live config and dispatch wrapper, and can start a previously unbound pair. `stop()` refuses future ensure calls and awaits an in-flight start. Local management applies and `ocx claude intercept start` (`POST /api/claude-intercept/start`) use this seam through `src/server/index/optional-listeners.ts`; hub-management ingress and data-plane credentials cannot start interception. Status reports `interceptReason` (with `interceptFailurePort` for a CONNECT bind failure) and optional `pickerReason` (with `pickerFailurePort` when the Desktop picker relay port is occupied), distinguishing disabled routing, client role, ephemeral public ports, occupied CONNECT ports and other failures. A changed configured port never rebinds a running pair: apply reports `port_mismatch` with both ports. With an ephemeral public port (`startServer(0)`), an explicit intercept port is required. The lifecycle makes a best-effort legacy picker-key cleanup attempt before checking client role, disabled routing/interception, or an ephemeral public port. Each new unbound lifecycle `ensure()` attempt retries cleanup; management requests rejected before that seam do not invoke it. Cleanup failures do not change the eligibility result or block startup. Direct `startClaudeIntercept` callers retain the same cleanup defense. Mock-only regression coverage lives in `tests/claude-integration/claude-intercept-cleanup.test.ts`. This ingress honours first-party model bindings (`claudeCode.intercept.modelMap`); see [Claude Desktop](clients/claude-desktop.md#first-party-model-bindings). Picker mode and the primitive's optional `allowedTargets` restriction are described under [Desktop egress proxy](clients/claude-desktop.md#picker-mode-the-desktop-egress-proxy). The macOS picker reuses an exportable signing identity protected by the OS credential store across normal restarts, without a plaintext key in the config directory. Restore verifies the constrained CA and current OS trust without installing trust; explicit picker `on` or `trust` grants missing or revoked trust. Legacy migration cleanup may still require consent, and native keychain unlock or application-access prompts are separate. Windows and Linux start no picker CA, credential-store or proxy work; the main intercept pair and best-effort legacy-key cleanup remain available. diff --git a/tests/claude-integration/claude-intercept-no-proxy.test.ts b/tests/claude-integration/claude-intercept-no-proxy.test.ts index aa5e8f8bbde..e8d11eb89db 100644 --- a/tests/claude-integration/claude-intercept-no-proxy.test.ts +++ b/tests/claude-integration/claude-intercept-no-proxy.test.ts @@ -10,6 +10,7 @@ import { inspectClaudeInterceptSettings, migrateClaudeInterceptSettings, removeClaudeInterceptSettings, + setClaudeSettingsPersistForTests, } from "../../src/claude/intercept/settings"; import { NO_PROXY_RECEIPT_FILE, setNoProxyReceiptIoForTests } from "../../src/claude/intercept/settings-no-proxy"; @@ -156,6 +157,38 @@ test("O11: an apply that starts a new episode drops a receipt left by an earlier expect(settings(configDir)).toEqual({ env: { NO_PROXY: LOOPBACK } }); }); +function failingPersist(): void { + restores.push(setClaudeSettingsPersistForTests(() => { throw new Error("settings write failed"); })); +} + +test("O14: an uncommitted removal keeps the episode, so a retry restores the user's value", () => { + const configDir = dir(); + seed(configDir, { env: { NO_PROXY: "" } }); + applyClaudeInterceptSettings(env, configDir); + const applied = bytes(configDir); + const proof = readFileSync(receipt(configDir), "utf8"); + failingPersist(); + expect(() => removeClaudeInterceptSettings(CA, configDir)).toThrow("settings write failed"); + expect(bytes(configDir)).toBe(applied); + expect(readFileSync(receipt(configDir), "utf8")).toBe(proof); + restores.pop()!(); + expect(removeClaudeInterceptSettings(CA, configDir)).toMatchObject({ ok: true, changed: true }); + expect(settings(configDir)).toEqual({ env: { NO_PROXY: "" } }); + expect(existsSync(receipt(configDir))).toBe(false); +}); + +test("O13: when the receipt cannot be put back either, the settings error still surfaces", () => { + const configDir = dir(); + seed(configDir, { env: { NO_PROXY: "" } }); + applyClaudeInterceptSettings(env, configDir); + const applied = bytes(configDir); + failingPersist(); + failing("write"); + expect(() => removeClaudeInterceptSettings(CA, configDir)).toThrow("settings write failed"); + expect(bytes(configDir)).toBe(applied); + expect(existsSync(receipt(configDir))).toBe(false); +}); + test("O6: a non-string NO_PROXY keeps the block stale and is never rewritten", () => { const configDir = dir(); seed(configDir, { env: { ...env, NO_PROXY: false } });