docs(wiki): auto-sync pages + cover counts with docs

github-actions[bot]
2026-07-02 13:47:30 +00:00
parent 0349e4e142
commit d7da8cdb3a
4 changed files with 392 additions and 3 deletions

136
Ban-Detection.md Normal file

@@ -0,0 +1,136 @@
> 🌍 [View in other languages](Languages)
# Account-Ban / Banned-Keyword Detection
OmniRoute scans upstream error responses for signals that indicate a provider
**account is permanently dead** (suspended / deactivated / ToS-banned) and, when
matched, moves that connection into a **terminal `banned` state** so it is no
longer selected for requests. This is what the **Security → Banned Keywords**
settings card configures ("Additional keywords that trigger permanent account
ban detection. Built-in keywords always apply.").
This page documents the built-in list, the detection flow, its scope, how to add
custom keywords safely, and how to recover a flagged connection. The terminal
state itself is part of the resilience model — see
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md) ("Terminal states").
**Source of truth:** `open-sse/services/accountFallback.ts`
(`ACCOUNT_DEACTIVATED_SIGNALS`, `getMergedBannedSignals()`, `isAccountDeactivated()`).
## Built-in keywords
These 8 substrings always apply (case-insensitive), regardless of any custom list:
```
account_deactivated
account has been deactivated
account has been disabled
your account has been suspended
this account is deactivated
verify your account to continue (Antigravity / Google Cloud Code)
this service has been disabled in this account for violation (Antigravity)
this service has been disabled in this account (Antigravity)
```
> This list evolves as providers change their ban wording. The authoritative
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
> treat the block above as a snapshot.
Two adjacent, **separate** signal tables live in the same file and are *not* part
of banned-keyword detection:
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
`credit_balance_too_low`, `payment required`, …) → terminal `credits_exhausted`.
- `OAUTH_INVALID_TOKEN_SIGNALS`**non-terminal**; a token refresh can recover.
Note: common transient phrases like **`rate limit`** / `429` are handled by the
rate-limit / connection-cooldown path and are **not** ban signals.
## Detection flow
```
upstream error response
→ body stringified + lowercased
→ isAccountDeactivated(body): getMergedBannedSignals().some(sig => body.includes(sig)) [substring match]
→ match?
→ connection testStatus = "banned" (permanent — 1-year cooldown, never auto-recovers)
→ if setting `autoDisableBannedAccounts` is on → also isActive = false
→ connection is skipped during account selection (combo QUOTA_BLOCKING statuses)
```
- The match is a **case-insensitive substring** search on the response **body**
(`isAccountDeactivated`, `accountFallback.ts`).
- The permanent `banned` terminalization fires on a banned-signal body at **any
HTTP status** (via `markAccountUnavailable``checkFallbackError`). The
narrower **`deactivated`** label (`isActive=false` when the connection has no
spare API keys) is written by the inline `chatCore.ts` path on **HTTP 401 / 403**
(classified via `classifyProviderError``ACCOUNT_DEACTIVATED`). Note the
`markAccountUnavailable()` path writes a *different* terminal status —
**`expired`** — for the same `ACCOUNT_DEACTIVATED` signal (via
`resolveTerminalConnectionStatus`), so the same ban can surface as either
`deactivated` or `expired` depending on which path handled the response. (The
older code comment says "when a 401 body contains these strings" — that
understates the current behavior.)
- A `banned` connection is excluded from selection everywhere terminal statuses
are filtered (`isTerminalConnectionStatus`, combo `QUOTA_BLOCKING_CONNECTION_STATUSES`).
## Scope — which providers are scanned
**All providers.** The check runs in the generic error-handling pipeline that
every failed upstream request flows through — it is **not** gated to
OAuth/subscription scrapers. The resulting terminal state is per **connection**,
not per provider.
That said, the built-in *strings* are oriented toward subscription/OAuth
providers with real ban risk (ChatGPT Web, Claude Web, Codex, Muse Spark,
Antigravity). An API-key provider will only trip the detector if its error body
literally contains one of the substrings.
## Custom banned keywords
Add or remove keywords in **Security → Banned Keywords** (persisted as the global
`customBannedSignals` setting via `PATCH /api/settings`). They are **added to**
the built-in list — never a replacement — and hot-reload on save (and at startup)
via `setCustomBannedSignals()`. Each keyword is capped at 200 characters; there is
no array-length limit.
**⚠ False-positive risk — choose specific phrases.** Detection is a raw substring
match on the whole response body, and a match is **permanent** (1-year cooldown,
manual recovery). A broad keyword can ban a perfectly healthy connection:
- **Bad:** `quota`, `limit`, `error`, `denied` — appear in many transient errors.
- **Good:** full ban sentences, e.g. `your account has been suspended for`,
`account permanently banned`, `violation of our terms`.
Prefer the longest unambiguous phrase the provider returns on a real ban. When in
doubt, watch the connection's `lastError` first, then add the exact wording.
## Recovering a flagged connection
Terminal `banned` / `deactivated` states **never auto-recover** (they are excluded
from the proactive-recovery tick — only `unavailable` cooldowns recover on their
own). An operator must clear them explicitly:
1. **Re-test the connection** — the dashboard **Test** action
(`POST /api/providers/{id}/test`); a successful probe resets `testStatus` to
`active` and clears the error fields.
2. **Re-authenticate / edit credentials** — for OAuth providers, re-run the login
/ refresh flow; provider create/import routes set `isActive = true`.
3. **Re-enable the connection** — if `autoDisableBannedAccounts` set
`isActive = false`, toggle it back on after fixing the account.
There is no separate "clear ban flag" button — recovery is re-test, re-auth, or
re-enable, matching the general terminal-state rule in
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md).
## Source files
| Concern | File |
| --- | --- |
| Signal tables + match | `open-sse/services/accountFallback.ts` |
| Terminalization / persistence | `src/sse/services/auth.ts` (`markAccountUnavailable`, `resolveTerminalConnectionStatus`, `clearAccountError`) |
| Inline classification | `open-sse/handlers/chatCore.ts`, `open-sse/services/errorClassifier.ts` |
| Terminal-state recovery exclusion | `src/lib/quota/connectionRecovery.ts` |
| Custom-keyword runtime load | `src/lib/config/runtimeSettings.ts` (`setCustomBannedSignals`) |
| Settings UI | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |

