Files
OmniRoute/docs/frameworks/AGENTBRIDGE.md
Diego Rodrigues de Sa e Souza a7ae9550bd Release v3.8.36 (#4854)
* chore(release): open v3.8.36 development cycle

* refactor(chatCore): extrai resolveCompressionSettings (#3501) (#4826)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 1/13)

* refactor(chatCore): extrai predicados puros de combo de compressão (#3501) (#4824)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 2/13)

* refactor(chatCore): extrai emitOutputStyleTelemetry (#3501) (#4811)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 3/13)

* refactor(chatCore): extrai writeCompressionAnalytics (bloco analytics completo, #3501) (#4817)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 4/13)

* refactor(chatCore): extrai runPluginOnRequestHook (#3501) (#4827)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 5/13)

* refactor(chatCore): extrai applyClientUsageBuffer (buffer/estimate de usage non-streaming, #3501) (#4832)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 6/13)

* refactor(chatCore): extrai buildPostCallGuardrailContext (contexto guardrail post-call, #3501) (#4831)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 7/13)

* refactor(chatCore): extrai storeSemanticCacheResponse (cache-store non-streaming, #3501) (#4828)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 8/13)

* refactor(chatCore): extrai buildNonStreamingResponseHeaders (headers de resposta non-streaming, #3501) (#4835)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 9/13)

* refactor(chatCore): extrai maybeConvertJsonBodyToSse (#3089 JSON→SSE streaming, #3501) (#4833)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 10/13)

* refactor(chatCore): extrai assembleStreamingResponseHeaders (headers de resposta streaming, #3501) (#4836)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 11/13)

* refactor(chatCore): extrai storeStreamingSemanticCacheResponse (cache-store streaming, #3501) (#4829)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 12/13)

* refactor(chatCore): extrai assembleStreamingPipeline (chain de transforms streaming, #3501) (#4837)

Integrated into release/v3.8.36 (#3501 chatCore extraction stack 13/13)

* ci(quality): shift heavy validations to the PR→release fast-path (release-acceleration) (#4857)

* feat(quality): add check:test-runner-api gate (vitest-only dirs must use vitest API)

* feat(release): reusable CHANGELOG i18n-mirror sync script

* chore(ops): add prune-stale-worktrees.sh (dry-run by default)

* ci(quality): run test-runner-api + docs-all + vitest + full unit suite on PR->release fast-path

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(quota): cota exclusiva lista qtSd/ no /v1/models (#4806) + limite EPSILON não bloqueia (#4830)

Integrated into release/v3.8.36 — quota-exclusive qtSd/ listing (#4806) + EPSILON placeholder no longer blocks; rebuilt from stale base (3 defining commits cherry-picked clean over release tip)

* feat(sse): add Google Flow video-generation provider (#4569) (#4769)

Integrated into release/v3.8.36 — Google Flow video-generation provider (#4569), release-green validated (typecheck + 21 tests + file-size)

* fix(api): auth on compression run-telemetry + document OMNIROUTE_EVAL_CREDENTIALS (#4694, #4720) (#4796)

Integrated into release/v3.8.36 — auth on compression run-telemetry + OMNIROUTE_EVAL_CREDENTIALS doc, release-green validated (typecheck + 3 tests + env-doc-sync)

* fix(translator): strip top-level client_metadata on the OpenAI passthrough (port from 9router#1157) (#4624)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): normalize `developer` role to `system` for OpenAI-format providers (#4625)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): emit </think> close marker for Anthropic thinking blocks (#4633)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(translator): normalize tools to Anthropic-native shape for non-Anthropic providers (#4650)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(gemini): preserve `pattern` in antigravity tool schema sanitizer (#4651)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(perplexity): validate API keys via /v1/models endpoint (#4654)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(image): prevent compatible nodes from shadowing provider aliases (#4656)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(cli-tools): tolerate JSONC (comments, trailing commas) in tool settings (#4659)

Integrated into release/v3.8.36 — port (rebuilt from stale base; defining commit cherry-picked clean over release tip, release-green validated)

* fix(security): validate kiro region to prevent SSRF (GHSA-6mwv-4mrm-5p3m) (#4629)

Integrated into release/v3.8.36 — kiro region SSRF guard (GHSA-6mwv-4mrm-5p3m), port rebuilt clean over release tip

* fix(cli): harden the systray2 tray runtime (port of 9router#1080) (#4628)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* fix(test): validate anthropic-compatible connections via POST /v1/messages (#4657)

Integrated into release/v3.8.36 — anthropic-compat validation via POST /v1/messages (port 584cf66a), rebuilt clean + baseline; release-green

* fix(executors): strip params unsupported by the target provider/model (#4658)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* fix(claude-oauth): respect 429 backoff on usage endpoint to reduce spam (#4655)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* feat(api/v1): include alias-backed models in /v1/models listing (#4630)

Integrated into release/v3.8.36 — port rebuilt clean over release tip, release-green validated

* chore(quality): rebaseline catalog.ts 1574->1577 (#4630 aliases sobre quota-exclusive da release) (#4879)

rebaseline

* feat(compression): Kiro/CodeWhisperer tool-result compression engine (#4635)

Integrated into release/v3.8.36 — port rebuilt clean, release-green

* fix(security): don't trust loopback socket as local when behind reverse proxy (#4632)

Integrated into release/v3.8.36 — port rebuilt clean, release-green

* fix(opencode): preserve DeepSeek reasoning content in streamed responses (#4631)

Integrated into release/v3.8.36 — DeepSeek reasoning_content injection (port #1099); release-green

* fix(copilot,antigravity): cap maxOutputTokens at 16384 to stop "Invalid Argument" 400 (#4636)

Integrated into release/v3.8.36 — cap maxOutputTokens 16384 antigravity (port #779); release-green

* fix(dashboard): show custom vision models in LLM selector (#4653)

Integrated into release/v3.8.36 — custom vision models in LLM selector (port 5e5e78d3); release-green

* fix(claude): omit adaptive thinking + output_config.effort for haiku (#4661)

Integrated into release/v3.8.36 — haiku adaptive-thinking omit (port); release-green

* feat(provider): CodeBuddy CN (copilot.tencent.com) — full stack (#4664)

Integrated into release/v3.8.36 — CodeBuddy CN provider (port efd20be8); usage.ts import + public-creds allowlist line reconciled; release-green

* feat(combo): Fusion strategy — parallel panel + judge synthesis (16th strategy) (#4652)

Integrated into release/v3.8.36 — Fusion combo strategy (16th, port 87e5c1c6); combo.ts baseline reconciled; release-green

* feat(proxy-pool): Deno Deploy relays + group action buttons (#4643)

Integrated into release/v3.8.36 — Deno Deploy relays (port #1437); proxies.ts baseline reconciled + env docs restored; release-green

* fix(security): pin image fetch DNS resolution to prevent SSRF rebinding (GHSA-cmhj-wh2f-9cgx) (#4634)

Integrated into release/v3.8.36 — pin DNS for image fetch SSRF rebinding guard (GHSA-cmhj-wh2f-9cgx, port c7d07448); caller DNS stubs + test-file baseline reconciled; release-green

* fix(github): route Copilot Codex models to /responses (port from 9router#102) (#4626)

Integrated into release/v3.8.36 — route Copilot Codex models to /responses (port #102); release-green

* fix(copilot): never route Gemini/Claude variants to /responses (chat-completions only) (#4627)

Integrated into release/v3.8.36 — never route Gemini/Claude to /responses (port #1536); fused with #4626 codex routing via supportsResponsesEndpoint gate; release-green

* docs(ops): add canonical incident response runbook (#4868)

Integrated into release/v3.8.36

* docs(perf): add per-endpoint p50/p95/p99 latency + cost budgets (#4867)

Integrated into release/v3.8.36

* fix(proxy): fan out direct dispatcher streams (#4803)

Integrated into release/v3.8.36

* fix(antigravity): exclude standard Gemini rate limit message from quota exhaustion keywords (#4810)

Integrated into release/v3.8.36

* fix(sse): skip third-party tool-name cloak for Anthropic server tools (#4808)

Integrated into release/v3.8.36

* fix(install): make transformers optional for CUDA-host installs (#4807)

Integrated into release/v3.8.36

* fix(combo): propagate selected connection ID to fallback error responses for correct model lockout (#4809)

Integrated into release/v3.8.36

* fix db storage tuning settings (#4834)

Integrated into release/v3.8.36

* fix(sse): drop ccp pin when pinned provider is durably unhealthy (failover + anti-flap) (#4864)

Integrated into release/v3.8.36

* fix(claude): skip mcp__ tool-name cloak + guard missing connectionId (#4861)

Integrated into release/v3.8.36

* chore(quality): reconcile env-doc + file-size base-reds in release/v3.8.36 (#4886)

- env-doc-sync: document PIN_DROP_BACKOFF_LEVEL / PIN_DROP_GRACE_MS (added by the
  ccp-pin health gate #4864) in .env.example + ENVIRONMENT.md.
- file-size: rebaseline image-generation-handler.test.ts 1996 -> 2019 to its actual
  size (pre-existing drift).

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(codex): drop non-standard codex.* events that break responses.stream (env-gated, #4602) (#4715)

Integrated into release/v3.8.36

* feat(routing): honor X-Route-Model header to override body.model (#4863)

Integrated into release/v3.8.36

* feat(live-ws): allow non-loopback clients via LIVE_WS_ALLOWED_HOSTS (closes #4873) (#4877)

Integrated into release/v3.8.36 (live-ws + combo-api commits; Tailscale CGNAT commit held pending opt-in/opt-out decision)

* chore(claude,codex): bump pinned CLI identity — Claude 2.1.158→2.1.187, Codex 0.132.0→0.142.0 (#4883)

Integrated into release/v3.8.36

* fix(security): SSRF allowlist bypass via x-relay-path nos relays Deno/Vercel (#4899)

Integrated into release/v3.8.36

* feat(quota): recuperação proativa de conexões em cooldown (cron heal) [Fase 3 #8] (#4900)

Integrated into release/v3.8.36

* fix(quota): policy inválida não vaza allow + guard connectionIds vazio [Fase 3 #10] (#4901)

Integrated into release/v3.8.36

* feat(quota): saturação real do Claude no fair-share via /api/oauth/usage (#4885)

Integrated into release/v3.8.36

* chore(dashboard): rename Qoder display label from "Qoder AI" to "Qoder" (#4733)

Integrated into release/v3.8.36

* fix(ci): include coverage/lcov.info in coverage-report artifact for SonarQube (#4670)

Integrated into release/v3.8.36

* fix(cli): bump better-sqlite3 runtime pin to 12.10.1 for Node 26 (#4685)

Integrated into release/v3.8.36

* docs: clarify Kiro is ~50 credits/month per account, not unlimited (#4690)

Integrated into release/v3.8.36

* docs(agentbridge): document Electron NODE_EXTRA_CA_CERTS, real model IDs, identity caveat (#4718)

Integrated into release/v3.8.36

* docs(ops): document the release-green family (green-prs, check:release-green, babysit, nightly) (#4679)

Integrated into release/v3.8.36

* fix(translator): replay reasoning_content on plain Xiaomi MiMo turns (port from 9router#1321) (#4639)

Integrated into release/v3.8.36

* feat(opencode-go): advertise glm-5.2 and kimi-k2.7-code (align with official Go endpoints) (#4711)

Integrated into release/v3.8.36

* feat(db): track API endpoint dimension on usage_history (#4676)

Integrated into release/v3.8.36 (migration renumbered 103→105; endpoint plumbed through extracted usage-stats helpers)

* fix(cli): SIGKILL systray child PID before IPC close to avoid macOS NSStatusItem orphan (#4732)

Integrated into release/v3.8.36

* feat(proxy-pool): Cloudflare Workers proxy deployer + pool integration (#4640)

Integrated into release/v3.8.36 (relay type added to RELAY_TYPES set; dropdown UX preserved + Cloudflare item added; proxies.ts file-size rebaselined 1057→1060)

* chore(quality): conserta base-red de release/v3.8.36 (gates + 7 testes + build MDX) (#4915)

A base tinha base-red sistêmica herdada de PRs de outras sessões, bloqueando
TODOS os PRs do ciclo (o TIA roda a suíte full em fail-safe p/ diffs hub).

4 Fast Quality Gates:
- test-discovery (#4877): live-server-allowlist.test.ts em tests/unit/server/
  (não-coletado) + vitest → nunca rodava. Convertido p/ node:test em tests/unit/security/.
- any-budget:t11 (#4664): 3 explicit-any em tokenRefresh.ts tipados (sem crescer file-size).
- docs-symbols (#4868): rotas inexistentes → /api/system/version e
  PUT /api/providers/{id} {isActive:false}.
- docs-all fabricated-claim (#4868 + #4718): 5 bin/*.sh reais criados (rollback,
  snapshot-data, restore-data, restore-policies, cold-start-bench) + _ops-common.sh
  (snapshot VACUUM INTO, guards de confirmação/TTY, testes de contrato); NODE_EXTRA_CA_CERTS
  (env de runtime Node) na allowlist do checker.

7 testes unit base-red (de features alheias à quota):
- oauth-providers-config (#4664): teste alinhado ao provider codebuddy-cn do registry.
- antigravity-model-aliases (#4636): maxOutputTokens esperado 32769→16384 (cap intencional).
- provider-request-capture #4091 (#4861): exemplo do teste trocado de mcp__ (que #4861
  isenta de cloak por causa dos 400s de assimetria de histórico) para um tool de terceiro
  cloakável — preserva o invariante de #4091 SEM reverter #4861.
- combo-error-response: convertido de vitest p/ node:test (era coletado pelo glob node:test
  e crashava); api/** e server/** removidos do vitest.config (config morta).

Build MDX (dast-smoke, #4679):
- docs/ops/RELEASE_GREEN.md não tinha frontmatter `title` → fumadocs-mdx rejeitava no
  webpack compile ("invalid frontmatter: title expected string"), quebrando o next build
  (e o deploy). Frontmatter title adicionado (único doc do collection sem ele).

17/17 Fast Quality Gates + suíte unit completa (17737 testes, 0 fail) + vitest verdes localmente.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): saturação proativa por headers de tokens (universal) [Fase 3 #2] (#4907)

storeRateLimitHeaders só capturava os headers de REQUESTS (RPM/min), que não
refletem a pressão de TOKENS. Agora também parseia os headers de tokens (em toda
resposta, sucesso também) para throttle proativo antes do 429:
- Anthropic: anthropic-ratelimit-tokens-{limit,remaining,reset} (+ input/output), RFC3339.
- OpenAI: x-ratelimit-{limit,remaining,reset}-tokens, reset em duração (6m0s).

saturation = 1 − remaining/limit; resetAt normalizado a epoch (parse de duração
ReDoS-safe). getTokenHeaderSaturation por (provider, connectionId). fetchGeneric-
Saturation passa a usar esse sinal (complementa o oauth/usage do #1, que segue
primário p/ Claude). Fail-open, cache mantido, request-path inalterado.

16 testes novos + regressão (oauth/usage #1 8/8, signals 6/6) = 30/30;
typecheck:core + eslint limpos.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): estratégia de combo "headroom" — seleção por folga de cota [Fase 3 #4] (#4908)

Nova estratégia de roteamento que escolhe a conexão com MAIS folga de plano:
headroom = 1 − max(util_5h, util_7d) (técnica do dario), via getSaturation
(melhorado p/ Claude no #1). Proativo em vez de só fill-first reativo.

- Helper PURO headroomRanking.ts (computeHeadroom + rankByHeadroom; saturação
  injetada, não-mutante, tie-break estável, fail-open).
- Orderer async em combo/quotaStrategies.ts (reusa a maquinaria reset-aware de
  expansão de conexões + concorrência limitada; seam injetável).
- Registrada como "headroom" em routingStrategies (combo-only); fill-first segue
  default — nenhuma estratégia existente tocada.
- baseline file-size combo.ts 3168->3180 (só +12L de dispatch; lógica fora do god-file).

16 testes novos + combo-strategies 15/15 = 31/31; typecheck:core + eslint +
file-size limpos.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): cap per-(key,model) — quota_allocation_model_caps [Fase 3 #7] (#4927)

* feat(quota): cap per-(key,model) com tabela quota_allocation_model_caps [Fase 3 #7]

Fecha o buraco onde uma API key pode drenar o pool inteiro consumindo um único modelo.

Tabela nova: quota_allocation_model_caps(pool_id, api_key_id, model, cap_value, cap_unit)
PK composta (pool_id, api_key_id, model). cap_unit alinhado ao QuotaUnit existente.

Comportamento: keyA acima do cap para modelo M → bloqueada somente em M; ainda
permitida em qualquer outro modelo no mesmo pool. Cap <= EPSILON → ignorado (seed).

Consumo por-(key,model) usa bucket segregado no quota_consumption existente
(poolId mangled ':model:<model>') com window fixa 'hourly'; nenhuma nova tabela
ou método de store necessário.

Módulo novo: src/lib/db/quotaModelCaps.ts (getModelCap/setModelCap/deleteModelCap/listModelCaps)
enforce.ts ganha o pre-check em enforceQuotaShare + recording em recordConsumption.
EnforceInput e RecordConsumptionInput ganham model?: string (backward-compatible).
localDb.ts re-exporta os 4 helpers (Hard Rule #2).

TDD: tests/unit/quota-per-key-model.test.ts — 4 cenários (bloqueia em M, permite em M2,
sem cap → sem bloqueio, EPSILON → ignorado). Todos os gates de qualidade passam.

* feat(quota): plumba model resolvido no hot path para ativar o per-(key,model) cap [Fase 3 #7]

A tabela/enforce do commit anterior estavam INERTES: o hot path não passava `model`
ao enforce nem ao record, então nenhum model-cap disparava em produção.

Plumbagem (model resolvido = mesma var usada no log/roteamento, pós background-redirect/alias):
- chatCore.ts: enforceQuotaShare ganha `model`; scheduleQuotaShareConsumption recebe `model`.
- chatCore/quotaShareConsumption.ts: threade `model` no RecordConsumptionInput (non-streaming).
- spendRecorder.ts: recordStreamingConsumption já recebia `model` — agora o coloca no
  RecordConsumptionInput (streaming accrue por-modelo).
- embeddings.ts: enforce + record ganham `model`.

Namespace do cap = id do modelo RESOLVIDO (o mesmo de modelForScope/pendingScope/getUnsupportedParams),
não o requestedModel cru nem o finalModelToUpstream (sem prefixo de provider). Operador configura
o cap contra esse id. `model || undefined` em todos os pontos: vazio/null → check pulado (fail-safe,
zero latência — só um campo no objeto).

Teste de integração novo (tests/unit/quota-per-key-model-hotpath.test.ts): prova end-to-end que
N consumos via scheduleQuotaShareConsumption({model}) → enforceQuotaShare({model}) bloqueia, e que
outro modelo no mesmo pool ainda passa; + guard de que enforce SEM model nunca dispara model-cap.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5] (#4929)

* feat(quota): session stickiness p/ integridade de prompt-cache [Fase 3 #5]

Adiciona stickiness de sessão ao roteamento de combo: uma conversa multi-turno
é roteada para a MESMA conexão enquanto ela permanecer saudável, evitando a
perda do prompt-cache do provider (custo 5-10× sem stickiness, efeito conhecido
no dario/clewdr).

Implementação:
- `open-sse/services/combo/sessionStickiness.ts` (novo, <800 linhas):
  mapa em memória (messageHash → connectionId) com TTL 15 min + cap 500 entradas;
  `applySessionStickiness` promove a conexão sticky ao índice 0 dos targets
  ordenados pelo strategy, guardado por `computeHeadroom > 0.15` (threshold);
  quando saturada (headroom ≤ 0.15), o binding é limpo e a seleção normal reage.
  Hash da sessão = SHA-256 dos primeiros chars da 1ª mensagem user → 16 hex chars.
  Seam de teste: `__setStickinessHeadroomFetcherForTests`.
- `open-sse/services/combo.ts`: import + 2 pontos de integração (pré-eval-scores
  e pós-success), dentro do orçamento congelado de 3180 linhas.
- `tests/unit/combo-session-stickiness.test.ts`: 19 testes node:test + assert/strict,
  todos via injeção de fetcher (zero rede/DB).

Threshold 0.15: conexão a >85% de utilização está a um burst de rate-limit;
o benefício de cache não compensa manter-se numa conexão degradada. Valor
alinhado com a zona de soft-penalty do restante do engine de quota-share.

* test(combo): isola combo-strategies da session stickiness (#5)

selectedConnectionFor reusa o mesmo body, então o sticky map (#5) fixava a
connection após a 1ª chamada e quebrava o round-robin tie-break do teste
reset-aware. Limpa o sticky map no início da helper — a stickiness tem suíte
própria (combo-session-stickiness). Sem enfraquecer asserts.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): buckets multi-janela por conexão (5h/7d/per-model) [Fase 3 #3] (#4928)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* refactor(providers): decompõe catálogo providers.ts em módulos de dados (godfile sweep, #3501) (#4917)

Integrado em release/v3.8.36 (godfile sweep providers.ts, #3501)

* refactor(pricing): decompõe pricing.ts em shared-tiers + DEFAULT_PRICING particionado (godfile sweep, #3501) (#4918)

Integrado em release/v3.8.36 (godfile sweep pricing.ts, #3501)

* refactor(api): extrai camada-folha pura de validation.ts (URL/headers/transport) (#4921)

Integrado em release/v3.8.36 (validation.ts split fatia 1 — leaf layer)

* refactor(api): extrai validators web-cookie + Meta AI de validation.ts (#4922)

Integrado em release/v3.8.36 (validation.ts split fatia 2 — web-cookie + Meta AI)

* refactor(api): extrai validators enterprise-cloud + probe compartilhado de validation.ts (#4923)

Integrado em release/v3.8.36 (validation.ts split fatia 3 — enterprise-cloud + probe)

* refactor(api): extrai validators áudio/speech + misc apikey de validation.ts (#4930)

Integrado em release/v3.8.36 (validation.ts split fatia 4 — áudio/speech + misc apikey)

* feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9] (#4939)

* feat(quota): estratégia dedicada de quota-share (DRR + P2C in-flight + gating per-model) [Fase 3 #9]

Estratégia interna "quota-share" isolada num módulo dedicado — NÃO toca a seleção/
fair-share genérica (decisão do dono: não mexer no que já funciona). Os combos qtSd/
(quotaCombos.ts) passam de fill-first para essa strategy; combo.ts ganha só 1 branch
de dispatch que delega 100% ao módulo (nenhum case existente alterado).

- quotaShareStrategy.ts: gating per-model (isBucketSaturated do #3) + DRR (quantum
  proporcional ao weight) + P2C sobre carga in-flight.
- quotaShareInflight.ts: contador in-flight com TTL/lease de 120s — fallback do
  decrement-on-abort sem precisar instrumentar o combo genérico.
- "quota-share" registrada como strategy INTERNA (não exposta na UI).
- testes de síntese (quota-combo-balancing, quota-multiprovider) alinhados: a strategy
  esperada dos combos qtSd/ passa de "fill-first" para "quota-share" (alinhamento ao novo
  comportamento intencional, não mascaramento — os 73 testes de qtSd/ seguem verdes).

* test(quota-share): alinha 2 scope-guards ao godfile sweep (base-reds que bloqueavam o CI)

Dois testes de "arquivo contém X" quebraram por decomposições de godfile que outras
sessões mergearam no release DURANTE a validação de #9 — NÃO são regressão de #9
(que não toca validation/oauth). Alinhados ao novo layout, asserts preservados:
- proxy-bypass-scope-guard #3226: bypassProxyPatch foi extraído de validation.ts para
  validation/headers.ts (split #4921–#4930) → o teste lê a camada de validação.
- sse-error-passthrough #3324: a windsurf authHint foi extraída de providers.ts para
  providers/oauth.ts → o teste lê o novo local.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* refactor(api): extrai validators search + embedding/rerank de validation.ts (#4932)

Integrated into release/v3.8.36

* refactor(api): extrai format-validators (OpenAI/Anthropic) de validation.ts (#4933)

Integrated into release/v3.8.36

* refactor(db): extrai model-permission matching de db/apiKeys.ts (#4936)

Integrated into release/v3.8.36

* refactor(db): extrai row-parsers + tipos compartilhados de db/apiKeys.ts (#4943)

Integrated into release/v3.8.36

* refactor(db): extrai column-mapping (snake↔camel) de db/core.ts (#4947)

Integrated into release/v3.8.36

* refactor(db): extrai schema-column reconciliation de db/core.ts (#4948)

Integrated into release/v3.8.36

* refactor(sse): extrai scalar/format helpers de services/usage.ts (#4949)

Integrated into release/v3.8.36

* refactor(sse): extrai quota-core (UsageQuota + builders) de services/usage.ts (#4950)

Integrated into release/v3.8.36

* fix(translator): regroup parallel tool results adjacent to their assistant (#4714) (#4882)

Integrated into release/v3.8.36 (fixes #4714)

* fix(qoder): exchange PAT for jt-* job token before Cosy chat (#4683) (#4884)

Integrated into release/v3.8.36 (fixes #4683)

* refactor(sse): dedup fallback tool_call id helper (#4736)

Integrated into release/v3.8.36

* refactor(open-sse): extract safeParseJSON util, dedup tryParseJSON (#4735)

Integrated into release/v3.8.36

* fix(compression): eliminate ReDoS in math_inline preservation pattern (#4795) (#4838)

Integrated into release/v3.8.36 (fixes #4795)

* fix(combo): fetch models dynamically from custom provider endpoints (#4860)

Integrated into release/v3.8.36

* feat(providers): update volcengine-ark model list with DeepSeek V4 (#4905)

Integrated into release/v3.8.36

* fix(translator): provider thinking compatibility (DeepSeek/Gemini) (#4946)

Integrated into release/v3.8.36

* feat(combo): task-aware routing strategy (#4945)

Integrated into release/v3.8.36

* refactor(sse): extrai a família MiniMax de services/usage.ts (#4952)

Integrated into release/v3.8.36

* refactor(sse): extrai a família GLM de services/usage.ts (#4953)

Integrated into release/v3.8.36

* refactor(sse): extrai a família Antigravity de services/usage.ts (#4956)

Integrated into release/v3.8.36

* fix(dashboard): show custom provider given-name instead of internal id across dashboard pages (#4603) (#4960)

Integrated into release/v3.8.36 (fixes #4603)

* fix(api): evict stale in-memory rate-limit windows to stop slow heap leak (#4041) (#4957)

Integrated into release/v3.8.36 (fixes #4041)

* fix(api): parse /v1/responses body once instead of 3-4x on the hot path (#4041) (#4958)

Integrated into release/v3.8.36 (fixes #4041)

* fix(translator): preserve legitimate empty-string tool arguments in openai-to-claude streaming (#4951) (#4959)

Integrated into release/v3.8.36 (fixes #4951)

* chore(quality): reconcile file-size baseline for #4960 provider-display-name (#4961)

Integrated into release/v3.8.36

* fix(dashboard): restore home provider-topology card hidden by #4596 default (#4963)

Integrated into release/v3.8.36 — restores home topology card (#4596 regression)

* fix(build): drop @omniroute/open-sse from optimizePackageImports (build OOM) (#4968)

Integrated into release/v3.8.36 — fixes build OOM (optimizePackageImports open-sse)

* fix(quota): migração 107 ativa estratégia quota-share nos combos qtSd/ existentes [Fase 3 #9] (#4962)

Integrated into release/v3.8.36

* feat(quota): respeita max_concurrent por conexão no roteamento (#4965)

Integrated into release/v3.8.36

* feat(quota): combo quota-share espera cooldown curto e re-despacha (Variante A) (#4967)

Integrated into release/v3.8.36

* fix(quality): resolve base-reds da release — db-rules allowlist + task-aware router precedence (#4973)

Dois base-reds pré-existentes que reprovavam o CI da release v3.8.36 (Fast
Quality Gates + Unit Tests fast-path), independentes de qualquer feature em voo:

1. check:db-rules / allowlist: os módulos db-internal caseMapping (#4947) e
   schemaColumns (#4948), extraídos de db/core.ts e importados só por ele, não
   estavam em INTENTIONALLY_INTERNAL. Registrados na allowlist (correção
   canônica — são internos legítimos, não re-exportados pelo localDb).

2. auto-strategy honra LKGP/cost (combo-routing-engine.test.ts, 2 testes): o
   task-aware reordering (#4945, reorderByTaskWeight) roda para strategy "auto" e
   era aplicado DEPOIS do router explícito (selectWithStrategy: lkgp/cost),
   sobrescrevendo o orderedTargets[0] que o operador escolheu. Instrumentação
   provou: post-filter [0]=claude (LKGP) → post-task [0]=gpt-oss. Correção: quando
   o auto usa router explícito, preserva o [0] dele e deixa o task-aware refinar
   só a cauda de fallback. gpt-oss-120b PERMANECE tool-capable (não é mudança de
   catálogo; o model-capabilities-registry test segue verde).

Validado: 121 testes (combo-routing-engine + combo-task-aware + registry) verdes,
red-check confirmado, db-rules/file-size/typecheck/lint/prettier OK.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(quota): serializa concorrência por conexão no caminho quota-share (FASE 2.1) (#4970)

O gating de quota-share em selectQuotaShareTarget é fail-open: uma conexão
at-cap só é despriorizada, nunca bloqueada. Com 1 conexão por conta de
assinatura (caso comum), chamadas concorrentes ainda floodam a conta (→ 429 +
cooldown) — provado live na .15: 3 chamadas concorrentes com max_concurrent=1
despacharam todas em 94ms.

Adiciona um semáforo POR CONEXÃO em torno do dispatch quota-share: chamadas
excedentes esperam na fila em vez de floodar (key qsconn:<connectionId>, cap =
max_concurrent da conexão). Fail-open em fila saturada/timeout para nunca
piorar disponibilidade. Gated por strategy===quota-share + kill-switch
resilienceSettings.quotaShareConcurrencyLimit (default on; UI no ResilienceTab).

Lógica extraível isolada no leaf puro combo/quotaShareConcurrency.ts
(unit-testado: estabilidade da key, no-op sem cap, serialização real,
fail-open). Settings + schema + UI espelham comboCooldownWait.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* docs(resilience): document Quota-Share Concurrency Control (max_concurrent + serialization + cooldown-wait) (#4980)

Documents the v3.8.36 quota-share concurrency layers in RESILIENCE_GUIDE.md:
per-connection max_concurrent cap, the quota-share request serialization semaphore
(FASE 2.1, qsconn:<connectionId>, fail-open, kill-switch), and the combo
cooldown-aware retry — so operators know how to cap a subscription account's
concurrency and why the routing gate alone cannot contain a single-connection flood.

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(dashboard): proxy-pool success gating, sync timestamp, opt-in Redis (#4878) (#4988)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): fail over on 400 responses carrying rate-limit text (#4976) (#4986)

* fix(sse): fail over on 400 responses carrying rate-limit text (#4976)

* chore(quality): rebaseline accountFallback.ts file-size for #4976 fix

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(compression): stop RTK over-truncating file-read tool results (#4559) (#4987)

* fix(compression): stop RTK over-truncating file-read tool results (#4559)

* chore(quality): trim #4559 comment to keep rtk/index.ts within size cap

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954) (#4989)

* fix(sse): honor per-account proxies and fingerprint rotation in opencode executor (#4954)

* chore(quality): rebaseline auth.ts file-size for #4954 (+39: synthetic no-auth providerSpecificData hydration of fingerprints/accountProxies; irreducible credential-path wiring, covered by opencode-proxy-rotation-4954.test.ts + 159 auth/noauth regression)

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540) (#4990)

* fix(sse): soft-penalize exhausted providers in auto-combo scoring (#4540)

* chore(quality): document STATUS_SOFT_DEPRIORITIZE_FACTOR + rebaseline combo.ts for #4540

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(dashboard): switch to visible filter after auto-hiding failed models in test-all (#4887) (#4991)

* fix(dashboard): switch to visible filter after auto-hiding failed models in OAuth provider test-all (#4887)

* test(dashboard): move #4887 test into tests/unit/ui so a CI runner collects it

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(pollinations): only enable jsonMode when JSON output is requested (#3981) (#5009)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003) (#5008)

* fix(antigravity): default safetySettings to all-OFF for parity with native Gemini paths (#5003)

* docs(changelog): restore #3981 pollinations entry eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665) (#5010)

* fix(chatgpt-web): map advertised gpt-5.5/5.4-pro/5.2-pro slugs to prevent silent model substitution (#4665)

MODEL_MAP was missing the advertised catalog ids gpt-5.5, gpt-5.5-pro,
gpt-5.4-pro and gpt-5.2-pro, so MODEL_MAP[model] ?? model sent the dot-form
id verbatim to the ChatGPT backend-api, which silently rejected it and served
the default Plus model. Map each to its dash-form slug. gpt-4-5 is already
dash-form and falls through correctly, so it is intentionally left unmapped.

Extends the executor MODEL_MAP test with the four ids and adds a drift guard
asserting every advertised dot-form catalog id reaches the backend in dash-form
(never verbatim), guarding future catalog<->map drift.

file-size: tests/unit/chatgpt-web.test.ts frozen baseline 2809->2855 (+46) for
the added test cases and drift-guard test; executor source unchanged in baseline.

* docs(changelog): restore #3981/#5003 entries eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* feat(combos): add editable per-combo description field persisted via /api/combos (#5005) (#5011)

* feat(combos): add editable per-combo description field persisted via /api/combos (#5005)

* docs(changelog): restore #3981/#5003/#4665 entries eaten by merge

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>

* Fix Ollama Cloud max reasoning effort (#4993)

Integrated into release/v3.8.36

* fix(copilot): replace execSync with execFile to prevent command injection (#5024)

Integrated into release/v3.8.36

* fix(plugin): auth.json dual-key fallback for auto-prefix migration (#5027)

Integrated into release/v3.8.36

* feat(endpoint): per-endpoint custom system prompt injection (#5022)

Integrated into release/v3.8.36

* fix(headroom): translate openai-responses input through OpenAI for compression (#5023)

Integrated into release/v3.8.36

* docs(changelog): add entries for #4993, #5024, #5027 (release notes credit)

* fix(api): stop /api/system/env/repair 500 on packaged install (#5006) (#5028)

* fix(api): stop /api/system/env/repair 500 on packaged install — lazy createRequire in sync-env.mjs (#5006)

scripts/dev/sync-env.mjs ran createRequire(import.meta.url) at module
top-level. When webpack bundles it into the standalone env-repair route,
import.meta.url is frozen to the build-machine path (file:///home/runner/...)
and createRequire throws during module evaluation, so the whole route
module fails to load and every GET returns HTTP 500 — breaking the
onboarding wizard on packaged/global installs.

- Move createRequire into the guarded better-sqlite3 block (only place
  that needs it); a bad import.meta.url now returns the safe default.
- resolveRootDir() falls back to process.cwd() when fileURLToPath throws.
- route.ts passes an explicit rootDir (process.cwd()) so the helper never
  derives the root from the frozen import.meta.url, matching the .env
  target used by createEnvBackup().
- Regression guard: assert sync-env.mjs has no top-level createRequire +
  getEnvSyncPlan(oauth) works with explicit rootDir without throwing.

* docs(changelog): restore #4993/#5023/#5024/#5027 + custom-system-prompt/headroom entries eaten by release merge

* chore(quality): rebaseline 3 inherited base-reds from release merge

Files NOT touched by this PR — grew on release/v3.8.36 via --admin merges and
inherited here through 'git merge origin/release':
- open-sse/executors/base.ts 1414->1416 (#4993 Ollama Cloud max-effort)
- src/lib/db/settings.ts 1149->1151 (#5023 custom system prompt)
- src/app/(dashboard)/.../endpoint/EndpointPageClient.tsx 2570->2612 (custom system prompt UI)

* chore(release): finalize v3.8.36 CHANGELOG + docs (2026-06-25)

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Makcim Ivanov <makcimbx@gmail.com>
Co-authored-by: Chewji <126886556+Chewji9875@users.noreply.github.com>
Co-authored-by: Anton <39598727+NomenAK@users.noreply.github.com>
Co-authored-by: Demiurge The Single <megamen932@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Éder Costa <eder.almeida.costa@gmail.com>
Co-authored-by: Jefferson Felizardo <jeffer1312@gmail.com>
Co-authored-by: Arthur Bodera <abodera@gmail.com>
Co-authored-by: Hamsa_M <116961508+hamsa0x7@users.noreply.github.com>
Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com>
2026-06-25 13:17:40 -03:00

27 KiB
Raw Blame History

title, version, lastUpdated
title version lastUpdated
AgentBridge 3.8.31 2026-06-20

AgentBridge

AgentBridge is OmniRoute's MITM (Man-in-the-Middle) proxy that intercepts HTTPS traffic from IDE AI agents and reroutes it through OmniRoute's unified routing engine. It supports 9 IDE agents — Antigravity, Kiro, GitHub Copilot, OpenAI Codex, Cursor, Zed, Claude Code, Open Code, and Trae (investigating) — making OmniRoute the broadest-coverage MITM proxy for AI coding assistants on the market.

Dashboard location: /dashboard/tools/agent-bridge Sidebar group: Tools (after Cloud Agents) See also: TRAFFIC_INSPECTOR.md — monitor all intercepted traffic in real-time; docs/security/MITM-TPROXY-DECRYPT.md — the Linux TPROXY transparent-decrypt capture mode driven by the /api/tools/agent-bridge/tproxy route.


§1 Overview

What is AgentBridge?

When an IDE agent (e.g., GitHub Copilot, Cursor, Claude Code) makes an API call, it connects directly to the upstream AI provider (OpenAI, Anthropic, etc.). AgentBridge intercepts that connection transparently at the TLS level — without requiring any agent configuration change — and rewrites the request through OmniRoute.

This means you can:

  • Reroute any agent to any provider: Copilot talking to OpenAI? Redirect it to Anthropic Claude, Gemini, or any of OmniRoute's 226+ providers.
  • Apply model mappings: gemini-3-flashclaude-sonnet-4.7 transparently at the handler level.
  • Observe all agent traffic: every intercepted request is published to the Traffic Inspector.
  • Apply OmniRoute resilience: combo routing, circuit breakers, fallbacks, and cost tracking work for IDE agent traffic too.

Positioning vs. the market

Feature 9router anti-api llm-interceptor OmniRoute AgentBridge
Antigravity
GitHub Copilot
Kiro (AWS)
OpenAI Codex
Cursor IDE
Zed Industries
Claude Code
Open Code
Trae 🔍 Investigating
Dashboard UI
Traffic Inspector
OmniRoute routing
Model mapping UI
Bypass list
Upstream CA cert

§2 Architecture

2.1 Components overview

IDE Agent (VS Code / Cursor / etc.)
    │  HTTPS (port 443)
    ▼
/etc/hosts — 127.0.0.1 api.githubcopilot.com   ← DNS redirect
    │
    ▼
src/mitm/server.cjs  (port 443, CJS child process)
    │  resolves target by Host header SNI
    │  generates per-SNI TLS cert signed by AgentBridge CA
    ├── Bypass list match? → TCP passthrough (no decrypt)
    ├── Target match? → fetch → OmniRoute router (port 20128)
    │       └── handler.intercept() — TypeScript
    │               ├── maskSecrets() on request body/headers
    │               ├── TrafficBuffer.push() — publishes to Traffic Inspector
    │               └── fetchRouter() → /v1/chat/completions
    └── No match? → TCP passthrough (no decrypt)

2.2 MITM server (src/mitm/server.cjs)

The core MITM server runs as a Node.js CJS child process (to avoid rewriting the existing CJS codebase). It:

  • Listens on port 443 (requires privilege or authbind/setcap)
  • Receives CONNECT tunnels from the OS (via /etc/hosts DNS redirect)
  • Generates per-SNI TLS certificates signed by the AgentBridge CA (DATA_DIR/mitm/ca.crt)
  • Resolves the target agent by Host header via targets/index.ts registry
  • Dispatches to the TypeScript handler layer via HTTP to http://127.0.0.1:20128

TARGET_HOSTS is loaded from DATA_DIR/mitm/targets.json (written by targets/index.ts at boot), allowing dynamic updates without restarting the CJS server.

2.3 Handler base (src/mitm/handlers/base.ts)

All agent handlers extend MitmHandlerBase:

export abstract class MitmHandlerBase {
  abstract readonly agentId: AgentId;

  abstract intercept(
    req: IncomingMessage,
    res: ServerResponse,
    body: Buffer,
    mappedModel: string,
  ): Promise<void>;

  // Protected helpers: fetchRouter, pipeSSE, hookBufferStart, hookBufferUpdate
}

Each handler calls hookBufferStart() before proxying and hookBufferUpdate() when complete. These push InterceptedRequest entries into globalTrafficBuffer (see Traffic Inspector §4).

2.4 Targets registry (src/mitm/targets/)

Each agent has a declarative target file:

// src/mitm/targets/copilot.ts
export const COPILOT_TARGET: MitmTarget = {
  id: "copilot",
  name: "GitHub Copilot",
  hosts: ["api.githubcopilot.com", "copilot-proxy.githubusercontent.com"],
  port: 443,
  endpointPatterns: ["/chat/completions", "/v1/chat/completions"],
  defaultModels: [
    { id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" },
  ],
  handler: () => import("../handlers/copilot"),
  riskNoticeKey: "providers.riskNotice.oauth",
};

The registry (targets/index.ts) exports ALL_TARGETS and emits DATA_DIR/mitm/targets.json on boot.

2.5 Passthrough and bypass list (src/mitm/passthrough.ts)

Bypass list (checked first, with precedence over target match):

  • Default patterns: banking hosts, .gov., OAuth/SSO providers (Okta, Auth0), etc.
  • User patterns: stored in DB table agent_bridge_bypass
  • Bypassed hosts receive a transparent TCP tunnel — TLS is never decrypted

Passthrough default (no target match and not in bypass):

  • Also receives a TCP tunnel — connections are never broken
  • Prevents the AgentBridge from disrupting general system HTTPS traffic

Routing precedence:

bypass list → target match → passthrough

2.6 Upstream CA cert (src/mitm/upstreamTrust.ts)

For corporate network environments with a custom CA:

AGENTBRIDGE_UPSTREAM_CA_CERT=/path/to/corporate-ca.pem

When set, configures undici's global dispatcher with the extra CA cert, allowing AgentBridge to reach upstream providers through corporate TLS termination proxies.

2.7 Secret masking (src/mitm/maskSecrets.ts)

Applied to all request bodies and headers before they enter the Traffic Inspector buffer or any log:

  • sk- / ak- / pk- prefixed tokens (OpenAI/Anthropic-style)
  • Authorization: Bearer <token> headers
  • Generic long tokens (≥40 chars)

§3 Setup

3.1 Start/stop the MITM server

Use the AgentBridge Server Card at /dashboard/tools/agent-bridge:

Action Description
Start Server Spawns src/mitm/server.cjs on port 443
Stop Server Gracefully shuts down the child process
Restart Server Stop + start (picks up target changes)
Trust Cert Installs DATA_DIR/mitm/ca.crt into OS trust store
Download Cert Downloads ca.crt for manual installation
Regenerate Cert Creates a new CA keypair (all existing per-agent certs are invalidated)

3.2 Trust the certificate

The AgentBridge CA certificate must be trusted by the OS before IDEs will accept the MITM connection.

Linux (NSS — Chrome/Firefox):

certutil -A -d sql:$HOME/.pki/nssdb -n "OmniRoute AgentBridge" -t CT,, -i ~/.omniroute/mitm/ca.crt

macOS (Keychain):

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ~/.omniroute/mitm/ca.crt

Windows (certmgr):

certutil -addstore -f Root $env:USERPROFILE\.omniroute\mitm\ca.crt

Or use the "Trust Cert" button in the dashboard (runs the appropriate command for your OS, with sudo prompt if needed).

Electron-based IDEs ignore the OS trust store (NODE_EXTRA_CA_CERTS)

Some IDEs — notably Antigravity IDE, and other Electron / VS Code-derived apps — bundle their own Node.js runtime that does not consult the OS trust store for outbound fetch/HTTPS. Trusting the CA at the OS/NSS level is enough for the IDE's native backend (e.g. a Go language server, which uses the OS CA bundle), but the Electron frontend will still fail TLS — it surfaces as the app being logged out or showing a "connection error" even though the MITM log shows the backend's bootstrap calls returning 200. Two steps are required, and both matter:

  1. Point the runtime at the CA explicitly:
    export NODE_EXTRA_CA_CERTS=/path/to/omniroute-agentbridge-ca.crt
    
  2. Launch the IDE from that shell. Starting it from the desktop icon / Dock / Start menu does not inherit shell exports, and ~/.config/environment.d/*.conf only applies after a fresh graphical login. Fully quit the IDE first — Electron's singleton lock means a second launch just focuses the existing process and the new environment is ignored.

The OS-trust + NSS step above remains necessary (the Chromium network stack used by some auth flows reads the per-user NSS store, and has its own static pins for *.googleapis.com that a locally-trusted CA overrides). NODE_EXTRA_CA_CERTS covers the Node fetch path on top of it.

3.3 DNS routing

For each agent you want to intercept, its API host(s) must resolve to 127.0.0.1. AgentBridge manages /etc/hosts entries automatically when you toggle DNS for an agent in the Setup Wizard.

Example /etc/hosts entries for GitHub Copilot:

127.0.0.1 api.githubcopilot.com
127.0.0.1 copilot-proxy.githubusercontent.com

3.4 Model mapping

Use the Model Mapping Table in each agent card to define source → target mappings:

Source model (agent native) Target model (OmniRoute)
gpt-4o claude-sonnet-4.7
* (wildcard) claude-haiku-4.7

Wildcard * maps any unrecognized model to the specified target. Persisted in agent_bridge_mappings table.

Tip — discover the agent's real model IDs. An IDE may send model names that differ from its UI labels and that change between major versions. For example Antigravity 2 sends gemini-3.1-pro-low, gemini-pro-agent, and gemini-3.1-flash-lite over the wire — not the gemini-2.5-pro shown in older docs. Send one chat with no matching mapping in place: the MITM logs the exact incoming model: and passes the request through. Map that literal value, then the next request is intercepted and routed to your target.

3.5 Risk notice

AgentBridge intercepts credentials (OAuth tokens, API keys) that the IDE uses to authenticate with upstream providers. These are masked before logging (see §2.7) but are visible to OmniRoute's MITM layer. First activation of each agent shows a dismissible risk notice modal.

3.6 Maintenance & Diagnostics

The dashboard exposes a Maintenance & Diagnostics card (AgentBridgeMaintenanceCard, in src/app/(dashboard)/dashboard/tools/agent-bridge/components/) that surfaces operational MITM routes which previously had no UI. Its subtitle: "Self-test the capture pipeline, undo leftover system state, and move your setup between machines." The card client helpers live in src/lib/inspector/agentBridgeMaintenanceApi.ts.

Button Route What it does
Diagnose GET /api/tools/agent-bridge/diagnose Runs the capture-pipeline self-test and shows a per-check report (✓/✗ + remediation hint).
Repair POST /api/tools/agent-bridge/repair Undoes orphaned MITM system state (DNS spoof entries, root CA, system proxy) left behind by a crash or SIGKILL. Idempotent — reports "Nothing to repair" when state is clean.
Remove CA DELETE /api/tools/agent-bridge/cert Untrusts and removes the MITM root CA from the OS trust store (explicit, idempotent). Shown only when the CA is currently trusted; requires an inline "Remove CA?" confirmation.
Export config GET /api/tools/agent-bridge/config Downloads the portable config JSON (see §3.7).
Import config POST /api/tools/agent-bridge/config Uploads a previously-exported config JSON (see §3.7).

Diagnostics checks (summarizeDiagnostics() in src/mitm/inspector/diagnostics.ts). The route runs the effectful probe for each and feeds the booleans into the pure summarizer; a single healthy verdict plus a per-failure hint is returned:

Check name What it verifies Hint on failure
server-running The MITM server process is active "The MITM server is not running. Start it from the AgentBridge tab."
server-reachable The MITM server accepts connections on its port (TCP probe) "The MITM server is not accepting connections on its port. Check that the port is free and that you have privileges to bind it."
cert-exists The MITM certificate has been generated on disk "No MITM certificate has been generated yet. Generate one from the AgentBridge tab."
cert-trusted The MITM root CA is in the OS trust store "The MITM root CA is not trusted by the OS store, so TLS interception will fail. Trust the certificate from the AgentBridge tab."
dns-configured Target hostnames are spoofed in /etc/hosts "Target hostnames are not spoofed in /etc/hosts, so traffic never reaches the proxy. Enable DNS for the agent(s) you want to capture."

Orphaned-state banner: when the page detects state left behind by a crash (DNS spoof / CA / system proxy), the card shows an amber banner — "A previous session left system state behind (DNS spoof, CA, or system proxy). Run Repair to clean it up." — and highlights the Repair button. Repair is the application-layer analogue of ProxyBridge's --cleanup flag (it delegates to repairMitm() in src/mitm/manager.ts).

The MITM root CA is kept installed across stop/start to avoid repeated sudo prompts (the same behavior as mitmproxy/Charles), so removing it is an explicit Remove CA action rather than something that happens automatically on stop.

3.7 Portable config import/export

AgentBridge can serialize the operator-tunable state into a versioned JSON blob so a setup can be replicated across machines. The serializer is src/lib/inspector/configPortability.ts (exportConfig() / importConfig()), validated by AgentBridgeConfigSchema.

The export includes exactly three pieces (built-in defaults are intentionally NOT exported, so importing never duplicates or fights them):

Field Source Notes
bypassPatterns user-defined bypass patterns (agent_bridge_bypass) default bank/gov/okta patterns are excluded
customHosts Traffic Inspector custom hosts (inspector_custom_hosts) each: { host, kind: "llm"|"app"|"custom", label? }
agentMappings per-agent model mappings (agent_bridge_mappings) { [agentId]: [{ source, target }] } for every agent that has mappings
// GET /api/tools/agent-bridge/config
{
  "version": 1,
  "bypassPatterns": ["*.internal.example.com"],
  "customHosts": [{ "host": "api.example.com", "kind": "llm", "label": null }],
  "agentMappings": { "copilot": [{ "source": "gpt-4o", "target": "claude-sonnet-4.7" }] }
}

Import behavior (POST /api/tools/agent-bridge/config): bypass patterns and per-agent mappings replace wholesale; custom hosts are added idempotently (INSERT OR IGNORE). The response reports how many of each were applied:

{ "ok": true, "bypassPatterns": 1, "customHosts": 1, "agents": 1 }

What is NOT in the config: server running state, cert paths, per-agent DNS state, upstream CA path, and TPROXY settings — those are host/runtime state, not portable preferences.


§4 Per-agent reference

# Agent Status Hosts intercepted Auth type
1 Antigravity Supported daily-cloudcode-pa.googleapis.com, cloudcode-pa.googleapis.com Firebase OAuth
2 Kiro (AWS) Supported prod.kiro.aws, dev.kiro.aws AWS SigV4
3 GitHub Copilot Supported api.githubcopilot.com, copilot-proxy.githubusercontent.com GitHub OAuth
4 OpenAI Codex Supported api.openai.com (Codex paths), chatgpt.com OpenAI key
5 Cursor IDE Supported api2.cursor.sh, api.cursor.sh Cursor OAuth
6 Zed Industries Supported api.zed.dev, llm.zed.dev Zed OAuth
7 Claude Code Supported api.anthropic.com (opt-in) Anthropic key
8 Open Code Supported openrouter.ai, api.openai.com (zen paths) API key
9 Trae 🔍 Investigating TBD — see §8 TBD

Setup wizard steps (per agent)

Each agent card has a 3-step setup wizard:

  1. Verify prerequisites — Server running? Cert trusted? IDE installed (auto-detected)?
  2. Enable DNS — Adds /etc/hosts entries (requires sudo). Shows exactly which lines will be added.
  3. Map models — Optional model mapping table. Wildcards accepted.

Agent detection

For agents 18, AgentBridge attempts to auto-detect IDE installation:

export async function detectAgent(agentId: AgentId): Promise<DetectionResult>
// Returns: { installed: boolean, version?: string, path?: string }

Detection uses OS-specific paths and binary checks (e.g., code --list-extensions | grep github.copilot for Copilot, ~/.config/antigravity/ for Antigravity).


§5 Security

Hard Rules applied

Rule Application
#12 sanitizeErrorMessage All handler errors are sanitized before response or buffer entry
#13 Shell env-passing /etc/hosts edits use env option — no string interpolation of paths
#15 + #17 isLocalOnlyPath() /api/tools/agent-bridge/ is LOCAL_ONLY + SPAWN_CAPABLE — loopback enforced before auth

Bypass list for sensitive hosts

The bypass list ensures that financial institutions, OAuth/SSO providers, and other sensitive hosts are never decrypted. Their TLS traffic passes through as a transparent TCP tunnel — OmniRoute never sees the plaintext.

Default bypass patterns include:

  • *.bank.*, *.gov.* (financial/government)
  • *.okta.com, *.auth0.com, *.microsoft.com (SSO/identity)
  • *.apple.com, *.icloud.com (Apple system services)

User-added bypass patterns are stored in agent_bridge_bypass table and take precedence over everything.

Secret masking

maskSecrets() from src/mitm/maskSecrets.ts is applied:

  • On every request body before TrafficBuffer.push()
  • On every header before logging or broadcasting

Patterns: sk-/ak-/pk- prefix tokens, Bearer tokens, and generic tokens ≥40 characters.

Upstream CA cert

When AGENTBRIDGE_UPSTREAM_CA_CERT is set, the file is read at startup. If the path exists but the file is unreadable, AgentBridge logs a clear error and refuses to start (prevents silent TLS failures in corporate environments).

Known limitations

  • Port 443 requires privilege: On Linux, AgentBridge needs setcap 'cap_net_bind_service=+ep' on the Node binary, or run via authbind. The Setup Wizard displays OS-specific instructions.
  • IDE restart required: After DNS redirect, the IDE must be restarted for the new host resolution to take effect.
  • Hardcoded OAuth tokens: Some agents (Kiro, Antigravity) store OAuth refresh tokens locally. These are transparent to AgentBridge — it sees the Bearer token in each request, which is masked before logging.
  • Electron frontends need NODE_EXTRA_CA_CERTS: IDEs whose frontend runs on a bundled Node/Electron runtime ignore the OS/NSS trust store and must be launched from a shell with NODE_EXTRA_CA_CERTS set (see §3.2). Symptom when missing: the IDE backend authenticates (MITM shows 200s) but the UI stays logged out.
  • Multiple installs of the same IDE are independent: a system install (e.g. /usr/share/antigravity/antigravity) and a user-local "Full" install (e.g. ~/AntigravityIDE_Full/antigravity-ide) are separate processes with their own runtimes — each must be relaunched with the CA injected. Identify which one is running by its binary path before relaunching.
  • Identity is set by the agent's system prompt, not the routed model: when you remap an agent's model to a different provider, the reply still claims the agent's native identity (e.g. Antigravity answers "I am powered by Gemini") because the IDE injects that into the system prompt. Confirm the real backend in call_logs / proxy_logs (provider, model, target_format), not by asking the model who it is.

§6 Troubleshooting

Port 443 conflict

If another process is already listening on port 443 (web server, VPN, etc.):

lsof -i :443          # find the process
sudo fuser -k 443/tcp  # force-kill (use with care)

Alternatively, configure a non-privileged port in AgentBridge settings and set up iptables / pf redirect rules.

Certificate not trusted

If the IDE shows TLS errors after starting AgentBridge:

  1. Verify the cert was installed: security find-certificate -c "OmniRoute AgentBridge" (macOS) or certutil -L -d sql:$HOME/.pki/nssdb (Linux/NSS)
  2. Some apps maintain their own trust store (Firefox, Chrome on Linux). Run "Trust Cert" again and check the NSS/Firefox-specific cert store.
  3. Restart the IDE after trusting — in-flight TLS sessions use the old trust state.

IDE logged out / "connection error" despite a trusted CA

Symptom: after redirecting DNS and trusting the CA, an Electron-based IDE (e.g. Antigravity) opens logged out or shows an authentication/connection error, yet the MITM log shows the bootstrap calls (loadCodeAssist, fetchAvailableModels, …) returning 200.

Cause: the IDE's bundled Node/Electron runtime ignores the OS trust store. The native backend (a Go language server) trusts the OS CA and authenticates, but the Electron frontend does not — so the UI believes it is offline.

Fix (both steps): export NODE_EXTRA_CA_CERTS=<ca.crt> and relaunch the IDE from that shell, not from the desktop icon. Fully quit the IDE first — Electron's singleton lock means a second launch just focuses the existing process and the new environment is ignored. See §3.2. This mirrors an open upstream report where a standalone agent works through a MITM but the IDE variant fails under the same setup.

DNS not propagated

Check that /etc/hosts was updated:

grep "omniroute\|127.0.0.1.*github\|127.0.0.1.*cursor" /etc/hosts

Flush DNS cache:

# macOS
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux (systemd-resolved)
sudo systemctl restart systemd-resolved
# Windows
ipconfig /flushdns

IDE not detected

Auto-detection uses common installation paths. If detection fails but the IDE is installed:

  • Check if the IDE binary is in a non-standard location
  • The Setup Wizard still works — detection failure just means the badge won't show the install path

Handler errors (upstream fetch fails)

If AgentBridge intercepts but all requests fail:

  1. Verify at least one provider is connected at /dashboard/providers
  2. Check OmniRoute server logs: APP_LOG_LEVEL=debug in .env
  3. Verify OMNIROUTE_BASE_URL points to the correct router endpoint (default: http://127.0.0.1:20128)

§7 API reference

All routes are LOCAL_ONLY (loopback-only, enforced before auth) and SPAWN_CAPABLE. See src/server/authz/routeGuard.ts.

Base path: /api/tools/agent-bridge/

Method Path Description
GET /api/tools/agent-bridge/state Global server state + per-agent detection/status
GET /api/tools/agent-bridge/agents List registered agents (id, name, hosts, viability, state)
GET /api/tools/agent-bridge/agents/{id} State of one agent (target config + detection + stored state)
PATCH /api/tools/agent-bridge/agents/{id} Update setup_completed for agent
GET /api/tools/agent-bridge/agents/{id}/detect Run detection probe for agent (installed, version?, path?)
POST /api/tools/agent-bridge/agents/{id}/dns Enable/disable DNS for agent ({enabled: boolean})
GET /api/tools/agent-bridge/agents/{id}/mappings Model mappings for agent
PUT /api/tools/agent-bridge/agents/{id}/mappings Replace model mappings
POST /api/tools/agent-bridge/server Start/stop/restart server (action: "start"|"stop"|"restart"|"trust-cert"|"regenerate-cert")
GET /api/tools/agent-bridge/cert Cert status (exists, trusted, path)
POST /api/tools/agent-bridge/cert Trust (install) the MITM root CA
DELETE /api/tools/agent-bridge/cert Untrust (remove) the MITM root CA — idempotent (see §3.6)
POST /api/tools/agent-bridge/cert/regenerate Regenerate the self-signed MITM cert
GET /api/tools/agent-bridge/cert/download Stream the PEM cert for download
GET /api/tools/agent-bridge/bypass List bypass patterns (default + user)
POST /api/tools/agent-bridge/bypass Replace user-defined bypass patterns wholesale
DELETE /api/tools/agent-bridge/bypass?pattern=... Remove a single user-defined bypass pattern
GET /api/tools/agent-bridge/diagnose Capture-pipeline self-test (see §3.6)
POST /api/tools/agent-bridge/repair Undo orphaned MITM system state (see §3.6)
GET /api/tools/agent-bridge/config Export portable config JSON (see §3.7)
POST /api/tools/agent-bridge/config Import portable config JSON (see §3.7)
GET /api/tools/agent-bridge/upstream-ca Get configured upstream CA path
POST /api/tools/agent-bridge/upstream-ca Validate + persist upstream CA path
POST /api/tools/agent-bridge/upstream-ca/test Validate-only (dry-run) an upstream CA path — does not persist
GET / POST / DELETE /api/tools/agent-bridge/tproxy TPROXY transparent-decrypt capture mode — see docs/security/MITM-TPROXY-DECRYPT.md

Full OpenAPI schemas: docs/openapi.yaml → tag AgentBridge.


§8 Roadmap

Trae investigation

Trae is a relatively new AI coding assistant. Before implementing a handler:

  1. Identify the binary/extension in VS Code / JetBrains marketplaces or as a standalone app
  2. Capture traffic with mitmproxy to discover API hosts and endpoint shapes
  3. Determine authentication mechanism
  4. Assess go/no-go based on TOS and API discoverability

Until investigation completes, the Trae card in the dashboard shows a "Investigating" badge with a "Report viability" link. The handler stub at src/mitm/handlers/trae.ts throws a structured Not yet implemented error.

Backlog agents (MITM required — no custom base URL support)

The following tools do not support custom base URLs in their current versions, making MITM the only interception path. Viability assessment is pending:

  • Windsurf (Codeium/Cognition)
  • Amp (Sourcegraph)
  • Amazon Q / Kiro CLI (AWS Bedrock — separate from Kiro IDE)
  • Cowork (Anthropic desktop)

Note: GitHub Copilot CLI ≥v1.0.19 supports COPILOT_PROVIDER_BASE_URL — use direct config instead of MITM for that tool.