The Agent Bridge page seeded a well-shaped initialData default then replaced it
wholesale with the raw /api/tools/agent-bridge/state response. The route returns
{ server, agents } but the UI reads { serverState, agentStates, bypassPatterns,
mappings }, so serverState became undefined and AgentBridgeServerCard crashed on
serverState.running — surfaced as the full-page 'Internal Server Error' boundary
(client render error, not a real 5xx).
Add a shared normalizeAgentBridgeState() that maps the route shape into the page
contract (server.running/certExists -> serverState) and always returns safe
defaults (never undefined serverState). Wired into both the SSR loader (page.tsx)
and the polling hook. The legacy 'agents' entry shape differs from AgentStateEntry
so it is not coerced; full route<->page contract reconciliation (port, upstreamCa,
bypassPatterns, mappings, agentStates) is a follow-up.
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
A /v1/responses request against the built-in codex/ provider does an
openai-responses -> openai-responses passthrough (CodexExecutor.transformRequest
returns the body early for _nativeCodexPassthrough). It forwarded client-only
fields verbatim and the Codex upstream rejected them with 400 Unsupported
parameter: prompt_cache_retention / safety_identifier / user — breaking Factory
Droid (which injects all three). The chat-completions path already strips these
(base.ts #1884, openai-responses translator #2770) but the passthrough skips
translation. Strip the three fields in the shared block before the passthrough
return; user is removed unconditionally since Codex /responses always rejects it.
Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com>
Finalize the v3.8.13 changelog with release date, maintenance notes,
and contributor credits. Update MCP docs to reference the correct tool
inventory diagram, exclude nested .claude worktrees from ESLint scans,
and tighten a response sanitizer type guard.
#3303 added the Gemini `-high`/`-low` budget tiers to ANTIGRAVITY_PUBLIC_MODELS
(user-callable on the Antigravity OAuth backend, verified via #3184), but did
not update the catalog-route test that asserted `antigravity/gemini-3.1-pro-high`
must NOT be exposed. The assertion now reflects the intended behavior — the
client-visible budget alias IS surfaced — while keeping the legacy
`gemini-claude-*` alias keys unexposed. Caught running the full catalog suite
on the merged release HEAD (the #3303 round only ran the antigravity-aliases
and usage-hardening files).
On the cached-token path the executor never enters the refresh branch, so the
same upstream Response was read with .text() twice (token-rejection check +
final body). A Response body is single-use, so the second read threw
'Body is unusable: Body has already been read', caught and surfaced as [502].
Read the body once into finalBody and only re-read after a token-rejection
refetch.
Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com>
omniroute update always failed on a global install:
- getCurrentVersion() read package.json from process.cwd(), which on a global
npm/brew install is the user's working dir, not the package root → null →
'Could not determine current version'.
- createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to
copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'.
Both now resolve package.json/bin relative to the script via import.meta.url,
and the backup uses cpSync({recursive:true}) so the cli/ directory is copied.
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) never
create a DB connection row so stats.total stays 0, which the configured-only
filter treated as 'unconfigured' and hid them — even though they are always
usable and appear unconditionally in /v1/models. filterConfiguredProviderEntries
now treats displayAuthType === 'no-auth' as configured.
Co-authored-by: uniQta <uniQta@users.noreply.github.com>
Integrated into release/v3.8.12. Salvaged the emitHookBlocking payload-chaining fix from the now-closed plugins-v4 branch (#3221) and adapted it to the shipped release hooks.ts: each blocking handler now sees the body/metadata as mutated by previous handlers. TDD regression test included (RED before, GREEN after); existing plugins-hooks suites green (19+5), typecheck + lint clean.
Integrated into release/v3.8.12. CodeQL hardening on the Chipotle executor: Math.random → crypto.randomInt/randomUUID, and a strict URL hostname check in the test. Fixed the node:crypto import (crypto.randomInt is not on the Web Crypto global → would crash at WS-connect) and added a regression guard exercising both helpers.
A working Qoder PAT was reported as "expired". The validator probes the Cosy endpoint
(api1.qoder.sh) — which IS the correct PAT path (the executor falls back to it after the
expected 401 from api.qoder.com). The bug was the verdict: isCosyAppError (added by #2860)
marked ANY Cosy 500 with "success":false as an auth failure, including a generic
{..."msgCode":500,"message":"Internal Server Error"} server fault — contradicting the
older #1391 "5xx = valid bypass" rule.
Narrow it: a Cosy 500 only marks the PAT invalid when the body carries an EXPLICIT auth
signal (unauthorized/forbidden/expired/token invalid/...); a generic Internal Server Error
falls back to valid-bypass. #2860's protection for genuine auth rejections is preserved.
Regression test: tests/unit/qoder-cli.test.ts — the two pre-existing generic-500 cases now
assert valid:true (they encoded the #3247 bug) + a new explicit-auth-signal case asserts
valid:false. 13/13 green.
Follow-up to the #3269 private-webhook opt-in. With the opt-in on, the private-host
check was bypassed entirely, leaving cloud-metadata endpoints (169.254.169.254,
metadata.google.internal, 100.100.100.200, link-local 169.254.0.0/16) reachable — the
classic SSRF -> IAM-credential pivot — and the webhook test endpoint returned the
upstream body, making it a content-exfiltration primitive against internal services.
- outboundUrlGuard: add isCloudMetadataHost(); parseAndValidateWebhookUrl blocks those
hosts UNCONDITIONALLY, even when private targets are opted in.
- webhooks/[id]/test: redact responseBody for private targets (status + latency only).
Regression test: tests/unit/webhook-metadata-guard-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/opt-in suites stay green (34/34).
Webhooks hardcoded parseAndValidatePublicUrl, which blocks any RFC1918/loopback host —
breaking self-hosted setups that legitimately point webhooks at internal services
(n8n, Home Assistant, a LAN box). Provider URLs already had an opt-in
(OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS); webhooks now reuse it.
- outboundUrlGuard: add parseAndValidateWebhookUrl — gates the private-host check on
arePrivateProviderUrlsAllowed() (default OFF); protocol + embedded-credential checks
stay unconditional.
- swap all webhook call sites (create/update/test/validate-url + dispatcher x2) to it.
Regression test: tests/unit/webhook-private-optin-3269.test.ts (RED before, GREEN after);
existing webhook SSRF/dispatcher suites stay green (33/33).
A custom OpenAI-compatible image-edit provider received an empty `model`. In production
`globalThis.fetch` is patched with node_modules/undici's fetch, whose `FormData` class
differs from `globalThis.FormData`; passing a native FormData made undici serialize it as
the string "[object FormData]" (text/plain), dropping every field including `model`.
handleOpenAIImageEdit now assembles the multipart body as a Buffer with an explicit
boundary + Content-Type, which every fetch impl accepts verbatim.
Regression test: tests/unit/image-edits-multipart-3273.test.ts reproduces the exact prod
condition (routes through undici's fetch) — RED before (upstream got text/plain
[object FormData]), GREEN after. Existing image suites stay green (50/50).
Regression of #764. Claude Code → Groq (llama-3.3-70b-versatile) returned HTTP 400
because the model was treated as reasoning-capable: supportsReasoning() defaulted to true,
so applyThinkingBudget did not strip reasoning params, and the claude→openai translator
forwarded reasoning_effort (and re-injected it from output_config.effort) — which Groq
rejects on non-reasoning models.
- providerRegistry: mark llama-3.3-70b-versatile + llama-4-scout supportsReasoning:false
(gpt-oss / qwen3-32b keep reasoning — they accept reasoning_effort).
- stripThinkingConfig: also strip output_config.effort so the translator can't re-inject
reasoning_effort downstream.
Regression test: tests/unit/thinking-budget-groq-3258.test.ts (RED before, GREEN after);
existing thinking-budget suites stay green (45/45).
ds-web/deepseek-v4-pro emits tool calls wrapped as
<tool_call name="skill">{"name":"customize-opencode"}</tool_call> instead of the
canonical <tool>{json}</tool>. webTools.ts only matched <tool>...</tool>, so the block
was silently dropped (and when arguments were present, the surrounding tag leaked into
content). Add TOOL_CALL_TAG_RE to capture the JSON body — the real tool name comes from
the body, never the tag's name= attribute — and extend the early-exit + range stripping.
Regression test: tests/unit/web-tools-translation-3260.test.ts (RED before, GREEN after).
Existing web-tools suites stay green (26/26).
CodeQL flagged 7 high alerts in code the cycle touched (the large release-PR diff
re-surfaces them). Resolved at the source — no dismissals:
- fix(images): resolveImageBaseUrl trimmed trailing slashes with `/\/+$/`, a
polynomial-ReDoS pattern (js/polynomial-redos) on the configured node base URL.
Replace it with a non-backtracking endsWith/slice loop.
- test(oauth): pin the Anthropic OAuth host with exact-equality asserts and a
parsed-hostname negative check instead of substring `.includes()`
(js/incomplete-url-substring-sanitization). The exact-equality assertions were
already present, so coverage is unchanged.
- test(images): drop the redundant `!includes("generativelanguage.googleapis.com")`
assert — the exact-equality assert on the resolved URL already guarantees it.
Finalize the 3.8.11 cycle CHANGELOG and clear the failures the full test:unit
gate surfaced (release-branch drift — PR merges bypassed pre-push):
- CHANGELOG: date the [3.8.11] section (2026-06-05) + repo-housekeeping roll-up
- docs(env): document THEOLDLLM_NAV_TIMEOUT_MS in .env.example + ENVIRONMENT.md
(env-doc-sync gate; #3217 added the var without docs)
- test(nvidia): exercise the #3226 bypass-fetch path via a local HTTP server and
hoist the validator import (patching globalThis.fetch no longer intercepts the
un-patched native fetch captured by proxyFetch)
- test(i18n): import the shipped normalizeComplianceEventTypes helper (#3185)
- test(model-caps): save synced metadata under the canonical gemini-3.1-pro key
now that #3229 aliases gemini-3.1-pro-high/-low to it
- test(web-session): expect the grok-web "sso + sso-rw" credential hint (#3180)
- test(synced-models): isolate DATA_DIR so the #3199 hidden-override stops
bleeding into the shared DB and breaking the re-run precondition
agy's gemini-3.1-pro-high/-low had no alias, so resolveAntigravityModelId sent the
speculative -high/-low suffix verbatim to upstream, which rejects it (400) for
gemini-3.x. Worse, the non-stream executor branch fed the 4xx response into the SSE
collector, returning a synthetic empty {object:chat.completion} envelope that masked
the error. Alias both to gemini-3.1-pro, and surface real upstream errors via
buildErrorBody for non-ok non-stream responses. + unit tests.
The Codex CLI WS->HTTP fallback rewrite (resolveResponsesApiModel) prefixes a
bare model id with codex/ whenever codex/<id> resolves to codex. Codex accepts
arbitrary model strings, so a combo name with no slash (e.g. n8n-text,
paid-premium) was rewritten to codex/<combo> and sent to Codex instead of being
resolved as a combo — regressing combos via /v1/responses in v3.8.9+. Skip the
rewrite when the bare id is a combo (getComboByName). + unit test.