@@ -1,6 +1,6 @@
# 🚀 OmniRoute — The Free AI Gateway
**Never stop coding. Connect every AI tool to 236 providers — 50+ free — through one endpoint.**
**Never stop coding. Connect every AI tool to 237 providers — 50+ free — through one endpoint.**
Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.
@@ -12,7 +12,7 @@ Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude /
| Feature | Description |
|---------|-------------|
| **236 AI Providers** | OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, and many more |
| **237 AI Providers** | OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, and many more |
| **50+ Free Tiers** | OAuth + free-tier providers for zero-cost AI access |
| **17 Routing Strategies** | Priority, weighted, round-robin, cost-optimized, context-relay, and more |
| **Token Compression** | RTK + Caveman engines save 1595% tokens automatically |
@@ -42,7 +42,7 @@ Use the **sidebar** to navigate through all documentation sections:
- **[Features](Features)** — All features explained
- **[Architecture](Architecture)** — System design and internals
- **[API Reference](API-Reference)** — REST API documentation
- **[Providers](Provider-Reference)** — All 236 supported providers
- **[Providers](Provider-Reference)** — All 237 supported providers
- **[Combos & Routing](Auto-Combo)** — Routing strategies
- **[Compression](Compression-Guide)** — Token compression pipeline
- **[MCP Server](MCP-Server)** — MCP tools and transports

107
Maturity-Reeval.md Normal file

