Skip to content

v0.3.7

Latest

Choose a tag to compare

@github-actions github-actions released this 02 Oct 15:20
· 1 commit to main since this release

A maintenance release with two credential-isolation fixes at the top — both could send one account's API key to an endpoint chosen by another account's configuration.

A media request could leak one account's key to another provider's endpoint

media image generate --provider B sent account A's API key upstream.

generic_media_handler resolved the provider purely from the model string via get_model_info, and body["provider"] — which media image generate, media embed and media search all send, and which --provider documents — was read by nothing. select_media_connections then took the first active connection for the resolved provider, so with more than one account under a provider the operator named one and the server dialled another.

Two changes: an explicit provider now wins whenever the model string carries no / (the dashboard's media skills build provider/model strings, which stay unambiguous and resolve exactly as before), and a connection name is resolved to its provider id before dispatch, so naming an account selects that account instead of failing with UnsupportedProvider("account-b").

Embeddings ignored a connection's baseUrl — and the SSRF guard blocked self-hosted search

Found by standing up a mock upstream and driving the real media CLI against it; every one of these returned a clean error or an empty success while the request never left the machine.

  1. Embeddings dialled api.openai.com regardless of configuration. OpenAiCompatAdapter::build_url returned its compile-time self.endpoint, so a connection pointed at a self-hosted OpenAI-compatible gateway, a corporate proxy, or a mock was still called with the user's key attached. tts/openai.rs has always honoured baseUrl; the embedding adapters now do too, reattaching their own path suffix and tolerating a base that already carries it.

  2. The SSRF guard was applied to the operator's own baseUrl. resolve_base_url and handle_search_value both went through get_provider_setting, which prefers provider_options over provider_specific_data — so the admin's saved connection config was treated as untrusted client input and run through assert_public_url. searxng's documented default is http://localhost:8888/search, which that rejects as an internal host, making a self-hosted search provider impossible to configure at all. Both sites now check the source explicitly: provider_options stays SSRF-checked, provider_specific_data is trusted as its doc comment always claimed.

The same baseUrl gap existed on the chat path: DefaultExecutor::build_url only consulted provider_specific_data["baseUrl"] for openai-compatible / anthropic-compatible nodes, so a plain connection was always dialled at the provider's hardcoded origin. The override is applied at the end of the resolution chain — runtime_transport, region/template branches, and provider-specific URL builders carry path or host logic a bare base URL does not, so those still win.

CLI writes were invisible to a running server

provider add, pool create/delete, combo create/edit/delete/enable/disable, key add/enable/disable/rotate, the four *_apply commands and provider models alias set/unset all commit through Db::update, but a running server keeps its own ArcSwap AppDb and only re-reads on the paths that already call reload_snapshot() — auth, the OAuth device poll, the chat dispatcher. Every one of those commands reported success while GET /api/providers and the dashboard kept serving the pre-write list until a restart. This is the tracked P0 "server never reloads API keys/providers added via CLI while running".

Three pieces: a new POST /api/db/reload re-reads SQLite into the snapshot; reload_running_server(db) calls it after every local write (best-effort, so a down server never turns a committed write into a failed command) and is wired at all 25 db.update sites; and openproxy.endpoint is now written at the bind site in main.rs rather than only by server start. That last one mattered most — the bare openproxy / openproxy --port N path the README documents never wrote the sidecar, so read_endpoint returned None and every CLI command silently fell back to http://127.0.0.1:4623. On a non-default port that is a different process entirely: the write landed in the right database while the reload went somewhere else. Pools also no longer lose their name.

opencode-go had no catalog discovery

import-models answered {"imported": 0, "skipped": 22, "total": 22} forever, because opencode-go was missing from supports_models_discovery — only its same-upstream sibling opencode-zen was listed, so GET /api/providers/{id}/models returned an error and import-models had no fetch branch at all. The catalog drifted so far from the provider that the model picker could not offer anything the account could reach.

Both now use the /zen/v1/models endpoint the live upstream actually serves (verified 200 with a real key): discovery returns 62 models, import-models reports imported: 55, skipped: 22, total: 62, and the served catalog grows 38 → 88 ocg/* entries including the -free models and claude-opus-5.

A failed fetch was also being reported as a successful sync — the fallback answered {"imported": 0, "skipped": <existing>, "total": <existing>}, which reads as "I looked upstream and found nothing new" when the upstream was never reached.

Three dead CLI subcommands

  • pxpipe double-wrapped its payload. pxpipe {status,health,stats,logs} re-wrapped the server's already-flat response in a second {ok, data, error} envelope, so --robot produced .data.data.installed and no agent following the documented openproxy.v1.* contract could read a single field. All four also shared the bare openproxy.v1.pxpipe schema. .data is now the payload itself (matching quota.list / settings.get), and each subcommand carries its own <action> segment.
  • media combo list applied no filter, despite a comment claiming it was "filtered to media kinds" — it returned every LLM fallback chain alongside the media ones. media combo create posted --members as providers, a field CreateComboRequest does not deserialise, so the field was dropped and every media combo was created empty: a 201 with the requested name and no members. Now narrows to combos whose kind is a non-empty media modality, and sends models.
  • tool antigravity-mitm failed on every invocation with HTTP 415, and --alias added a 422 on top. Both handlers extract Json<...> bodies, so axum rejected the CLI's post_empty / delete_json before the handler ran; and the alias endpoint takes {tool, mappings} while the CLI sent a bare {"alias": name}. Verified end to end against a live server: enable --alias agtest5 starts the proxy and persists {"aliases":{"agtest5":"agtest5"}}, disable --alias agtest5 clears it.

tool revert claude left ANTHROPIC_MODEL pinned

tool apply claude --model X writes ANTHROPIC_MODEL into ~/.claude/settings.json, but the revert endpoint only stripped a fixed list of keys that did not include it. Revert reported success and cleared ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN while leaving the model in place, so Claude Code stayed routed at the OpenProxy model after the operator asked to be un-wired. The asymmetry is structural: save persists whatever env object the caller sends, so the reset list has to cover every key a caller can write. The other eight tools were checked for the same mismatch — cline/roo remove their own written keys, continue filters by apiBase, hermes/kilo strip their blocks, codex and cowork reset the whole file, droid retains by filter. Claude was the only one with a fixed list that fell behind its own write path.

Also

The Docker publish workflow is removed — the project does not need image builds or publishing.

28 files changed, 1636 insertions, 144 deletions. Regression tests added throughout: 5 in cli_m6 (pxpipe envelope + schemas), 3 in cli_m5 (media combo list/create), 279 lines in cli_writes_reach_running_server, 184 in media_provider_credential_isolation, and 52 in cli_tool_native_settings_api. Full suite green: 2732 lib tests and 107 integration tests across 14 suites.

Full Changelog: v0.3.6...v0.3.7