Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/manual-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
1 change: 1 addition & 0 deletions scripts/macos/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 2 additions & 5 deletions scripts/windows/setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions tests/macos/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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': [],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions tests/windows/desktop-config.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 17 additions & 13 deletions tests/windows/smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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" `
Expand Down
Loading