@@ -0,0 +1,107 @@
> 🌍 [View in other languages](Languages)
# Reavaliação de Maturidade — pós-Ondas 03 (Quality-Gate v2)
> **O que é este documento.** Uma re-medição da maturidade do sistema de quality-gates
> **após** as Ondas 03 do programa Quality-Gate v2, comparada ao baseline registrado em
> [`QUALITY_GATE_PLAYBOOK.md`](./QUALITY_GATE_PLAYBOOK.md) (2026-06-16). Mede o que mudou,
> contra DSOMM L5 / OpenSSF Scorecard 9 / SLSA L3, separando o que é **CI-mensurável**
> (já entregue / entregável por código) do que é **processo/owner** (settings de organização).
>
> **Data:** 2026-06-30. Gerado do estado real do repositório, não da memória.
> **Régua:** OWASP DSOMM · OpenSSF Scorecard · SLSA · SonarQube "Clean as You Code".
---
## 1. Veredito atualizado
**Nota geral: A → A ("Avançado", topo ~5%).** As **duas maiores fraquezas estruturais**
do baseline 06-16 — o *buraco fast-gates* e o *mutation-score-não-catraca* — foram **fechadas**.
Os gaps residuais para "máximo absoluto" são quase todos **owner/infra-gated** (branch-protection,
SLSA L3, CodeQL advanced); o lado-código do programa está essencialmente completo.
| Framework de referência | Baseline 06-16 | Agora 06-30 | Movimento | Evidência |
| --- | --- | --- | --- | --- |
| **OWASP DSOMM** (5 níveis) | L3→L4 | **L4** em *Test Intensity* e *Static Depth*; L3 sólido nas demais | ▲ | mutation-ratchet bloqueante + suíte determinística no gate-de-merge |
| **OpenSSF Scorecard** | ~78/10 | ~78/10 (inalterado — gate é o **owner**) | = | falta Branch-Protection na `main` (setting do dono) + pin de actions |
| **SLSA** | L2→L3 | **L2** (encostando em L3) | = | falta builder hermético/reprodutível (infra/owner) |
| **SonarQube "Clean as You Code"** | Alinhado c/ ressalva | Alinhado c/ ressalva | = | ressalva de *sprawl* (~46+ gates) permanece — review de ROI pendente |
| **Quality-Ratchet pattern** | Exemplar | **Exemplar+** | ▲ | novo `dedicatedGate` de `mutationScore` (direction up) |
| **Mutation testing** | "Quase lá" (não-catraca) | **Catraca ativa** | ▲▲ | `check-mutation-ratchet.mjs` + baseline semeado + job nightly bloqueante |
---
## 2. Deltas desde 2026-06-16 (o que as Ondas 03 entregaram)
### 2.1 🔴→✅ Buraco fast-gates FECHADO (era a fraqueza estrutural #1)
O baseline alertava: `quality.yml` (PR→`release/**`) rodava **só gates de filesystem** — sem
typecheck, testes ou build —, então regressões determinísticas só explodiam no PR→`main`.
**Hoje** `.github/workflows/quality.yml` roda, no job *Fast Quality Gates*: `typecheck:core`,
**testes unitários impactados (TIA) bloqueantes com fail-safe para a suíte completa**, o
fast-path do **vitest**, e shards de unit. O gate agora roda **onde o merge acontece** (shift-left),
exatamente o princípio transversal que o playbook prescreve.
### 2.2 🟠→✅ Mutation score virou CATRACA (era a fraqueza #3 / P0 #1)
O antídoto mais forte contra coverage-gaming estava **advisory**. **Hoje**:
- `scripts/check/check-mutation-ratchet.mjs` (advisory por default, `--ratchet` bloqueante, skip gracioso);
- `config/quality/quality-baseline.json` tem entradas `mutationScore.<módulo>` semeadas (`direction: up`, `dedicatedGate`);
- `.github/workflows/nightly-mutation.yml` tem o job **"Mutation score ratchet (blocking)"** que unifica os relatórios por-batch e ratcheteia os scores merged por-módulo.
Resultado: o score de mutação por-módulo **não pode regredir** — cobertura deixou de ser vanity-metric.
### 2.3 ✅ Quick-wins de gate (Fase 6A/7) entregues
- **a11y axe-core "fake-green" corrigido:** `@axe-core/playwright` em devDeps; `a11y.spec.ts` com skip condicional `REQUIRE_AXE`; job no `nightly-resilience.yml`.
- **complexity varre `bin/`+`electron`:** `check-complexity.mjs` inclui esses diretórios no `ESLINT_ARGS`.
- **tracked-artifacts no pre-commit + pre-push:** `.husky/pre-commit` + `pre-push` bloqueiam artefato rastreado por engano.
---
## 3. As 12 categorias — situação (delta-focada)
| # | Categoria | Situação 06-30 |
| --- | --- | --- |
| 1 | Estilo & formatação | ✅ inalterado (Prettier+ESLint lint-staged) |
| 2 | Tipos | ✅ **reforçado**`typecheck:core` agora também no gate PR→release |
| 3 | Testes (intensidade) | ✅ **reforçado** — mutation testing virou catraca; suíte determinística no gate-de-merge |
| 4 | Política de testes (anti-gaming) | ✅ inalterado (pr-test-policy/test-masking/pr-evidence) |
| 5 | Complexidade & saúde | ✅ **reforçado** — complexity varre bin/electron |
| 6 | Segurança estática (SAST+segredos) | 🟡 CodeQL default-setup (advanced = owner); semgrep cloud não-versionado |
| 7 | Supply-chain (deps) | ✅ inalterado (osv/audit/Trivy/Dependabot + allowlist) |
| 8 | Supply-chain (build/release) | 🟡 SLSA L2 (L3 = builder hermético, owner/infra) |
| 9 | Contratos & API | 🟡 oasdiff/osv advisory (candidatos a bloqueante-com-escopo, P1) |
| 10 | Docs & i18n (anti-rot) | ✅ **reforçado**`fabricated-docs --strict` bloqueante (verificado exit 0) |
| 11 | Anti-alucinação / consistência | ✅ inalterado (known-symbols/fetch-targets/docs-symbols/db-rules) |
| 12 | Resiliência & domínio | ✅ inalterado (chaos/heap/k6/promptfoo/garak nightly) |
---
## 4. Gap residual para "máximo absoluto"
### 4.1 CI-mensurável / entregável por código (backlog deste programa)
- **P1 — osv/oasdiff → bloqueante com escopo certo:** osv só `CRITICAL`+fixable (two-step como o Trivy); oasdiff bloqueia breaking-change de contrato.
- **P1 — `require-tighten` bloqueante (fim de ciclo):** trava ganhos de métrica (impede afrouxar baseline sem registrar).
- **P1/P2 — review de ROI / sprawl de gates:** consolidar micro-gates de doc-sync; medir timing por-gate no `ci-summary` (combate a fadiga — ressalva do SonarQube/DORA). Os merges ROI deferidos (complexity unificada; `/api` anti-alucinação unificada) entram aqui.
- **P2 — CodeQL config commitado + semgrep versionado:** mais controle/reprodutibilidade.
### 4.2 Processo / owner (CI não move — settings de organização)
- **Branch-protection na `main`** (sobe Scorecard, fecha o gap DSOMM). Ver [`BRANCH_PROTECTION_MAIN.md`](./BRANCH_PROTECTION_MAIN.md).
- **CodeQL Default → Advanced setup.**
- **SLSA L3** — builder hermético/reprodutível (gerador SLSA do GitHub). Stretch (diminishing returns).
### 4.3 Explicitamente fora-de-escopo
- **DSOMM L5** é majoritariamente **org-level / processo** (não CI-codificável).
- **SLSA L4** (bit-a-bit reprodutível) é stretch declarado.
---
## 5. Itens deferidos / removidos (housekeeping da cauda)
- **`semcheck.yaml` (camada LLM de drift semântico docs↔code) — REMOVIDO.** Estava **órfão**
(nenhum workflow/script o invocava) e com contagens stale nas regras. A cobertura determinística
já existe (`check:fabricated-docs --strict` + `check:docs-counts-sync` + `check:docs-symbols`),
e a ressalva de *gate sprawl* desaconselha adicionar um gate LLM advisory de custo recorrente.
Pode ser re-introduzido no futuro como job nightly opt-in se o drift semântico virar problema real.
- **`agent-lsp` scaffold — DEFERIDO / opt-in não-ativado.** Existe como menção em docs
(`docs/architecture/QUALITY_GATES.md`, CHANGELOG) mas **sem wiring** e sem `.mcp.json.example`
no repo. Permanece como scaffold opt-in documentado; não é um gate ativo nem um gap de maturidade.

