From b8901dd82fcb1d14f159a2409ba499d9d70d7c60 Mon Sep 17 00:00:00 2001 From: Roman Date: Sat, 3 Oct 2026 18:07:54 +0800 Subject: [PATCH] fix(agents): enable verified search and preserve local plugins --- AGENTS.md | 2 +- README.en.md | 2 +- README.md | 2 +- docs/copy-paste-setup.md | 119 ++++++++++++++++++++++++++--- docs/manual-setup.md | 22 ++++-- docs/troubleshooting.md | 5 +- scripts/macos/desktop-config.js | 34 +++++++++ scripts/macos/install.sh | 5 +- scripts/windows/desktop-config.ps1 | 49 +++++++++++- scripts/windows/setup.ps1 | 5 +- tests/macos/desktop_config.py | 62 +++++++++++++++ tests/macos/smoke.sh | 6 +- tests/static/profile_contract.py | 3 +- tests/static/validate.ps1 | 8 +- tests/windows/desktop-config.ps1 | 35 +++++++++ 15 files changed, 328 insertions(+), 31 deletions(-) create mode 100644 tests/macos/desktop_config.py diff --git a/AGENTS.md b/AGENTS.md index e15fe1c..868a05e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ - Validate script syntax, generated TOML/JSON, idempotency, secret handling, and uninstall boundaries before publishing. - Codex CLI uses a user-level profile-v2 file, `https://codex.neuroapi.host/v1`, and WebSocket by default; the optional Codex Desktop setup uses the same host with HTTP/SSE. Publish installers only after authenticated catalog and transport checks. -- Keep unsupported hosted Codex tools (`web_search`, multi-agent namespace, goals, apps and browser use) disabled in this profile until the server can execute and bill them safely; local file and shell tools must remain available. +- Managed Codex profiles use verified hosted `web_search = "live"` and `remote_plugin = false`; keep local plugins available. Multi-agent namespace, goals, apps and browser use stay disabled in the CLI profile until independently verified. Do not advertise standalone web-search support. Preserve unrelated Desktop feature preferences. - Managed launchers fetch and validate fresh key-scoped catalogs into private per-launch snapshots; never fall back to stale/bundled lists or accept executable server settings. Claude Code's base URL is `https://claude.neuroapi.host`; Claude Desktop gateway is configured separately in its UI. Keep ordinary keys, preserve unrelated configuration, and document managed-policy/explicit-override boundaries. - Claude client-settings v2 is opt-in via `X-NeuroAPI-Client-Settings-Version: 2`; accept only the three reviewed limit/hint env keys with canonical bounded decimal strings (or hint `1`), retain the 4096 output fallback for old servers. `--doctor` performs no generation; `--doctor-generate` explicitly opts into one bounded HTTP probe with no retries, secret-free output and no claim of WebSocket/tool compatibility. diff --git a/README.en.md b/README.en.md index 48a641b..3e30549 100644 --- a/README.en.md +++ b/README.en.md @@ -6,7 +6,7 @@ Public, auditable guided setup for routing local Codex CLI and Claude Code sessi ## Release compatibility -This version configures `https://codex.neuroapi.host/v1` with `supports_websockets = true`, and `https://claude.neuroapi.host` for Claude Code. The Codex profile disables hosted web search, multi-agent, goals, apps, and browser use because the current client includes these tools even in simple local tasks, while NeuroAPI does not guarantee their upstream execution. Local shell and file tools remain available. Setup checks both key-scoped catalogs without a paid generation. After setup, send one short real request and confirm it in your [NeuroAPI usage log](https://neuroapi.host/dashboard/logs); a catalog check alone does not prove generation works. +This version configures `https://codex.neuroapi.host/v1` with `supports_websockets = true`, and `https://claude.neuroapi.host` for Claude Code. The Codex profile enables hosted web search through NeuroAPI (`web_search = "live"`) and disables remote plugin catalog synchronization (`remote_plugin = false`). Local plugins, shell and file tools remain available. Multi-agent, goals, apps and browser use remain disabled in the CLI profile until their execution is independently verified. Setup checks both key-scoped catalogs without a paid generation. After setup, send one short real request and confirm it in your [NeuroAPI usage log](https://neuroapi.host/dashboard/logs); a catalog check alone does not prove generation works. An existing `CODEX_HOME` selects the profile directory without being modified. Keep its value consistent for setup, launch and uninstall. diff --git a/README.md b/README.md index c201a03..c1ef869 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ NeuroAPI — российский AI API-сервис: единый доступ ## Совместимость версии -Эта версия создаёт профиль Codex с `https://codex.neuroapi.host/v1` и `supports_websockets = true`, а профиль Claude Code — с `https://claude.neuroapi.host`. Codex-профиль отключает hosted web search, multi-agent, goals, apps и browser use: эти инструменты новейший клиент отправляет даже в простых задачах, а NeuroAPI пока не гарантирует их провайдерское исполнение. Локальные команды, чтение и редактирование файлов работают. Установщик проверяет доступ ключа к обоим каталогам без платной генерации. После установки отправьте короткий реальный запрос и проверьте его в [истории использования NeuroAPI](https://neuroapi.host/dashboard/logs): проверка каталога сама по себе не подтверждает генерацию. +Эта версия создаёт профиль Codex с `https://codex.neuroapi.host/v1` и `supports_websockets = true`, а профиль Claude Code — с `https://claude.neuroapi.host`. Codex-профиль включает hosted web search (`web_search = "live"`) через NeuroAPI и отключает синхронизацию удалённого каталога плагинов (`remote_plugin = false`). Локальные плагины, команды, чтение и редактирование файлов остаются доступны. Multi-agent, goals, apps и browser use в CLI-профиле отключены до отдельной проверки их исполнения. Установщик проверяет доступ ключа к обоим каталогам без платной генерации. После установки отправьте короткий реальный запрос и проверьте его в [истории использования NeuroAPI](https://neuroapi.host/dashboard/logs): проверка каталога сама по себе не подтверждает генерацию. Уже заданный `CODEX_HOME` учитывается для профиля и не изменяется; сохраняйте одинаковое значение при установке, запуске и удалении. diff --git a/docs/copy-paste-setup.md b/docs/copy-paste-setup.md index 1d29e39..14f91e1 100644 --- a/docs/copy-paste-setup.md +++ b/docs/copy-paste-setup.md @@ -10,8 +10,8 @@ | Клиент | Файл или экран | Что меняется | |---|---|---| -| Codex CLI | `~/.codex/neuroapi-manual.config.toml` или `%USERPROFILE%\.codex\neuroapi-manual.config.toml` | `model`, `model_provider`, `base_url`, `env_key` | -| Codex Desktop | `~/.codex/config.toml` или `%USERPROFILE%\.codex\config.toml` | root `model`/`model_provider` и секция провайдера; нужен защищённый helper | +| Codex CLI | `~/.codex/neuroapi-manual.config.toml` или `%USERPROFILE%\.codex\neuroapi-manual.config.toml` | `model`, `model_catalog_json`, `model_provider`, `base_url`, `env_key` | +| Codex Desktop | `~/.codex/config.toml` или `%USERPROFILE%\.codex\config.toml` | root `model`/`model_catalog_json`/`model_provider` и секция провайдера; нужен защищённый helper | | Claude Code | отдельный `~/.claude/neuroapi-manual-settings.json` или `%USERPROFILE%\.claude\neuroapi-manual-settings.json` | `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`; ключ только в терминале | | Claude Desktop | `Developer → Configure Third-Party Inference` | Gateway URL, ключ и схема авторизации в форме приложения | @@ -42,16 +42,112 @@ Remove-Variable secret, ptr Переменная существует только в этом терминале и его дочерних процессах. Не записывайте строку ключа через `export ...="sk-..."`, `setx` или в файл настроек. -### 2. Создайте отдельный профиль +### 2. Загрузите проверенный каталог + +Скачайте и распакуйте [публичный ZIP](https://github.com/neurogen-dev/NeuroAPI/archive/refs/heads/agents.zip), затем откройте терминал **в корне распакованной папки** (рядом с `scripts/`). Запускать setup для этого шага не нужно: команды используют его проверенные библиотеки. При ручном CLI используется временный `NEUROAPI_API_KEY` из шага 1; для Desktop без этой переменной команда читает уже сохранённый Keychain/DPAPI ключ установщика. + +Каталог определяет возможности модели, включая поиск, поэтому обязателен вместе с `web_search = "live"`. Нельзя заменять его стандартным OpenAI-списком `{ "data": [...] }` или придумывать capability-поля. Команды принимают только актуальный authenticated ответ выделенного Codex endpoint, проверяют схему, IDs, default, права и отсутствие отражённого ключа, затем атомарно заменяют приватный файл. При ошибке **не запускайте клиент со старым каталогом**. + +macOS — выполните весь блок из Bash (`/bin/bash`, если текущий shell другой): + +```bash +( + set +x + set -euo pipefail + umask 077 + repo_root="$(pwd -P)" + . "$repo_root/scripts/macos/common.sh" + catalog_root="$HOME/.local/share/neuroapi-manual-codex" + [[ ! -L "$catalog_root" ]] || exit 1 + mkdir -p "$catalog_root" + chmod 700 "$catalog_root" + [[ ! -L "$catalog_root/models.json" ]] || exit 1 + [[ ! -e "$catalog_root/models.json" || -f "$catalog_root/models.json" ]] || exit 1 + stage="$(mktemp -d "$catalog_root/.update.XXXXXX")" + trap 'rm -rf -- "$stage"' EXIT + credential="${NEUROAPI_API_KEY:-}" + helper="$HOME/.local/share/neuroapi-agents/bin/get-neuroapi-key.sh" + if [[ -z "$credential" ]]; then credential="$("$helper" 2>/dev/null)"; fi + [[ ${#credential} -le 4096 && "$credential" =~ ^[A-Za-z0-9._~-]+$ ]] || exit 1 + if ! printf 'header = "Authorization: Bearer %s"\n' "$credential" | + /usr/bin/curl --disable --config - --silent --fail --proto '=https' \ + --proto-redir '=https' --max-redirs 0 --connect-timeout 5 --max-time 20 \ + --max-filesize 4194304 --header 'Accept: application/json' \ + --write-out '\n%{http_code}' 'https://codex.neuroapi.host/v1/models' 2>/dev/null | + /usr/bin/osascript -l JavaScript "$repo_root/scripts/macos/catalog-validator.js" \ + codex "$stage" "$helper" 3< <(printf '%s' "$credential") >/dev/null 2>/dev/null; then + printf 'Каталог не обновлён. Проверьте ключ и соединение; запуск отменён.\n' >&2 + exit 1 + fi + unset credential + model="$(<"$stage/model.txt")" + mv -f -- "$stage/models.json" "$catalog_root/models.json" + printf 'model_catalog_json = "%s"\nmodel = "%s"\n' \ + "$(toml_escape "$catalog_root/models.json")" "$model" +) +``` + +Windows PowerShell — выполните весь блок из распакованной папки: + +```powershell +& { + $ErrorActionPreference = 'Stop' + $repoRoot = (Get-Location).Path + . (Join-Path $repoRoot 'scripts/windows/common.ps1') + . (Join-Path $repoRoot 'scripts/windows/managed-catalog.ps1') + $catalogRoot = Join-Path $env:LOCALAPPDATA 'NeuroAPIManualCodex' + Ensure-Directory -Path $catalogRoot + Set-NeuroAPIPrivateDirectory -Path $catalogRoot + $catalogPath = Join-Path $catalogRoot 'models.json' + if (Test-Path -LiteralPath $catalogPath) { + $item = Get-Item -LiteralPath $catalogPath -Force + if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { + throw 'Каталог моделей не должен быть каталогом или ссылкой.' + } + } + $stage = New-NeuroAPILaunchDirectory -StateRoot $catalogRoot + try { + $secretPath = Join-Path $env:LOCALAPPDATA 'NeuroAPIAgents/secret/api-key.dpapi' + if (-not [string]::IsNullOrEmpty($env:NEUROAPI_API_KEY)) { + # Windows DPAPI ciphertext only; never write the plaintext key. + $secretPath = Join-Path $stage 'temporary-key.dpapi' + $secure = ConvertTo-SecureString $env:NEUROAPI_API_KEY -AsPlainText -Force + try { Write-Utf8NoBom -Path $secretPath -Content (ConvertFrom-SecureString $secure) } + finally { $secure.Dispose() } + } + $json = Get-NeuroAPICatalogJson -Client codex -SecretPath $secretPath + $catalog = ConvertFrom-NeuroAPICatalog -Client codex -Json $json + $stagedCatalog = Join-Path $stage 'models.json' + Write-Utf8NoBom -Path $stagedCatalog -Content ($catalog.Content | ConvertTo-Json -Depth 100) + if (Test-Path -LiteralPath $catalogPath) { + [IO.File]::Replace($stagedCatalog, $catalogPath, (Join-Path $stage 'previous-models.json')) + } else { + [IO.File]::Move($stagedCatalog, $catalogPath) + } + 'model_catalog_json = ' + (ConvertTo-TomlBasicString -Value $catalogPath) + 'model = ' + (ConvertTo-TomlBasicString -Value $catalog.DefaultModel) + } finally { + Remove-Item -LiteralPath $stage -Recurse -Force + } +} +``` + +Оба блока печатают **две безопасные TOML-строки**: абсолютный `model_catalog_json` и доступную `model`. Скопируйте их в root конфигурации перед первой `[секцией]`, заменяя существующие значения без дубликатов. JSON содержит только проверенные метаданные моделей, не ключ. Обновляйте его перед каждым ручным CLI-запуском, перед запуском Desktop и после смены ключа/тарифа; после обновления перезапустите Desktop. Для автоматического обновления каждого CLI-запуска используйте `codex-neuroapi`. + +### 3. Создайте отдельный профиль macOS: `mkdir -p ~/.codex && nano ~/.codex/neuroapi-manual.config.toml`. Windows: `New-Item -ItemType Directory -Force "$HOME\.codex" | Out-Null; notepad "$HOME\.codex\neuroapi-manual.config.toml"`. Если установлен `CODEX_HOME`, откройте файл в нём. Вставьте весь блок: +Hosted поиск включён (`web_search = "live"`). `remote_plugin = false` отключает удалённый каталог плагинов и сохраняет локальные плагины; не добавляйте `plugins = false`. Остальные ограничения CLI-профиля оставьте как в примере. Подробнее — [ручная настройка](manual-setup.md). + ```toml model = "gpt-6-sol" # замените только ID модели, если вашему ключу доступна другая +model_catalog_json = "/АБСОЛЮТНЫЙ/ПУТЬ/models.json" # строка из шага 2 model_provider = "neuroapi_manual" -web_search = "disabled" +web_search = "live" [features] +remote_plugin = false multi_agent = false goals = false apps = false @@ -65,7 +161,7 @@ supports_websockets = true env_key = "NEUROAPI_API_KEY" ``` -Сохраните файл и **из того же терминала** запустите `codex --profile neuroapi-manual`. В клиенте проверьте `/debug-config`, затем поручите короткую файловую задачу. При проблеме с WebSocket замените только `supports_websockets = true` на `false`, перезапустите CLI и повторите задачу по HTTP/SSE. При ручном профиле меню Codex может содержать модели, которых нет у вашего ключа; используйте точный ID из каталога. После работы закройте терминал либо выполните `unset NEUROAPI_API_KEY` (macOS) / `Remove-Item Env:NEUROAPI_API_KEY` (PowerShell). +Сохраните файл и **из того же терминала** запустите `codex --profile neuroapi-manual`. В клиенте проверьте `/debug-config`, затем поручите короткую файловую задачу. При проблеме с WebSocket замените только `supports_websockets = true` на `false`, перезапустите CLI и повторите задачу по HTTP/SSE. Перед каждым запуском повторите шаг 2; при ошибке обновления остановитесь. `model` должна присутствовать в проверенном каталоге. Не удаляйте файл, пока клиент работает. После работы закройте терминал либо выполните `unset NEUROAPI_API_KEY` (macOS) / `Remove-Item Env:NEUROAPI_API_KEY` (PowerShell). ## Claude Code без установщика @@ -104,16 +200,19 @@ claude --settings "$HOME\.claude\neuroapi-manual-settings.json" 1. Полностью закройте Codex Desktop. Скопируйте существующий `config.toml` в резервную копию с датой. macOS: `cp -p ~/.codex/config.toml ~/.codex/config.toml.before-neuroapi-$(date +%Y%m%d-%H%M%S)` (если файла ещё нет, пропустите). Windows PowerShell: `Copy-Item "$HOME\.codex\config.toml" "$HOME\.codex\config.toml.before-neuroapi-$(Get-Date -Format yyyyMMdd-HHmmss)"` (если файл существует). 2. Проверьте наличие helper: macOS — `~/.local/share/neuroapi-agents/bin/get-neuroapi-key.sh`; Windows — `%LOCALAPPDATA%\NeuroAPIAgents\bin\get-neuroapi-key.ps1` и `%LOCALAPPDATA%\NeuroAPIAgents\secret\api-key.dpapi`. Не запускайте helper для диагностики с выводом на экран: он печатает секрет. -3. Откройте `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`). Если файл пустой, вставьте соответствующий блок ниже целиком. Если уже содержит настройки, **замените** существующие root `model` и `model_provider`, добавьте `web_search = "disabled"` в root, а секцию `[model_providers.neuroapi_manual_desktop]` с auth добавьте один раз в конец. Не создавайте второй `[features]`: обновите указанные поля внутри существующей секции. Не удаляйте другие провайдеры. +3. Для Desktop очистите только временную переменную текущего терминала: `unset NEUROAPI_API_KEY` (macOS) или `Remove-Item Env:NEUROAPI_API_KEY -ErrorAction SilentlyContinue` (PowerShell). Затем из распакованного ZIP выполните [шаг загрузки каталога](#2-загрузите-проверенный-каталог) выше: он использует уже сохранённый ключ helper — тот же, с которым Desktop будет отправлять запросы. Сохраните выведенные `model_catalog_json` и `model`. +4. Откройте `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`). Если файл пустой, вставьте соответствующий блок ниже целиком. Если уже содержит настройки, **замените** существующие root `model`, `model_catalog_json` и `model_provider`, добавьте `web_search = "live"` в root, а секцию `[model_providers.neuroapi_manual_desktop]` с auth добавьте один раз в конец. Не создавайте второй `[features]`: обновите указанные поля внутри существующей секции. Не удаляйте другие провайдеры. macOS: в `command` замените `ИМЯ_ПОЛЬЗОВАТЕЛЯ` на имя домашней папки из `echo "$HOME"` (например, `roman`): ```toml model = "gpt-6-sol" +model_catalog_json = "/АБСОЛЮТНЫЙ/ПУТЬ/models.json" # строка из шага 3 model_provider = "neuroapi_manual_desktop" -web_search = "disabled" +web_search = "live" [features] +remote_plugin = false multi_agent = false goals = false apps = false @@ -135,10 +234,12 @@ Windows: сначала выполните `$env:LOCALAPPDATA` в PowerShell. П ```toml model = "gpt-6-sol" +model_catalog_json = "/АБСОЛЮТНЫЙ/ПУТЬ/models.json" # строка из шага 3 model_provider = "neuroapi_manual_desktop" -web_search = "disabled" +web_search = "live" [features] +remote_plugin = false multi_agent = false goals = false apps = false @@ -157,7 +258,7 @@ timeout_ms = 5000 refresh_interval_ms = 300000 ``` -Также замените `model` на доступный ключу ID. Этот **ручной** Desktop-вариант не создаёт управляемый `model_catalog_json`, поэтому встроенный список моделей может содержать посторонние варианты. Перезапустите приложение, создайте **новую локальную** задачу и проверьте её в [истории NeuroAPI](https://neuroapi.host/dashboard/logs). Не удаляйте helper/ключ через uninstaller, пока Desktop-config ссылается на него. Если захотите перейти на автоматическое управление Desktop, сначала уберите ручную секцию и верните прежние root-настройки из сохранённой копии, затем повторите setup с опцией Desktop: иначе последующее удаление установщика может восстановить конфиг, который всё ещё ссылается на удаляемый helper. +В обоих Desktop-блоках замените пример `model_catalog_json` и `model` **точными строками**, выведенными шагом 3. В Windows это уже экранированный TOML-путь; не вставляйте буквально `%LOCALAPPDATA%`, `$HOME` или `~`. Каталог постоянный и приватный, но обновляется вручную: перед запуском Desktop и после смены ключа/тарифа повторяйте загрузку; при ошибке обновления клиент не запускайте. Перезапустите приложение, создайте **новую локальную** задачу и проверьте её в [истории NeuroAPI](https://neuroapi.host/dashboard/logs). Не удаляйте helper/ключ через uninstaller, пока Desktop-config ссылается на него. Если захотите перейти на автоматическое управление Desktop, сначала уберите ручную секцию и верните прежние root-настройки из сохранённой копии, затем повторите setup с опцией Desktop: иначе последующее удаление установщика может восстановить конфиг, который всё ещё ссылается на удаляемый helper. ## Claude Desktop вручную diff --git a/docs/manual-setup.md b/docs/manual-setup.md index a8bd762..7f8e997 100644 --- a/docs/manual-setup.md +++ b/docs/manual-setup.md @@ -37,14 +37,18 @@ finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } Remove-Variable secure, ptr ``` -Для Codex создайте файл `~/.codex/neuroapi-manual.config.toml` (Windows: `%USERPROFILE%\.codex\neuroapi-manual.config.toml`): +Для Codex сначала [загрузите проверенный приватный каталог](copy-paste-setup.md#2-загрузите-проверенный-каталог) командами для своей ОС из распакованного публичного ZIP. Установка не обязательна: для CLI используются временный ключ и проверенные библиотеки архива, для Desktop — уже сохранённый helper. Команды проверяют authenticated ответ, атомарно сохраняют JSON без ключа и печатают абсолютный `model_catalog_json` и доступную `model`. Не запускайте клиент, если обновление не удалось. + +Затем создайте файл `~/.codex/neuroapi-manual.config.toml` (Windows: `%USERPROFILE%\.codex\neuroapi-manual.config.toml`): ```toml model = "gpt-6-sol" # замените на модель, доступную вашему ключу +model_catalog_json = "/АБСОЛЮТНЫЙ/ПУТЬ/models.json" # точная строка из загрузчика каталога model_provider = "neuroapi_manual" -web_search = "disabled" +web_search = "live" [features] +remote_plugin = false multi_agent = false goals = false apps = false @@ -58,7 +62,7 @@ supports_websockets = true env_key = "NEUROAPI_API_KEY" ``` -Запустите `codex --profile neuroapi-manual`, проверьте `/debug-config` и выполните короткую задачу. Этот ручной профиль не получает управляемый каталог: встроенное меню Codex может содержать недоступные модели. Выбирайте проверенный ID явно. Файл профиля должен находиться в том же `CODEX_HOME`, с которым вы запускаете Codex; проектный `.codex/config.toml` не заменяет его. +Замените пример пути `model_catalog_json` точной выведенной строкой и `model` доступным ID из того же каталога. Перед каждым запуском обновляйте каталог по ссылке выше, затем запустите `codex --profile neuroapi-manual`, проверьте `/debug-config` и выполните короткую задачу. Каталог задаёт также возможности модели, включая hosted search; встроенный каталог не заменяет его. Файл профиля должен находиться в том же `CODEX_HOME`, с которым вы запускаете Codex; проектный `.codex/config.toml` не заменяет его. Для Claude Code в том же терминале задайте адрес и модель, затем запустите `claude`: @@ -85,10 +89,12 @@ Codex Desktop не наследует переменную из терминал Основная конфигурация: ```toml +# Launcher передаёт актуальный абсолютный model_catalog_json через -c при каждом запуске. model_provider = "neuroapi" -web_search = "disabled" +web_search = "live" [features] +remote_plugin = false multi_agent = false goals = false apps = false @@ -108,11 +114,13 @@ refresh_interval_ms = 300000 На Windows `command` — `powershell.exe`, а helper и DPAPI secret передаются отдельными элементами `args`. -Codex 0.158.0 по умолчанию добавляет к каждому запросу hosted `web_search` и namespace-инструмент для multi-agent, даже при локальном чтении файла. NeuroAPI не объявляет эти инструменты как поддерживаемые для Codex-профиля: они требуют отдельного провайдерского контракта и тарификации. Профиль отключает только эти возможности, а чтение, правка и запуск команд остаются доступны. Возвращать их вручную в профиле можно лишь после отдельной проверки поддержки сервером. +Hosted web search включён через NeuroAPI: `web_search = "live"` запрашивает актуальные страницы. Его использование может тарифицироваться отдельно от токенов; актуальные цены указаны в [каталоге NeuroAPI](https://neuroapi.host/price). `remote_plugin = false` отключает удалённый каталог плагинов; поле `plugins` не отключается, поэтому установленные локальные плагины остаются доступны. CLI-профиль сохраняет отключёнными multi-agent, goals, apps и browser use до отдельной проверки поддержки. + +Режимы поиска описаны в [официальной документации](https://learn.chatgpt.com/docs/web-search). Раздельные флаги `plugins` и `remote_plugin` есть уже в [Codex 0.158.0](https://github.com/openai/codex/blob/rust-v0.158.0/codex-rs/features/src/lib.rs); минимум версии установщика не меняется. Это не отключает всю сеть плагинов: локальные плагины и их MCP, обновление Git-каталогов и функции приложения могут обращаться к собственным сервисам. Не включайте `supports_standalone_web_search`: отдельная самостоятельная search-сессия у стороннего провайдера не заявлена. Запуск: `codex-neuroapi`. Перед каждым запуском launcher получает `https://codex.neuroapi.host/v1/models` с обычным ключом NeuroAPI, проверяет ответ и передаёт приватный файл через `model_catalog_json` вместе с доступной моделью по умолчанию. Файл удаляется после завершения клиента. Проверка: `/debug-config` и `/model`. -Прямой `codex --profile neuroapi-host` пропускает этот механизм: command-auth discovery может подмешать встроенные модели. При ручной настройке без launcher можно задать собственный проверенный `model_catalog_json`; поддерживать его актуальность тогда нужно самостоятельно. +Прямой `codex --profile neuroapi-host` пропускает получение каталога и не является эквивалентом launcher. Для ручного профиля обязательно задайте проверенный `model_catalog_json` в root по [командам загрузки](copy-paste-setup.md#2-загрузите-проверенный-каталог), обновляя его перед каждым запуском; `model` выбирайте из того же snapshot. Project `.codex/config.toml` не подходит для provider/auth redirect: актуальный Codex игнорирует там `model_provider` и `model_providers` по соображениям безопасности. @@ -120,7 +128,7 @@ Project `.codex/config.toml` не подходит для provider/auth redirect Установщик предлагает включить пользовательский Codex Desktop. В этом случае он сохраняет точную исходную копию `~/.codex/config.toml`, затем устанавливает в нём `model_provider = "neuroapi_agents"`, `model_catalog_json` с моделями, доступными введённому ключу, и подходящую модель по умолчанию. Провайдер использует `https://codex.neuroapi.host/v1`, Responses API, HTTP/SSE (`supports_websockets = false`) и тот же защищённый DPAPI/Keychain helper. Отдельный профиль CLI остаётся независимым. -Если в исходном файле есть конфликтующий провайдер, необычная форма root-настроек, неверный TOML либо файл изменился во время установки, setup останавливается без перезаписи. Повторная установка сохраняет первоначальную копию. При удалении проверяется хеш конфигурации: если пользователь изменил файл после setup, uninstaller не удаляет helper и ключ, чтобы не сломать действующую настройку. После установки перезапустите Codex Desktop и проверьте новую локальную задачу; полная инструкция: [Codex Desktop](https://neuroapi.host/docs/codex-desktop). +Если в исходном файле есть конфликтующий провайдер, необычная форма root-настроек, неверный TOML либо файл изменился во время установки, setup останавливается без перезаписи. Повторная установка сохраняет первоначальную копию. При удалении проверяется хеш конфигурации: если пользователь изменил файл после setup, uninstaller не удаляет helper и ключ, чтобы не сломать действующую настройку. Для ручного Desktop-конфига с уже сохранённым helper также обязательны [загрузка каталога и root model_catalog_json](copy-paste-setup.md#codex-desktop-вручную-с-уже-сохранённым-ключом). Ручной JSON не обновляется автоматически: обновляйте его перед запуском приложения и после смены ключа/тарифа, а при ошибке остановитесь. После установки перезапустите Codex Desktop и проверьте новую локальную задачу; полная инструкция: [Codex Desktop](https://neuroapi.host/docs/codex-desktop). ## HTTP/SSE для диагностики diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ddd5e6a..eb42bf1 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -6,6 +6,8 @@ Проверьте пользовательский `~/.codex/config.toml`: `model_provider = "neuroapi_agents"`, `base_url = "https://codex.neuroapi.host/v1"`, `supports_websockets = false`. Перезапустите приложение и создайте **новую локальную** задачу. Старые задачи и облачные функции могут сохранять прежний маршрут. Если вы пропустили предложение установщика, запустите setup заново и согласитесь на подключение Desktop. +Если ошибка относится к загрузке плагинов, проверьте `remote_plugin = false` в существующей секции `[features]` и запустите setup повторно. Синхронизация удалённого каталога плагинов и генерация модели — разные операции. Этот флаг сохраняет локальные плагины; их MCP, обновления Git-каталогов и облачные функции приложения могут использовать собственную сеть. + Если удаление останавливается из-за изменения `config.toml`, сначала сравните текущий файл с резервной копией в каталоге установщика. Это защищает ваши правки и сохраняет key helper до ручного разбора. ## `codex-neuroapi` или `claude-neuroapi` не найдены @@ -23,7 +25,8 @@ macOS: используйте полный путь `~/.local/bin/codex-neuroapi - base URL `https://codex.neuroapi.host/v1`; - `wire_api = "responses"`; - `supports_websockets = true` (или `false` для диагностики HTTP/SSE). -- `web_search = "disabled"` и `[features]` с `multi_agent = false`, `goals = false`, `apps = false`, `browser_use = false`: без них Codex 0.158.0 может включить неподдерживаемые hosted/namespace-инструменты в обычный запрос к файлу и получить `effective_request_unsupported`. +- `web_search = "live"` и `[features]` с `remote_plugin = false`: hosted поиск идёт через NeuroAPI, удалённый каталог плагинов отключён. Не задавайте `plugins = false`, если используете установленные локальные плагины. +- В CLI-профиле остаются `multi_agent = false`, `goals = false`, `apps = false`, `browser_use = false`: эти возможности требуют отдельной проверки исполнения. Если `~/.codex/neuroapi-host.config.toml` существовал до установки без ownership-marker, setup должен отказать, а не перезаписать его. diff --git a/scripts/macos/desktop-config.js b/scripts/macos/desktop-config.js index a50f44b..74e7703 100644 --- a/scripts/macos/desktop-config.js +++ b/scripts/macos/desktop-config.js @@ -35,6 +35,38 @@ function rootSetting(source, key, value, newline) { } return root + rest } +function disableRemotePlugins(source, newline) { + var firstTable = /^[ \t]*\[[^\r\n]*\]/m.exec(source) + var root = source.slice(0, firstTable ? firstTable.index : source.length) + if (/^[ \t]*(?:features|"features"|'features')[ \t]*(?:=|\.)/m.test(root)) + fail('Inline or dotted features require manual Codex Desktop setup') + var headers = [], match + var headerPattern = /^[ \t]*\[[ \t]*(?:features|"features"|'features')[ \t]*\][ \t]*(?:#.*)?\r?$/gm + while ((match = headerPattern.exec(source)) !== null) headers.push(match) + if (headers.length > 1) fail('Duplicate Codex features table') + if (headers.length === 0) { + if (source.length && !/\n$/.test(source)) source += newline + return source + newline + '[features]' + newline + 'remote_plugin = false' + newline + } + var start = headers[0].index + headers[0][0].length + var following = /^[ \t]*\[[^\r\n]*\]/m.exec(source.slice(start)) + var end = following ? start + following.index : source.length + var section = source.slice(start, end) + var toggles = [], toggle + var togglePattern = /^([ \t]*)(?:remote_plugin|"remote_plugin"|'remote_plugin')[ \t]*=[ \t]*(.*)$/gm + while ((toggle = togglePattern.exec(section)) !== null) toggles.push(toggle) + if (toggles.length > 1) fail('Duplicate Codex remote_plugin setting') + if (toggles.length === 1) { + var suffix = /^(?:true|false)([ \t]*(?:#.*)?\r?)$/.exec(toggles[0][2]) + if (!suffix) fail('Unsupported Codex remote_plugin setting') + section = section.slice(0, toggles[0].index) + toggles[0][1] + 'remote_plugin = false' + suffix[1] + + section.slice(toggles[0].index + toggles[0][0].length) + } else { + section = newline + 'remote_plugin = false' + section + if (!/\n$/.test(section)) section += newline + } + return source.slice(0, start) + section + source.slice(end) +} function run(args) { if (args.length !== 5) fail('Invalid desktop config arguments') var original = read(args[0]) @@ -50,6 +82,8 @@ function run(args) { result = rootSetting(result, 'model_provider', 'neuroapi_agents', newline) result = rootSetting(result, 'model_catalog_json', args[2], newline) result = rootSetting(result, 'model', model, newline) + result = rootSetting(result, 'web_search', 'live', newline) + result = disableRemotePlugins(result, newline) if (result.length && !/\n$/.test(result)) result += newline result += ('\n# NeuroAPI Agents desktop provider. Owned by the installer.\n' + '[model_providers.neuroapi_agents]\n' + diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh index 3f2b82d..7b0979d 100755 --- a/scripts/macos/install.sh +++ b/scripts/macos/install.sh @@ -304,10 +304,11 @@ ESCAPED_HELPER_PATH="$(toml_escape "$HELPER_PATH")" cat >"$STAGE_ROOT/profile.toml" <\s*)(?:remote_plugin|"remote_plugin"|''remote_plugin'')\s*=\s*(?:true|false)(?\s*(?:#.*)?)$') { + throw 'Unsupported Codex remote_plugin setting.' + } + $lines[$found[0]] = $Matches.indent + 'remote_plugin = false' + $Matches.suffix + } else { + $suffix = @() + if ($featureStart -lt $lines.Count) { $suffix = @($lines[$featureStart..($lines.Count - 1)]) } + $lines = @($lines[0..($featureStart - 1)]) + @('remote_plugin = false') + $suffix + } + return $lines -join "`n" +} + function New-NeuroAPIDesktopConfig { param( [AllowEmptyString()][string]$Original, @@ -50,6 +89,11 @@ function New-NeuroAPIDesktopConfig { [string]$DefaultModel ) if (-not (Test-NeuroAPIModelId $DefaultModel)) { throw 'No valid Codex model is available for this key.' } + # Table-looking text inside multiline strings is not a real TOML table. + # This editor deliberately supports only unambiguous single-line values. + if ($Original -match '("""|'''''')') { + throw 'Multiline TOML requires manual Codex Desktop setup. No changes were made.' + } if ($Original -match '(?m)^\s*\[\s*model_providers\.\s*(?:neuroapi_agents|"neuroapi_agents"|''neuroapi_agents'')(?:\.|\])') { throw 'The Codex provider ID is already in use. No changes were made.' } @@ -62,13 +106,11 @@ function New-NeuroAPIDesktopConfig { [string[]]$tail = @() if ($firstTable -gt 0) { $header = @($lines[0..($firstTable - 1)]) } if ($firstTable -lt $lines.Count) { $tail = @($lines[$firstTable..($lines.Count - 1)]) } - if (($header -join "`n") -match '("""|'''''')') { - throw 'Multiline root TOML cannot be edited safely. No changes were made.' - } $values = [ordered]@{ model_provider = (ConvertTo-TomlBasicString -Value $script:DesktopProviderId) model_catalog_json = (ConvertTo-TomlBasicString -Value $CatalogPath) model = (ConvertTo-TomlBasicString -Value $DefaultModel) + web_search = '"live"' } foreach ($key in @($values.Keys)) { $found = @() @@ -87,6 +129,7 @@ function New-NeuroAPIDesktopConfig { } } $base = ((@($header) + @($tail)) -join "`n").TrimEnd("`r", "`n") + $base = Disable-NeuroAPIDesktopRemotePlugins -Config $base $tomlHelper = ConvertTo-TomlBasicString -Value $HelperPath $tomlSecret = ConvertTo-TomlBasicString -Value $SecretPath return ($base + "`n`n" + @" diff --git a/scripts/windows/setup.ps1 b/scripts/windows/setup.ps1 index 8d82ac7..72fed28 100644 --- a/scripts/windows/setup.ps1 +++ b/scripts/windows/setup.ps1 @@ -183,10 +183,11 @@ try { $profile = @" # Managed by the NeuroAPI Agents installer. model_provider = "neuroapi" -web_search = "disabled" +web_search = "live" -# These Codex-hosted tools are not part of the NeuroAPI Responses contract. +# Keep unsupported agent features off; local plugins remain available. [features] +remote_plugin = false multi_agent = false goals = false apps = false diff --git a/tests/macos/desktop_config.py b/tests/macos/desktop_config.py new file mode 100644 index 0000000..22b618d --- /dev/null +++ b/tests/macos/desktop_config.py @@ -0,0 +1,62 @@ +"""Exercise the actual JXA desktop editor without credentials or user settings.""" +import pathlib +import subprocess +import tempfile +import tomllib + + +EDITOR = pathlib.Path(__file__).resolve().parents[2] / "scripts/macos/desktop-config.js" + + +def render(original: str, expected_success: bool = True) -> str: + with tempfile.TemporaryDirectory(prefix="neuroapi-desktop-config-") as temp: + root = pathlib.Path(temp) + (root / "original").write_text(original) + (root / "model").write_text("gpt-6-sol") + result = subprocess.run( + ["/usr/bin/osascript", "-l", "JavaScript", str(EDITOR), + str(root / "original"), str(root / "model"), + str(root / "catalog.json"), "/tmp/synthetic-key-helper", str(root / "output")], + capture_output=True, text=True, timeout=15, + ) + assert (result.returncode == 0) == expected_success, result.stderr + if not expected_success: + assert not (root / "output").exists(), "Invalid config was staged" + return "" + output = (root / "output").read_text() + config = tomllib.loads(output) + assert config["web_search"] == "live" + assert config["features"]["remote_plugin"] is False + assert config["model"] == "gpt-6-sol" + assert config["model_catalog_json"] == str(root / "catalog.json") + assert config["model_providers"]["neuroapi_agents"]["supports_websockets"] is False + return output + + +for fixture in ("", "[features]", "['features']\nplugins = true", + '["features"]\n"remote_plugin" = true', + '[features]\r\nremote_plugin = true\r\nplugins = true\r\n'): + render(fixture) + +preserved = render('''# user preferences +web_search = "cached" +[features] +plugins = true +remote_plugin = true # keep comment +multi_agent = true +[mcp_servers.demo] +url = "https://example.test/mcp" +''') +parsed = tomllib.loads(preserved) +assert parsed["features"]["plugins"] is True +assert parsed["features"]["multi_agent"] is True +assert parsed["mcp_servers"]["demo"]["url"] == "https://example.test/mcp" +assert "remote_plugin = false # keep comment" in preserved +assert preserved.count("[features]") == 1 + +for fixture in ("features = { plugins = true }", "features.plugins = true", + "[features]\n[features]", "[features]\nremote_plugin = 'true'", + "[features]\nremote_plugin = true\nremote_plugin = false"): + render(fixture, expected_success=False) + +print("macOS Desktop config tests passed.") diff --git a/tests/macos/smoke.sh b/tests/macos/smoke.sh index 199113c..ec975a6 100755 --- a/tests/macos/smoke.sh +++ b/tests/macos/smoke.sh @@ -4,6 +4,7 @@ set -euo pipefail REPO_ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)" PYTHON_BIN="${NEUROAPI_AGENTS_TEST_PYTHON:-python3}" "$PYTHON_BIN" -c 'import tomllib' +"$PYTHON_BIN" "$REPO_ROOT/tests/macos/desktop_config.py" TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/neuroapi-agents-test.XXXXXX")" # Resolve synthetic values through the same pure helper used by setup/uninstall. # shellcheck source=scripts/macos/common.sh @@ -242,8 +243,11 @@ mkdir -p "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME" cat >"$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" <<'EOF' # personal Codex settings model = "gpt-6-astra" +web_search = "cached" [features] +plugins = true +remote_plugin = true # existing preference apps = false EOF cp "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" "$TMP_ROOT/desktop-original.toml" @@ -285,7 +289,7 @@ grep -Fq 'base_url = "https://codex.neuroapi.host/v1"' "$desktop_config" grep -Fq 'supports_websockets = false' "$desktop_config" grep -Fq 'model = "gpt-6-sol"' "$desktop_config" [[ -f "$desktop_catalog" ]] -"$PYTHON_BIN" -c 'import json,pathlib,sys,tomllib; cfg=tomllib.loads(pathlib.Path(sys.argv[1]).read_text()); catalog=json.loads(pathlib.Path(sys.argv[2]).read_text()); assert cfg["model"]==catalog["models"][0]["slug"]' "$desktop_config" "$desktop_catalog" +"$PYTHON_BIN" -c 'import json,pathlib,sys,tomllib; cfg=tomllib.loads(pathlib.Path(sys.argv[1]).read_text()); catalog=json.loads(pathlib.Path(sys.argv[2]).read_text()); assert cfg["model"]==catalog["models"][0]["slug"]; assert cfg["web_search"]=="live"; assert cfg["features"]=={"plugins":True,"remote_plugin":False,"apps":False}' "$desktop_config" "$desktop_catalog" if grep -R -Fq 'good-rotation' "$desktop_config" "$desktop_catalog"; then printf 'Desktop configuration exposed the dummy token.\n' >&2 exit 1 diff --git a/tests/static/profile_contract.py b/tests/static/profile_contract.py index 8384683..37a2f8e 100644 --- a/tests/static/profile_contract.py +++ b/tests/static/profile_contract.py @@ -8,8 +8,9 @@ profile = tomllib.loads(raw) assert "model" not in profile # The authenticated startup catalog supplies it. assert profile["model_provider"] == "neuroapi" -assert profile["web_search"] == "disabled" +assert profile["web_search"] == "live" assert profile["features"] == { + "remote_plugin": False, "multi_agent": False, "goals": False, "apps": False, diff --git a/tests/static/validate.ps1 b/tests/static/validate.ps1 index 9a60216..716b06e 100644 --- a/tests/static/validate.ps1 +++ b/tests/static/validate.ps1 @@ -89,8 +89,12 @@ foreach ($setup in @($windowsSetup, $macSetup)) { 'Codex must use the profile-specific API and catalog.' Assert-Contains $setup 'supports_websockets = true' ` 'Codex must enable the supported Responses WebSocket transport.' - Assert-Contains $setup 'web_search = "disabled"' ` - 'Codex must omit unsupported hosted web search from ordinary requests.' + Assert-Contains $setup 'web_search = "live"' ` + 'Codex must enable verified hosted web search through NeuroAPI.' + Assert-Contains $setup 'remote_plugin = false' ` + 'Codex must avoid remote ChatGPT plugin synchronization without disabling local plugins.' + Assert-NotMatches $setup '(?m)^plugins\s*=\s*false' ` + 'Setup must preserve local plugin availability.' Assert-Contains $setup 'multi_agent = false' ` 'Codex must omit unsupported namespace tools from ordinary requests.' Assert-Contains $setup '[model_providers.neuroapi.auth]' ` diff --git a/tests/windows/desktop-config.ps1 b/tests/windows/desktop-config.ps1 index e0aaaa8..7e3d4dc 100644 --- a/tests/windows/desktop-config.ps1 +++ b/tests/windows/desktop-config.ps1 @@ -24,6 +24,8 @@ $options = @{ } $fresh = New-NeuroAPIDesktopConfig -Original '' @options Assert-Desktop ($fresh -match '(?m)^model_provider = "neuroapi_agents"$') 'Fresh config has no provider.' +Assert-Desktop ($fresh -match '(?m)^web_search = "live"$') 'Fresh config has no hosted search.' +Assert-Desktop ($fresh -match '(?m)^remote_plugin = false$') 'Fresh config enables remote plugin sync.' Assert-Desktop ($fresh -match '(?m)^model = "gpt-6-sol"$') 'Fresh config has no supported default.' Assert-Desktop ($fresh -match '(?m)^base_url = "https://codex.neuroapi.host/v1"$') 'Desktop subdomain is wrong.' Assert-Desktop ($fresh -match '(?m)^supports_websockets = false$') 'Unproven WebSocket path was enabled.' @@ -34,12 +36,20 @@ $existing = @' # User comment model = "old-model" # keep elsewhere model_provider = "other" +web_search = "cached" [features] +plugins = true +remote_plugin = true # preserve comment multi_agent = true [mcp_servers.demo] url = "https://example.test/mcp" '@ $updated = New-NeuroAPIDesktopConfig -Original $existing @options +Assert-Desktop ($updated -match '(?m)^web_search = "live"$') 'Hosted search was not enabled.' +Assert-Desktop ($updated -match '(?m)^remote_plugin = false # preserve comment$') 'Remote plugin sync was not disabled.' +Assert-Desktop ($updated -match '(?m)^plugins = true$') 'Local plugins were disabled.' +Assert-Desktop (([regex]::Matches($updated, '(?m)^\[features\]$')).Count -eq 1) 'Features table was duplicated.' + Assert-Desktop ($updated -match '(?m)^multi_agent = true$') 'User feature was changed.' Assert-Desktop ($updated -match '(?m)^url = "https://example.test/mcp"$') 'User MCP was changed.' Assert-Desktop (([regex]::Matches($updated, '(?m)^model_provider\s*=')).Count -eq 1) 'Provider root key was duplicated.' @@ -51,7 +61,32 @@ Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original '[model_providers."n $literal = New-NeuroAPIDesktopConfig -Original "model = 'literal'" @options Assert-Desktop ($literal -match '(?m)^model = "gpt-6-sol"$') 'Literal TOML strings were not handled.' Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original 'model = """multiline"""' @options } +$nestedMultiline = @' +[mcp_servers.demo] +url = "https://example.test/mcp" +notes = """ +[features] +remote_plugin = true +""" +'@ +# Both forms are valid TOML strings; their fake table must never be edited. +Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original $nestedMultiline @options } +$nestedLiteral = $nestedMultiline.Replace('"""', ("'" * 3)) +Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original $nestedLiteral @options } Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original "model = `"one`"`nmodel = `"two`"" @options } +foreach ($shape in @('[features]', "['features']`nplugins = true", "[`"features`"]`n`"remote_plugin`" = true")) { + $configured = New-NeuroAPIDesktopConfig -Original $shape @options + Assert-Desktop ($configured -match '(?m)^remote_plugin = false$') 'Empty or quoted features table was not updated.' +} +foreach ($shape in @( + "[features]`nremote_plugin = true`nremote_plugin = false", + "[features]`nremote_plugin = 'true'", + "[features]`n[features]", + 'features = { plugins = true }', + 'features.plugins = true' +)) { + Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original $shape @options } +} Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original '' -HelperPath $options.HelperPath -SecretPath $options.SecretPath -CatalogPath $options.CatalogPath -DefaultModel 'bad model' } $temp = Join-Path ([IO.Path]::GetTempPath()) ('neuroapi-desktop-' + [Guid]::NewGuid().ToString('N'))