diff --git a/AGENTS.md b/AGENTS.md index c5b194c..c292a2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ - 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. - Managed Codex CLI uses verified hosted `web_search = "live"`, `plugins = false` and `remote_plugin = false`: API-key startup must not sync plugin catalogs. Standalone user-configured MCP remains independent. Multi-agent namespace, goals, apps and browser use stay disabled in the CLI profile until independently verified. Do not advertise standalone web-search support. Desktop edits only `remote_plugin`, preserving existing `plugins` and other feature preferences; document optional manual plugin disabling. +- The validated Codex catalog `default_model` is authoritative for setup and launch on both platforms, including Desktop; never hardcode a preferred model or infer the default from list order. - 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/docs/manual-setup.md b/docs/manual-setup.md index dcb688f..4adb041 100644 --- a/docs/manual-setup.md +++ b/docs/manual-setup.md @@ -128,7 +128,7 @@ Project `.codex/config.toml` не подходит для provider/auth redirect ## Codex Desktop (по отдельному согласию) -Установщик предлагает включить пользовательский 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 остаётся независимым. +Установщик предлагает включить пользовательский Codex Desktop. В этом случае он сохраняет точную исходную копию `~/.codex/config.toml`, затем устанавливает в нём `model_provider = "neuroapi_agents"`, `model_catalog_json` с моделями, доступными введённому ключу, и модель из проверенного серверного `default_model`. Порядок списка или наличие Sol не заменяют эту рекомендацию; правило одинаково на Windows и macOS. Провайдер использует `https://codex.neuroapi.host/v1`, Responses API, HTTP/SSE (`supports_websockets = false`) и тот же защищённый DPAPI/Keychain helper. Отдельный профиль CLI остаётся независимым. Если в исходном файле есть конфликтующий провайдер, необычная форма 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). diff --git a/scripts/macos/install.sh b/scripts/macos/install.sh index acd865a..ad6b83d 100755 --- a/scripts/macos/install.sh +++ b/scripts/macos/install.sh @@ -272,6 +272,7 @@ if [[ "$DESKTOP_OPT_IN" == '1' ]]; then printf 'Не удалось проверить модели Codex Desktop для этого ключа. Настройки сохранены.\n' >&2 exit 1 fi + # model.txt is the validated server default, never a hardcoded preferred model. /usr/bin/osascript -l JavaScript "$SCRIPT_DIR/desktop-config.js" \ "$STAGE_ROOT/desktop/original.toml" "$STAGE_ROOT/desktop/model.txt" \ "$DESKTOP_CATALOG_PATH" "$HELPER_PATH" "$STAGE_ROOT/desktop/config.toml" diff --git a/scripts/windows/setup.ps1 b/scripts/windows/setup.ps1 index d744324..2844012 100644 --- a/scripts/windows/setup.ps1 +++ b/scripts/windows/setup.ps1 @@ -225,13 +225,10 @@ refresh_interval_ms = 300000 Get-NeuroAPICatalogJson -Client codex -SecretPath (Join-Path $stageRoot 'api-key.dpapi') } $catalog = ConvertFrom-NeuroAPICatalog -Client codex -Json $catalogJson -HelperCommand '' - $preferred = @($catalog.Content.models | Where-Object { - $_.slug -ceq 'gpt-6-sol' -and $_.visibility -ceq 'list' -and $_.supported_in_api - }) - $desktopModel = if ($preferred.Count -gt 0) { 'gpt-6-sol' } else { $catalog.DefaultModel } + # The validated server default owns the recommendation, independently of model order. $desktopContent = New-NeuroAPIDesktopConfig -Original $desktopOriginal ` -HelperPath $helperPath -SecretPath $secretPath -CatalogPath $desktopCatalogPath ` - -DefaultModel $desktopModel + -DefaultModel $catalog.DefaultModel $catalogFile = @{ models = @($catalog.Content.models) } | ConvertTo-Json -Depth 32 $stagedDesktopCatalog = Join-Path $stageRoot 'desktop-catalog.json' Write-Utf8NoBom -Path $stagedDesktopCatalog -Content $catalogFile diff --git a/tests/macos/smoke.sh b/tests/macos/smoke.sh index ec975a6..4215dcd 100755 --- a/tests/macos/smoke.sh +++ b/tests/macos/smoke.sh @@ -178,7 +178,8 @@ model = dict(slug='gpt-6-sol', display_name='GPT-6 Sol', description='Test model effective_context_window_percent=90, experimental_supported_tools=[], input_modalities=['text'], supports_search_tool=False, use_responses_lite=False, input_token_limit=8000, output_token_limit=2000) -pathlib.Path(sys.argv[1]).write_text(json.dumps({'models': [model], 'default_model': model['slug']})) +luna = dict(model, slug='gpt-6-luna', display_name='GPT-6 Luna') +pathlib.Path(sys.argv[1]).write_text(json.dumps({'models': [model, luna], 'default_model': luna['slug']})) pathlib.Path(sys.argv[2]).write_text(json.dumps({ 'model': 'claude-opus-5-5', 'availableModels': ['claude-opus-5-5'], 'enforceAvailableModels': True, 'fallbackModel': [], @@ -287,9 +288,9 @@ desktop_catalog="$NEUROAPI_AGENTS_STATE_ROOT/config/codex-desktop-models.json" grep -Fq 'model_provider = "neuroapi_agents"' "$desktop_config" 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" +grep -Fq 'model = "gpt-6-luna"' "$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"]; assert cfg["web_search"]=="live"; assert cfg["features"]=={"plugins":True,"remote_plugin":False,"apps":False}' "$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"]=="gpt-6-luna"; assert [m["slug"] for m in catalog["models"]]==["gpt-6-sol","gpt-6-luna"]; 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 @@ -381,9 +382,17 @@ if PATH="$MOCK_CLIENT_BIN:$PATH" /bin/bash "$REPO_ROOT/scripts/macos/install.sh" fi [[ "$(<"$TMP_ROOT/malformed-desktop.toml")" == 'broken = [' ]] unlink "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" +# A catalog without Sol must keep its validated Luna default as well. +"$PYTHON_BIN" - "$NEUROAPI_TEST_CODEX_CATALOG" <<'PYCODE' +import json, pathlib, sys +path = pathlib.Path(sys.argv[1]) +catalog = json.loads(path.read_text()) +catalog['models'] = [m for m in catalog['models'] if m['slug'] == 'gpt-6-luna'] +path.write_text(json.dumps(catalog)) +PYCODE PATH="$MOCK_CLIENT_BIN:$PATH" /bin/bash "$REPO_ROOT/scripts/macos/install.sh" >/dev/null 2>"$TMP_ROOT/install.err" [[ -f "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" ]] -grep -Fq 'model = "gpt-6-sol"' "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" +grep -Fq 'model = "gpt-6-luna"' "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" /bin/bash "$REPO_ROOT/scripts/macos/uninstall.sh" >/dev/null [[ ! -e "$NEUROAPI_AGENTS_DESKTOP_CODEX_HOME/config.toml" ]] unset NEUROAPI_AGENTS_TEST_DESKTOP_OPT_IN NEUROAPI_AGENTS_CURL_BIN diff --git a/tests/windows/desktop-config.ps1 b/tests/windows/desktop-config.ps1 index 7e3d4dc..9ae8d20 100644 --- a/tests/windows/desktop-config.ps1 +++ b/tests/windows/desktop-config.ps1 @@ -56,6 +56,50 @@ Assert-Desktop (([regex]::Matches($updated, '(?m)^model_provider\s*=')).Count -e Assert-Desktop (([regex]::Matches($updated, '(?m)^model\s*=')).Count -eq 1) 'Model root key was duplicated.' Assert-Desktop ($updated -match '(?s)^.*model_catalog_json = .*\[features\]') 'Catalog root key landed inside a table.' +# Execute the actual setup selection/rendering block without credentials, DPAPI or writes. +# Sol is deliberately listed first: neither availability nor order may override default_model. +$setupSource = Get-Content -LiteralPath (Join-Path $repoRoot 'scripts/windows/setup.ps1') -Raw +$selection = [regex]::Match($setupSource, '(?ms)^ (\$catalog = ConvertFrom-NeuroAPICatalog.*?)(?=^ \$catalogFile =)') +Assert-Desktop $selection.Success 'Desktop setup selection block was not found.' +$selectionScript = [scriptblock]::Create($selection.Groups[1].Value) +function New-DesktopCatalogModel { + param([string]$Id) + return @{ + slug = $Id; display_name = $Id; description = 'Test'; base_instructions = 'Test' + supported_in_api = $true; supports_reasoning_summary_parameter = $true; support_verbosity = $false + supports_parallel_tool_calls = $true; supports_search_tool = $true; use_responses_lite = $false + priority = 0; context_window = 128000; max_context_window = 128000; auto_compact_token_limit = 100000 + effective_context_window_percent = 95; input_token_limit = 128000; output_token_limit = 16000 + supported_reasoning_levels = @(); shell_type = 'shell_command'; visibility = 'list' + model_messages = @{ instructions_template = 'Test' }; truncation_policy = @{ mode = 'tokens'; limit = 10000 } + experimental_supported_tools = @(); input_modalities = @('text') + } +} +function Get-DesktopSetupConfig { + param([string]$Json) + $catalogJson = $Json + $desktopOriginal = $existing + $helperPath = $options.HelperPath + $secretPath = $options.SecretPath + $desktopCatalogPath = $options.CatalogPath + . $selectionScript + return $desktopContent +} +foreach ($case in @( + @{ models = @('gpt-6-sol', 'gpt-6-luna'); default = 'gpt-6-luna' }, + @{ models = @('gpt-6-sol'); default = 'gpt-6-sol' }, + @{ models = @('model-one'); default = 'model-one' } +)) { + $models = @($case.models | ForEach-Object { New-DesktopCatalogModel $_ }) + $json = @{ models = $models; default_model = $case.default } | ConvertTo-Json -Depth 8 + $selected = Get-DesktopSetupConfig -Json $json + Assert-Desktop ($selected -match ('(?m)^model = "' + [regex]::Escape($case.default) + '"\s*(?:#.*)?$')) 'Setup ignored the validated server default.' + Assert-Desktop ($selected -match '(?m)^plugins = true$' -and $selected -match '(?m)^multi_agent = true$') 'Setup changed unrelated feature preferences.' + Assert-Desktop ($selected -match '(?m)^url = "https://example.test/mcp"$') 'Setup changed user MCP settings.' +} +$missingDefault = @{ models = @((New-DesktopCatalogModel 'gpt-6-sol')); default_model = 'gpt-6-luna' } | ConvertTo-Json -Depth 8 +Assert-DesktopFailure { Get-DesktopSetupConfig -Json $missingDefault } + Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original "[model_providers.neuroapi_agents]`nname = 'mine'" @options } Assert-DesktopFailure { New-NeuroAPIDesktopConfig -Original '[model_providers."neuroapi_agents"]' @options } $literal = New-NeuroAPIDesktopConfig -Original "model = 'literal'" @options diff --git a/tests/windows/smoke.ps1 b/tests/windows/smoke.ps1 index ffa1e9d..bd4fe35 100644 --- a/tests/windows/smoke.ps1 +++ b/tests/windows/smoke.ps1 @@ -12,17 +12,21 @@ function Assert-True { } function New-DesktopTestCatalog { - param([string]$Model) - return @{ models = @(@{ - slug = $Model; display_name = 'Test model'; description = 'Description'; base_instructions = 'Instructions' - supported_in_api = $true; supports_reasoning_summary_parameter = $true; support_verbosity = $false - supports_parallel_tool_calls = $true; supports_search_tool = $false; use_responses_lite = $false - priority = 0; context_window = 128000; max_context_window = 128000; auto_compact_token_limit = 100000 - effective_context_window_percent = 95; input_token_limit = 128000; output_token_limit = 16000 - supported_reasoning_levels = @(@{ effort = 'medium'; description = 'Medium' }); shell_type = 'shell_command'; visibility = 'list' - model_messages = @{ instructions_template = 'Instructions' }; truncation_policy = @{ mode = 'tokens'; limit = 10000 } - experimental_supported_tools = @(); input_modalities = @('text') - }); default_model = $Model } | ConvertTo-Json -Depth 8 -Compress + param([string]$Model, [string[]]$AdditionalModels = @()) + $models = @() + foreach ($id in (@($Model) + @($AdditionalModels))) { + $models += @{ + slug = $id; display_name = 'Test model'; description = 'Description'; base_instructions = 'Instructions' + supported_in_api = $true; supports_reasoning_summary_parameter = $true; support_verbosity = $false + supports_parallel_tool_calls = $true; supports_search_tool = $false; use_responses_lite = $false + priority = 0; context_window = 128000; max_context_window = 128000; auto_compact_token_limit = 100000 + effective_context_window_percent = 95; input_token_limit = 128000; output_token_limit = 16000 + supported_reasoning_levels = @(@{ effort = 'medium'; description = 'Medium' }); shell_type = 'shell_command'; visibility = 'list' + model_messages = @{ instructions_template = 'Instructions' }; truncation_policy = @{ mode = 'tokens'; limit = 10000 } + experimental_supported_tools = @(); input_modalities = @('text') + } + } + return @{ models = $models; default_model = $Model } | ConvertTo-Json -Depth 8 -Compress } $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) @@ -170,7 +174,7 @@ try { Assert-True ($rotatedKey -ceq 'rotated-test-token') 'Successful retry did not install the replacement key.' $env:NEUROAPI_AGENTS_TEST_TOKEN = $null - $desktopCatalog = New-DesktopTestCatalog 'gpt-6-sol' + $desktopCatalog = New-DesktopTestCatalog 'gpt-6-luna' -AdditionalModels @('gpt-6-sol') & "$repoRoot\scripts\windows\setup.ps1" ` -TestMode -StateRoot $stateRoot -CodexHome $codexHome -NoPathUpdate ` -EnableCodexDesktop -DesktopCatalogJson $desktopCatalog | Out-Null @@ -181,7 +185,7 @@ try { Assert-True (Test-Path -LiteralPath $desktopStatePath) 'Desktop ownership metadata is missing.' Assert-True (Test-Path -LiteralPath $desktopCatalogPath) 'Desktop model catalog is missing.' $desktopBefore = [IO.File]::ReadAllText($desktopConfigPath) - Assert-True ($desktopBefore -match '(?m)^model = "gpt-6-sol"$') 'Entitled default model was not selected.' + Assert-True ($desktopBefore -match '(?m)^model = "gpt-6-luna"$') 'Listed Sol overrode the validated server default Luna.' Assert-True ($desktopBefore -match 'https://codex.neuroapi.host/v1') 'Desktop URL is incorrect.' Assert-True ($desktopBefore -notmatch 'test-neuroapi-token') 'Desktop config contains a plaintext key.' & "$repoRoot\scripts\windows\setup.ps1" `