146
Router-Backends.md Normal file

@@ -0,0 +1,146 @@
> 🌍 [View in other languages](Languages)
# Router Backends & Embedded Services — architecture contract (ADR)
> **Status:** Accepted · **Context:** [#5670](https://github.com/diegosouzapw/OmniRoute/issues/5670),
> [#5603](https://github.com/diegosouzapw/OmniRoute/issues/5603) · **Contract:** `domain/routing/routerBackends.ts`
> (typed registry — code lands with [#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868))
This ADR pins down how `ts` (native), `bifrost`, `cliproxy`, `9router`, and
VibeProxy-compatible engines relate to each other, so contributors stop
conflating two things that are architecturally distinct. It documents the typed
registry introduced by the router-backend-registry work as the single source of
truth for that model.
## The core distinction — two orthogonal axes
An engine's role is described by **two independent axes**, encoded together in the
registry's `RouterBackendDefinition`:
1. **Lifecycle** (`RouterBackendLifecycle`) — _how the engine runs_:
- `in-process` — runs inside the OmniRoute Node process (the native TS pipeline).
- `supervised` — a local child process OmniRoute installs/starts/stops/health-checks
via `ServiceSupervisor`, then consumes as a provider connection.
- `external` — an HTTP endpoint OmniRoute dispatches to but does **not** manage
(configured by an env base URL).
- `disabled` — registered but not selectable.
2. **Selection axis** (relay routing backend) — _whether the relay dispatches to it_:
`RelayRoutingBackend = "ts" | "bifrost" | "auto"` in
`src/app/api/v1/relay/chat/completions/routingBackend.ts`.
The mistake to avoid: treating "embedded service" and "routing backend" as one
list. They are not. A `supervised` engine (9router/cliproxy) is a **provider
connection consumed by the native pipeline**, not an alternate relay dispatch
backend. `bifrost` is the reverse — a relay dispatch backend that (historically)
was `external`-only.
## The registry — single source of truth
The `domain/routing/routerBackends.ts` contract (code lands with
[#5868](https://github.com/diegosouzapw/OmniRoute/pull/5868)) declares every engine once, with its
lifecycle, capabilities, service identity, default port, health config, and
telemetry support. Consumers look engines up via `getRouterBackend(id)`,
`listRouterBackends()`, and `listRouterBackendsByCapability(cap)` instead of
special-casing each sidecar.
| Backend | Lifecycle | Service (axis A) | Relay backend (axis B) | Health | Default port |
| ----------- | ------------ | ---------------- | ---------------------- | ------------- | ------------ |
| `ts` | `in-process` | — | `ts` (native) | — | — |
| `bifrost` | `external`¹ | —¹ | `bifrost` / `auto` | `/health` | — |
| `cliproxy` | `supervised` | `cliproxy` | — (provider) | `/v1/models` | 8317 |
| `9router` | `supervised` | `9router` | — (provider) | `/api/health` | 20130 |
| `vibeproxy` | `external` | — | — (provider adapter) | `/v1/models` | — |
¹ Bifrost's promotion to a `supervised` embedded service (installable/startable
from `/api/services/bifrost/`) is tracked in
[#5817](https://github.com/diegosouzapw/OmniRoute/pull/5817); until it merges,
Bifrost is `external`-only (reachable solely via `BIFROST_BASE_URL`).
`capabilities` (`chat`, `responses`, `streaming`, `tools`, `vision`,
`oauth-backed`, `dashboard-embed`, `model-sync`, `native-hot-path`) let callers
filter by what an engine can actually do rather than hard-coding per-id branches.
## Axis A — embedded services (supervised process side)
- **Registry of supervised processes:** `src/lib/services/bootstrap.ts` `SERVICES[]`
(today: `9router`, `cliproxy`).
- **Lifecycle owner:** `src/lib/services/ServiceSupervisor.ts``start()` spawns the
child, gates on `waitForHealthy()`, taps stdout/stderr into a ring buffer;
`stop()` SIGTERM→SIGKILL; all serialized under a lock.
- **State union** (`src/lib/services/types.ts`):
`not_installed | stopped | starting | running | stopping | error`, plus an
orthogonal `HealthState = healthy | unhealthy | unknown`.
- **Why a separate process (not an in-proc SDK)?** Process isolation is what makes
install/start/stop/health/logs independently controllable per sidecar and lets the
loopback spawn-guard apply. Modeling an in-proc adapter is future work — the
`native-hot-path` capability flag is where that would be expressed.
### Lifecycle route contract (`/api/services/<tool>/…`)
Status codes are **state/verb/path-specific by design** — this is the contract, not
inconsistency:
| Call | Condition | Status |
| ---------------------------- | ------------------------------- | ------------------------------------ |
| `POST .../start` | service `not_installed` | **409** (precondition) |
| `POST .../stop` | already stopped | **200** (idempotent no-op) |
| `GET .../status` | OK | **200** (`live ?? row ?? "unknown"`) |
| `POST .../start` | spawn failure | **503** (transient) |
| `GET .../status`, `.../stop` | uncaught error | **500** |
| `GET /api/services/<x>/logs` | unknown tool `<x>` | **404** `Service '<x>' not found` |
| `GET .../status?reveal=key` | missing `X-Reveal-Confirm: yes` | **403** (9router only) |
| **any** `/api/services/*` | caller not loopback/private-LAN | **403 LOCAL_ONLY** |
All error bodies are shaped by `createErrorResponse()`
`{ error: { message, type }, requestId }`, where `type` is derived from the status
(`500→server_error`, `404→not_found`, `409→conflict`, else `invalid_request`) and is
the machine-actionable discriminator. Messages are pre-sanitized
(`sanitizeErrorMessage()`, Hard Rule #12).
**The loopback guard** is the most common source of a `403`: `/api/services/` is in
`LOCAL_ONLY_API_PREFIXES` (`src/server/authz/routeGuard.ts`) and
`src/server/authz/policies/management.ts` rejects any non-loopback / non-private-LAN
caller **before auth**, because these routes spawn child processes (Hard Rules 15
and 17). Reaching them through a public tunnel is `403` by design.
## Axis B — relay routing backend (dispatch side)
Only the relay proxy path `/api/v1/relay/chat/completions` selects a dispatch
backend; the main `/api/v1/chat/completions` surface never consults
`routingBackend.ts`.
- **Selection** (`resolveRelayRoutingBackend`): a single global env toggle —
`OMNIROUTE_RELAY_BACKEND` / `RELAY_ROUTING_BACKEND` ∈ {`ts`, `bifrost`, `auto`}.
If unset, `auto` when Bifrost is configured+enabled, else `ts`.
- **Behavior:**
- `bifrost` (forced): Bifrost failure → hard `502`, no fallback.
- `auto`: try Bifrost, on failure/cooldown silently fall through to native.
- `ts` / post-fallback: the native `open-sse` translator/executor pipeline.
- **Cooldown:** per-`baseUrl` failure cooldown in `bifrostCooldown.ts`.
Selection is **all-or-nothing at the relay level today** — there is no per-provider
or per-request engine swap on `release/v3.8.43`. The per-request gate is being added
by the sidecar-manifest work
([#5869](https://github.com/diegosouzapw/OmniRoute/pull/5869) manifest +
[#5870](https://github.com/diegosouzapw/OmniRoute/pull/5870) `shouldTryBifrostForRequest`),
which lets `auto` route only manifest-eligible providers through Bifrost.
## Dashboard integration
The services dashboard polls `GET /api/services/<tool>/status` every 5s via
`src/app/(dashboard)/dashboard/providers/services/hooks/useServiceStatus.ts`,
returning `{ tool, state, pid, port, health, installedVersion, latestVersion,
updateAvailable, autoStart, … }`. There is no shared availability-context provider —
each component calls the hook per tool. On `!res.ok` the hook currently surfaces a
bare `HTTP <status>`; mapping the `error.type` field to a human explanation is a
tracked UX improvement, not a contract change.
## Consequences
- New engines register once in `ROUTER_BACKENDS`; consumers gain them via capability
queries without new per-id branches.
- "Is this a service or a routing backend?" is answered by the `lifecycle` field, not
by which list an id happens to appear in.
- The Bifrost supervision (#5817) and native hot-path migration (#5670) build on this
shared contract instead of special-casing each sidecar.