Compare commits

..

5 Commits

Author SHA1 Message Date
oyi77
8f96b10b40 fix(sse): resolve type errors in browserBackedChat stub
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-02 00:37:04 -03:00
oyi77
ce8cdc8224 chore: move sqlite-vec to optionalDependencies, fix js-tiktoken static import
Both changes ensure native binary dependencies are properly categorized as optional:

- sqlite-vec: moved from dependencies to optionalDependencies. Only used via
  lazy _require("sqlite-vec") in vectorStore.ts — zero static imports.
- js-tiktoken: already in optionalDependencies, import changed to createRequire
  pattern to avoid crash when package is not installed (same pattern as sqlite-vec
  in vectorStore.ts).

Resolves ScoutDeps findings from browser-pool pluginization audit.
2026-07-31 02:42:23 +07:00
oyi77
43b8819f99 fix(pr-8299): address all 5 review issues
Issue #1: Add @omniroute/browser-pool path to root tsconfig.json paths
Issue #2: Fix tryBackedChat fallback — call browserBackedChat outside if(loaded) guard
Issue #3: Fix grokClearance stub signature (signal?: AbortSignal) → string|null
Issue #4: Add comment clarifying async __resetBrowserPoolMetricsForTest vs upstream sync
Issue #5: Add test case for package-absent fallback in tryBackedChat

All 25 browser tests pass across 4 suites. typecheck:core passes.
2026-07-31 02:42:23 +07:00
oyi77
6880b3c821 fix: remove duplicate getMod/modPromise in browserBackedChat stub
Two copies of the module proxy got committed — the typed BrowserPoolModule
version at lines 50-56 and a stale any-typed duplicate at lines 64-71.
Removed the duplicate, keeping the typed version.

Verification:
- 40/40 browser tests pass (both previously-failing suites now green)
- typecheck:core: 0 errors
- env kill switch (OMNIROUTE_BROWSER_POOL=off): verified
2026-07-31 02:41:16 +07:00
oyi77
ad554a386c fix: align three stub implementations with original code
- chatUrlMatcher: restore original 3-arg signature (u, matchDomain, chatUrl)
  with PLACEHOLDER-aware path segment matching
- shouldUseGrokBrowserBacked: remove required param, restore env-var logic
  checking both WEB_COOKIE_USE_BROWSER and OMNIROUTE_BROWSER_POOL
- browserPool.ts: add Turbopack rationale comment and join-trick helper
  to satisfy the optional-import test assertions
- browserBackedChat.ts: replace any types with typed BrowserPoolModule interface

Verification: 40/40 browser node:test pass, typecheck:core 0 errors
2026-07-31 02:41:15 +07:00
460 changed files with 6134 additions and 41149 deletions

View File

@@ -1,197 +0,0 @@
# codebase-memory-mcp ignore list
#
# Padrão gitignore-style. Linhas começando com `#` são comentários.
# Barra final (`/`) = só diretório. Sem barra = casa arquivo OU diretório.
#
# O CBM também lê `.gitignore` automaticamente — esta lista deixa explícito o que
# os hooks do CBM vão pular. Se uma regra entrar em conflito entre os dois arquivos,
# vale a união. Editar este arquivo é mais barato do que confiar na herança implícita.
#
# Última reconciliação: 2026-07-31, status `ready` (513k nodes / 689k edges),
# `auto_index_limit=50000`, total indexável medido ≈11.546 arquivos (folga 4,3×).
#
# Fontes cruzadas:
# - `codebase-memory-mcp cli index_status --project home-diegosouzapw-dev-proxys-OmniRoute`
# → `not_indexed.dirs` (27) + `not_indexed.files` (336), todos `BY DESIGN`.
# - `.gitignore` deste repo (5.691 B) — fonte canônica secundária.
#
# Como auditar mudanças: depois de editar este arquivo, rodar `index_repository`
# (ou esperar `auto_watch` re-indexar) e re-checar `cli index_status` → comparar
# contagens em `not_indexed.dirs_count` e `not_indexed.files_count`.
# ─────────────────────────────────────────────────────────────────────────────
# 1. Diretorios de runtime / pacote — nao sao codigo-fonte
# ─────────────────────────────────────────────────────────────────────────────
node_modules/
node_modules
# Builds e artefatos reproduziveis (Layer 1 Next.js / Electron)
.build/
dist/
.next/
out/
# Electron especifico
electron/dist-electron/
electron/node_modules/
icon.iconset/
# Workspaces internos que tem proprio node_modules
@omniroute/opencode-plugin/dist/
@omniroute/opencode-plugin/node_modules/
@omniroute/opencode-provider/dist/
@omniroute/opencode-provider/node_modules/
# Recursos nativos compilados (C/JNI/wasm)
src/mitm/tproxy/native/build/
# Artefatos locais do Stryker / Playwright / coverage
.stryker-tmp/
reports/mutation/
stryker-output-*.json
.playwright-mcp/
test-results/
playwright-report/
blob-report/
# Analise / linters / caches
.analysis/
.sisyphus/
.plans/
.gitnexus
.worktrees
.codegraph/
# Quality artifacts (gerados por npm run lint --cache etc)
.eslintcache
.eslintcache-complexity
# Claude Code local state
.claude/scheduled_tasks.lock
.claude/scheduled_tasks/
.claude/sessions/
.claude/state.json
.claude/settings.local.json
# Serena / Antigravity / outras tools locais
.serena/
.antigravitycli/
.gemini/
.config/
# ─────────────────────────────────────────────────────────────────────────────
# 2. Diretorios com prefixo `_` — locais / privados (regra global do .gitignore)
# ─────────────────────────────────────────────────────────────────────────────
_*/
_artifacts/
_cache/
_mono_repo/
_references/
_tasks/
# ─────────────────────────────────────────────────────────────────────────────
# 3. Diretorios de tooling IA (state local, nao codigo)
# ─────────────────────────────────────────────────────────────────────────────
.agents/
.claude/
.vscode/
.idea/
.junie/
.omc/
.data/
.data-dev/
.local-data/
.logs/
.artifacts/
.source/
.superpowers/
.claude-flow/
.omnivscodeagent/
omnirouteCloud/
omnirouteSite/
.omniroute/
.stent/
# Subpaths especificos do Claude Code que nao estao em .claude/ (criados sob repo)
.claude/worktrees/
# ─────────────────────────────────────────────────────────────────────────────
# 4. Diretorios de dados / runtime locais (storage, env, secrets, scratch)
# ─────────────────────────────────────────────────────────────────────────────
data/
src/lib/env/
src/app/api/agent-skills/coverage/
src/app/api/cloud/
src/app/api/sync/cloud/
src/app/api/system/env/
tests/golden-set/data/
# Logs e saida de teste
logs/*
test_output.log
home-diegosouzapw-dev-automacoes-*.txt
# ─────────────────────────────────────────────────────────────────────────────
# 5. Diretorios do monorepo por subprojeto (nao fazem parte do app principal)
# ─────────────────────────────────────────────────────────────────────────────
security-analysis/
vscode-extension/
obsidian-plugin/node_modules/
# ─────────────────────────────────────────────────────────────────────────────
# 6. Diretorios de documentacao interna / workflow
# ─────────────────────────────────────────────────────────────────────────────
docs/superpowers/
# ─────────────────────────────────────────────────────────────────────────────
# 7. Arquivos especificos (nao diretorios inteiros)
# ─────────────────────────────────────────────────────────────────────────────
# Segredos e env — NUNCA indexar
.env
.env.*
!.env.example
!.env.homolog.example
# TypeScript build info e next env declaration
*.tsbuildinfo
next-env.d.ts
typescript
# SQLite transient files (WAL/SHM/journal)
*.sqlite-shm
*.sqlite-wal
*.sqlite-journal
# Mapas e source maps
*.map
# Bun / npm lockfiles ruidosos
bun.lock
# `cheaper-inference-gateway.svg` e arquivos de midia na raiz/asset ja cobertos
# pelos `ignored-suffix` do indexador (svg/png/jpg/ico/etc >50kB ou >500linhas);
# manter a regra explicita aqui ajuda a auditar:
cheaper-inference-gateway.svg
cheaper-inference-gateway-*.svg
# Husky internals
.husky/_/
# CI / quality metric artifacts
config/quality/quality-metrics.json
config/quality/test-impact-map.json
audit-report.json
.gh-discussions.json
# i18n audit (gerado por npm run scripts)
scripts/i18n/_audit.json
scripts/i18n/_pending-keys.json
# Cli binario local (scratch)
bin/omniroute.mjs
# Deploy / docker backups
deploy.sh
docker-compose.yml.bak
docker-compose.minimal.yml

View File

@@ -18,7 +18,6 @@ coverage
# Runtime data and logs
data
logs
.sandbox
# Local env files (inject at runtime via --env-file or -e)
.env

View File

@@ -1,6 +0,0 @@
ENABLE_LIVE_DEVIN_TESTS=0
DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7

View File

@@ -1414,6 +1414,10 @@ APP_LOG_TO_FILE=true
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
# OMNIROUTE_PLUGIN_PATH=
# Allow plugins to request the 'exec' permission (spawn child processes from the
# plugin worker sandbox). Disabled by default; set to 1 to enable (local operator only).
# OMNIROUTE_PLUGINS_ALLOW_EXEC=0
# ── Prompt cache (system prompt deduplication) ──
# Used by: open-sse/services — caches identical system prompts across requests.
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)
@@ -1839,18 +1843,6 @@ APP_LOG_TO_FILE=true
# ── Devin CLI binary path ──
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
# CLI_DEVIN_BIN=devin
# Agentic bridge-only binary override. The bridge still executes ACP stdio only.
# CLI_DEVIN_AGENTIC_BIN=devin
# Required isolated HOME for the agentic Devin child process.
# DEVIN_AGENTIC_HOME=/home/bridge
# Bounded ACP turn timeout in milliseconds. Default: 120000.
# DEVIN_AGENTIC_ACP_TIMEOUT_MS=120000
# Agentic bridge model aliases. Values must keep the devin-cli-agentic/ prefix.
# DEVIN_BRIDGE_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SONNET_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_OPUS_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_HAIKU_MODEL=devin-cli-agentic/swe-1-7
# DEVIN_BRIDGE_SUBAGENT_MODEL=devin-cli-agentic/swe-1-7
# ── Command Code (custom CLI) callback ──
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
@@ -2312,18 +2304,6 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# ─────────────────────────────────────────────────────────────────────────────
# HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage
# ─────────────────────────────────────────────────────────────────────────────
# ChatGPT Web (Codex) headless browser and outbound tool tunnel
# Used by: open-sse/executors/chatgpt-web-codex.ts
# Connection values entered in the dashboard override these global defaults.
# ─────────────────────────────────────────────────────────────────────────────
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
# CHROME_PATH=/usr/bin/chromium
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex
# ─────────────────────────────────────────────────────────────────────────────
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
# Containerized Chromium+VNC used for interactive browser-login credential

View File

@@ -9,14 +9,11 @@
## Validation
Choose the change type and focused loop from the
[Contribution Golden Path](../docs/dev/CONTRIBUTION_GOLDEN_PATH.md). The full unit suite,
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
Run only the focused loop for what you changed — the full unit suite, Vitest, the
60% coverage gate, and the production build all run in CI on this PR (#8329):
- [ ] Change type: provider / routing / UI / i18n / CLI / DB / build-deploy / other
- [ ] Focused tests and category gates from the golden path
- [ ] Focused tests for the change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- [ ] `npm run lint`
- [ ] Reconciled with the current active release base; focused checks rerun afterward
- [ ] Production-code changes include a new or updated automated test in this PR
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
@@ -32,4 +29,4 @@ Vitest, the 60% coverage gate, and the production build all run in CI on this PR
## Reviewer Notes
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.
- Call out any risky areas, migrations, feature flags, or manual validation that reviewers should know about.

View File

@@ -27,7 +27,7 @@ env:
jobs:
changes:
name: Change Classification
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
@@ -35,10 +35,13 @@ jobs:
workflow: ${{ steps.classify.outputs.workflow }}
testsOnly: ${{ steps.classify.outputs.testsOnly }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
# Refuse a PR that targets its own head branch before spending anything on it. #8912 has
# head == base == release/v3.8.50: no diff, can never merge, and it sits in the queue with
# a full check board attached on every push to that branch. One field comparison.
@@ -74,7 +77,7 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
@@ -88,9 +91,13 @@ jobs:
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run audit:deps
@@ -164,7 +171,7 @@ jobs:
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# needs lint so eslint-results artifact is available (same inventory as the
# blocking lint step). Allow lint failure so other ratchets still run.
needs: [changes, test-coverage, lint]
@@ -184,9 +191,13 @@ jobs:
contents: read
security-events: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Restore ESLint file cache
uses: actions/cache@v6
@@ -278,7 +289,7 @@ jobs:
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
quality-extended:
name: Quality Gates (Extended)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
@@ -288,10 +299,14 @@ jobs:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
# would lack the base ref and the gate would self-skip (base-unresolved).
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# Dead-code, cognitive-complexity, type-coverage foram promovidos ao job
# quality-gate (bloqueante) na Fase 7 INT — não rodam aqui para evitar duplo custo.
@@ -394,16 +409,20 @@ jobs:
docs-sync-strict:
name: Docs Sync (Strict)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:docs-all
# Previously-orphaned contract gates (existed as files, never wired anywhere).
@@ -423,7 +442,7 @@ jobs:
docs-lint:
name: Docs Lint (prose — advisory)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
@@ -433,9 +452,13 @@ jobs:
# existing doc corpus is brought up to style. Promote to blocking once it converges.
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- name: markdownlint (docs + root, advisory)
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
- name: Vale prose lint (Microsoft style, advisory)
@@ -450,7 +473,7 @@ jobs:
i18n-ui-coverage:
name: i18n UI Coverage
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
@@ -460,10 +483,14 @@ jobs:
# fetch-depth: 0 — the value-drift gate diffs en.json against the merge base to
# find rewritten English strings. On a shallow clone the base ref is missing and
# the gate self-skips (base-unresolved), so it would never actually run.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
# #8463: a rewritten English value used to leave its 39 translations behind
@@ -479,13 +506,17 @@ jobs:
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
i18n-glossary-zhcn:
name: i18n Glossary (zh-CN)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-CN
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
@@ -497,7 +528,7 @@ jobs:
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
i18n:
name: i18n Validation (all languages)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
@@ -505,7 +536,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
@@ -540,12 +571,15 @@ jobs:
pr-test-policy:
name: PR Test Policy
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
- name: Fetch base branch
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
- name: Validate source changes include tests
@@ -572,17 +606,17 @@ jobs:
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
# back to ubuntu-26.04 unless the PR head repo is this repository (push /
# back to ubuntu-latest unless the PR head repo is this repository (push /
# dispatch events are own-origin by definition). Any failure path (VM down,
# var unset/false) also falls back to ubuntu-26.04.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
# var unset/false) also falls back to ubuntu-latest.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
@@ -622,14 +656,18 @@ jobs:
package-artifact:
name: Package Artifact
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: build
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Download Next.js build artifact
@@ -665,15 +703,15 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-26.04, windows-latest]
os: [ubuntu-latest, windows-latest]
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
CSC_IDENTITY_AUTO_DISCOVERY: "false"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
@@ -712,14 +750,14 @@ jobs:
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 25
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
@@ -737,9 +775,13 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
@@ -769,27 +811,31 @@ jobs:
test-bun-sqlite:
name: Bun SQLite Compatibility
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run test:bun:db
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
@@ -799,9 +845,13 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
@@ -829,7 +879,7 @@ jobs:
# the release gate can still exercise them via workflow_dispatch when needed).
test-coverage:
name: Coverage
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
@@ -840,9 +890,13 @@ jobs:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Download all shard coverage
uses: actions/download-artifact@v8
@@ -926,14 +980,14 @@ jobs:
sonarqube:
name: SonarQube
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: test-coverage
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
@@ -978,7 +1032,7 @@ jobs:
coverage-pr-comment:
name: PR Coverage Comment
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }}
needs:
- changes
@@ -1057,7 +1111,7 @@ jobs:
test-e2e:
name: E2E Tests (${{ matrix.shard }}/9)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# Build artifact from the `build` job is downloaded instead of rebuilding
# (~5min saved per shard). 9 shards (up from 6) reduces tests per shard by
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
@@ -1079,9 +1133,13 @@ jobs:
DISABLE_SQLITE_AUTO_BACKUP: "true"
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Playwright browsers
@@ -1130,7 +1188,7 @@ jobs:
test-integration:
name: Integration Tests (${{ matrix.shard }}/2)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
@@ -1146,9 +1204,13 @@ jobs:
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
@@ -1156,7 +1218,7 @@ jobs:
test-security:
name: Security Tests
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
@@ -1165,16 +1227,20 @@ jobs:
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run test:security
ci-summary:
name: CI Dashboard
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
needs:
- changes

View File

@@ -21,7 +21,7 @@ jobs:
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
@@ -30,7 +30,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 1

View File

@@ -13,7 +13,7 @@ permissions:
jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read

View File

@@ -18,7 +18,7 @@ concurrency:
cancel-in-progress: true
jobs:
dast-smoke:
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
continue-on-error: true
@@ -33,6 +33,10 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
env:

View File

@@ -15,7 +15,7 @@ jobs:
(github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
&& vars.DEPLOY_ENABLED == 'true'
name: Deploy OmniRoute to VPS
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Check VPS SSH reachability from runner
id: reach

View File

@@ -33,7 +33,7 @@ permissions:
jobs:
prepare:
name: Resolve Docker release metadata
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
promote_latest: ${{ steps.version.outputs.promote_latest }}
@@ -42,7 +42,7 @@ jobs:
IMAGE_NAME: diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
@@ -145,7 +145,7 @@ jobs:
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
@@ -233,7 +233,7 @@ jobs:
- prepare
- build
if: needs.prepare.outputs.skip != 'true'
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
@@ -245,7 +245,7 @@ jobs:
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}

View File

@@ -20,14 +20,14 @@ permissions:
jobs:
validate:
name: Validate version
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
@@ -78,17 +78,17 @@ jobs:
target: mac-arm64
ext: -arm64.dmg
- platform: linux
runner: ubuntu-26.04
runner: ubuntu-latest
target: linux
ext: .AppImage
deb_ext: .deb
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
@@ -239,12 +239,12 @@ jobs:
# Now: attach everything that did build, then fail the job loudly (see the last
# step) so an incomplete channel is visible instead of silent.
if: ${{ !cancelled() && needs.validate.result == 'success' }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: write # softprops/action-gh-release creates the GitHub Release
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
@@ -329,7 +329,7 @@ jobs:
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
# green, because nothing ever asserted the release HAS binaries.
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: read
steps:

View File

@@ -40,7 +40,7 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────
lock-branch:
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Lock release/<tag> branch
env:
@@ -97,7 +97,7 @@ jobs:
# ─────────────────────────────────────────────────────────────────────────
guard-no-push-after-release:
if: github.event_name == 'push'
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Reject push if matching release tag exists
env:

View File

@@ -22,7 +22,7 @@ permissions:
jobs:
stryker-nobail:
name: Stryker disableBail (batch ${{ matrix.batch.name }})
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
@@ -41,9 +41,13 @@ jobs:
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
timeout-minutes: 300
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Run Stryker (disableBail)
env:

View File

@@ -28,11 +28,11 @@ concurrency:
jobs:
resolve-branch:
name: Resolve active release branch
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
outputs:
target: ${{ steps.branch.outputs.target }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
@@ -58,15 +58,15 @@ jobs:
compat-build-26:
name: Node 26 Compatibility Build
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: "26"
cache: npm
@@ -75,7 +75,7 @@ jobs:
# CI_NODE_VERSION=24). It failed every nightly with the runner-reclaimed
# signature ("The runner has received a shutdown signal" / "The operation was
# canceled", no exit code) always at the same Turbopack compile phase — a
# classic OOM kill on the memory-constrained 16 GB hosted runner. Turbopack's
# classic OOM kill on the memory-constrained ubuntu-latest runner. Turbopack's
# native (Rust, off-V8-heap) allocation is NOT bounded by --max-old-space-size
# and peaks far higher than webpack on this large module graph (#6409), and is
# heavier still under Node 26. Use the documented webpack fallback here: it still
@@ -88,7 +88,7 @@ jobs:
compat-tests:
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
strategy:
@@ -102,11 +102,11 @@ jobs:
DISABLE_SQLITE_AUTO_BACKUP: "true"
TEST_SHARD: ${{ matrix.shard }}/4
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
@@ -116,7 +116,7 @@ jobs:
report:
name: Open / update tracking issue on failure
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
needs: [resolve-branch, compat-build-26, compat-tests]
permissions:

View File

@@ -10,11 +10,13 @@ permissions:
jobs:
promptfoo-guard:
name: promptfoo — injection guard (block mode, no secret)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env:
@@ -44,7 +46,7 @@ jobs:
garak:
name: garak probes (skip without provider secret)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# NOTE: the `secrets` context is NOT available in a job-level `if:` — referencing
# it there makes GitHub reject the file on push (startup_failure on every push).
# Map the secret into a job-level env and gate each step on a presence check, so
@@ -61,10 +63,13 @@ jobs:
echo "run=false" >> "$GITHUB_OUTPUT"
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
if: steps.gate.outputs.run == 'true'
- uses: actions/setup-node@v7
if: steps.gate.outputs.run == 'true'
with: { node-version: "24", cache: npm }
- run: npm ci
if: steps.gate.outputs.run == 'true'
- name: Build CLI bundle

View File

@@ -10,7 +10,7 @@ permissions:
jobs:
stryker:
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# Mutation testing is expensive. History of the budget:
# - Full 8-module set TIMED OUT at the 180min cap (run 27705123780 = exactly 180min).
# The two god-files chatCore.ts/combo.ts dominated ~2/3 of the mutants and were
@@ -104,9 +104,13 @@ jobs:
# scripts/quality/mutation-radiography.mjs both merge per file).
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Restore Stryker incremental cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -141,12 +145,15 @@ jobs:
name: Mutation score ratchet (blocking)
needs: stryker
if: always()
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
- name: Download all mutation reports
uses: actions/download-artifact@v8
with:

View File

@@ -8,11 +8,15 @@ permissions:
issues: write
jobs:
property-random-seed:
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- name: fast-check random seed (high runs)
id: prop

View File

@@ -68,13 +68,13 @@ jobs:
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
# no local noauth CLIs => zero machine-specific false positives) and no contention.
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
@@ -116,7 +116,7 @@ jobs:
git checkout "$TARGET"
git log -1 --oneline
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
@@ -217,19 +217,19 @@ jobs:
# On a push, only run for a push to main — a push to release/* is handled by
# release-green above. Schedule/dispatch always run (they also sweep main).
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
@@ -331,12 +331,12 @@ jobs:
bank-ratchet-shrinks:
name: Bank ratchet shrinks
if: ${{ github.event_name != 'push' }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -371,6 +371,11 @@ jobs:
git checkout "$TARGET"
git log -1 --oneline
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Ratchet the baselines down

View File

@@ -10,31 +10,43 @@ permissions:
jobs:
heap:
name: Heap-growth gate
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:heap
chaos:
name: Resilience chaos (fault injection)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npm run test:chaos
k6-soak:
name: k6 load/soak
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Build CLI bundle
env:
@@ -66,7 +78,7 @@ jobs:
a11y:
name: A11y axe (nightly, freeze-and-alert)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and
# boots the standalone server itself (waits on /api/monitoring/health, 15min webServer
# timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact,
@@ -80,9 +92,13 @@ jobs:
DISABLE_SQLITE_AUTO_BACKUP: "true"
REQUIRE_AXE: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v6.1.0

View File

@@ -10,12 +10,14 @@ permissions:
jobs:
schemathesis:
name: Schemathesis — OpenAPI contract fuzz (advisory)
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with: { node-version: "24", cache: npm }
- run: npm ci
- name: Build CLI bundle
env:

View File

@@ -62,7 +62,7 @@ jobs:
# mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min.
# This job never runs on `pull_request`, so the fork-safety clause is always true here;
# it is kept verbatim so the expression stays greppable against ci.yml.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
permissions:
actions: read # find + download the CI run's next-build artifact for this SHA
contents: write # gh release upload (attach SBOM to the GitHub Release)
@@ -70,7 +70,7 @@ jobs:
packages: write # publish to npm.pkg.github.com
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
# Need full tag history to compare against highest semver when
@@ -78,7 +78,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
uses: actions/setup-node@v7
with:
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
registry-url: https://registry.npmjs.org
@@ -339,20 +339,20 @@ jobs:
echo "✅ Action finished for GitHub Packages"
publish-opencode-plugin:
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm provenance
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
# Full history needed for auto-bump: git diff against previous release tag
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
uses: actions/setup-node@v7
with:
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
registry-url: https://registry.npmjs.org

View File

@@ -26,16 +26,16 @@ defaults:
jobs:
test:
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
@@ -46,13 +46,13 @@ jobs:
build:
name: Build
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm

View File

@@ -26,16 +26,16 @@ defaults:
jobs:
test:
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
@@ -45,13 +45,13 @@ jobs:
build:
name: Build
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: npm

View File

@@ -25,17 +25,20 @@ jobs:
# path filters share existence reasons: code / docs / i18n / workflow.
changes:
name: Change Classification
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
@@ -58,16 +61,19 @@ jobs:
name: Build (advisory)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Fork-safe fallback uses Ubuntu 26.04's bundled Node 24 without setup-node.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-26.04' }}
# Dynamic runner — same fork-safe rule as ci.yml / fast-gates.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
# #7307: advisory for the first week of release-PR runs; remove
# continue-on-error after the production-build signal is stable.
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- run: node -e 'if (process.versions.node.split(".")[0] !== process.env.CI_NODE_VERSION) throw new Error("Expected Node " + process.env.CI_NODE_VERSION)'
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run build
@@ -82,11 +88,15 @@ jobs:
name: Docs Gates (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
- run: npm run check:api-docs-refs
@@ -101,7 +111,7 @@ jobs:
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-26.04, so this is inert until the flag flips.
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
# expression was DEAD CONFIGURATION: across 160 quality.yml runs the job never once landed on
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
@@ -115,7 +125,7 @@ jobs:
#
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
@@ -123,10 +133,14 @@ jobs:
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
@@ -165,81 +179,6 @@ jobs:
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
# ── G0 (trilho .50): gates do trilho A que faltavam no trilho B ──────────────
# The god-file refactor happens in PRs→release/**; without these, the release
# rail never sees a new import cycle, dead code, duplication or a security
# regression until the release PR to main. Deliberately NOT brought here:
# bundle-size (self-skips without a build — this rail's build job is advisory
# and uploads nothing, so it would be dead configuration) and the coverage
# run (fast-unit already runs the full suite; the coverage ratchet stays on
# the main rail via --allow-missing in lint-guard).
- run: npm run check:cycles
- run: npm run check:lockfile
- name: Duplication ratchet
run: npm run check:duplication
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet
run: npm run check:compression-budget
# Security scanners — same hardened install as ci.yml quality-extended
# (gh release download = authenticated, 5000 req/hr; curl to api.github.com
# is rate-limited to 60/hr and silently no-ops when throttled). The blocking
# gates below SKIP (exit 0) when their binary is absent — only a measured
# regression vs config/quality/quality-baseline.json blocks.
- name: Install security scanners (gitleaks/osv/actionlint/zizmor/oasdiff)
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
set +e
mkdir -p "$HOME/.local/bin"
# Ratchets compare scanner COUNTS across runs. Pin every auditor: a rule-set
# update must be an explicit PR that re-measures/rebaselines, never a random
# red (or green) caused by whatever "latest" served that morning.
GITLEAKS_VERSION=v8.30.1
OSV_SCANNER_VERSION=v2.3.8
ACTIONLINT_VERSION=v1.7.12
ZIZMOR_VERSION=1.25.2
OASDIFF_VERSION=v1.19.1
# gitleaks — pinned linux x64 tarball via gh (authed), extract binary
rm -rf /tmp/gl && mkdir -p /tmp/gl
gh release download "$GITLEAKS_VERSION" --repo gitleaks/gitleaks --pattern '*linux_x64.tar.gz' --dir /tmp/gl
tar -xzf /tmp/gl/*linux_x64.tar.gz -C "$HOME/.local/bin" gitleaks
# osv-scanner — pinned linux amd64 bare binary via gh (authed)
rm -rf /tmp/osv && mkdir -p /tmp/osv
gh release download "$OSV_SCANNER_VERSION" --repo google/osv-scanner --pattern '*linux_amd64' --dir /tmp/osv
install -m 0755 /tmp/osv/*linux_amd64 "$HOME/.local/bin/osv-scanner"
# actionlint — official installer from a pinned release tag (never main)
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") "$ACTIONLINT_VERSION" "$HOME/.local/bin"
# zizmor — pinned PyPI package (same version as ci.yml quality-extended)
pipx install "zizmor==$ZIZMOR_VERSION" || pip install --user "zizmor==$ZIZMOR_VERSION"
# oasdiff — pinned linux amd64 tarball via gh (authed), extract binary
rm -rf /tmp/oasd && mkdir -p /tmp/oasd
gh release download "$OASDIFF_VERSION" --repo Tufin/oasdiff --pattern '*linux_amd64.tar.gz' --dir /tmp/oasd
tar -xzf /tmp/oasd/*linux_amd64.tar.gz -C "$HOME/.local/bin" oasdiff
# ALWAYS export the bin dir (even if any step above failed)
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
"$HOME/.local/bin/gitleaks" version || true
"$HOME/.local/bin/actionlint" -version || true
"$HOME/.local/bin/osv-scanner" --version || true
"$HOME/.local/bin/oasdiff" --version || true
zizmor --version || true
- name: Secret scan (gitleaks, ratchet, blocking)
run: npm run check:secrets -- --ratchet
- name: Vulnerability ratchet (osv-scanner, ratchet, blocking)
run: npm run check:vuln-ratchet -- --ratchet
- name: Workflow lint (actionlint+zizmor, ratchet, blocking)
run: npm run check:workflows -- --ratchet
# BASE_REF is read by the script from the env (never interpolated into a
# shell body) — workflow-injection-safe. actions/checkout fetches remote
# refs, not a local branch named github.base_ref, so prefix origin/ or this
# gate self-skips every PR with reason=base-unresolved.
- name: OpenAPI breaking-change (oasdiff, ratchet, blocking)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: npm run check:openapi-breaking -- --ratchet
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
@@ -265,7 +204,7 @@ jobs:
# selector returns __RUN_ALL__ — full-suite authority is the parallel
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
# doubled wall time (~16 min extra on the hosted runner) without extra coverage.
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
#
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
# coverage remains required via `Unit Tests fast-path` (fast-unit).
@@ -329,15 +268,36 @@ jobs:
if-no-files-found: ignore
retention-days: 30
# Share fast-gates' checkout + npm ci instead of spending ~80s preparing a
# separate runner for a ~13s Vitest invocation. !cancelled() preserves the
# independent test signal when an earlier fast gate fails.
- name: Vitest
if: ${{ !cancelled() }}
run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast path runs on every PR.
# Advisory upload, own-origin only.
- name: Upload Vitest results to Trunk (advisory)
fast-vitest:
name: Vitest (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
# which is where flaky-detection volume actually comes from (ci.yml's heavy
# jobs only run on the release PR). Advisory upload, own-origin only.
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
@@ -350,9 +310,9 @@ jobs:
name: Unit Tests fast-path (${{ matrix.shard }}/4)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-26.04).
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
# critical path again (~8.5min → ~4.5min hosted; ~2min on the 8-slot
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
@@ -361,7 +321,7 @@ jobs:
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
# cache restore saturating the link), while the tests themselves tied, 2m54 vs 2m31. So
# self-hosted is strictly worse here and there is nothing to configure.
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
@@ -371,9 +331,13 @@ jobs:
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
@@ -398,18 +362,16 @@ jobs:
name: No new ESLint warnings
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
# quality-gate job). contents: read keeps checkout working.
permissions:
contents: read
security-events: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
@@ -423,29 +385,6 @@ jobs:
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
# inventory, two consumers; same reason ci.yml chains lint → quality-gate).
# The coverage-report artifact does not exist on this rail, so both ratchet
# invocations run --allow-missing: coverage.* metrics skip gracefully while
# the deterministic ones (eslint / openapi-coverage / i18n-ui) stay BLOCKING.
# Coverage authority remains on the main rail (ci.yml test-coverage → quality-gate).
- run: npm run quality:collect
- name: Ratchet check (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --summary .artifacts/quality-ratchet.md
- name: Require-tighten (blocking)
run: node scripts/quality/check-quality-ratchet.mjs --allow-missing --require-tighten
# CodeQL alerts ratchet — same semantics as ci.yml quality-gate: exits 1 ONLY
# on a real regression (open alerts > baseline in quality-baseline.json);
# a measurement failure (gh/auth/api) self-skips with exit 0.
- name: CodeQL alerts ratchet (blocking)
run: npm run check:codeql-ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Append ratchet summary
if: always()
run: cat .artifacts/quality-ratchet.md >> "$GITHUB_STEP_SUMMARY" || true
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
@@ -462,17 +401,21 @@ jobs:
name: Merge integrity (changelog + generated skills)
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
run: npm run check:changelog-integrity

View File

@@ -11,7 +11,7 @@ permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
# security-events: write removed — Scorecard findings are advisory and no longer
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
@@ -21,7 +21,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
with:
persist-credentials: false

View File

@@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true
jobs:
semgrep:
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:

View File

@@ -34,10 +34,15 @@ concurrency:
jobs:
sync-wiki:
name: Sync wiki with docs
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: "24"
- name: Clone wiki
env:

12
.gitignore vendored
View File

@@ -72,7 +72,6 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.devin-bridge.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
@@ -172,6 +171,7 @@ config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
.worktrees
bin/omniroute.mjs
# Consistent with .dockerignore / .npmignore
.omc/
@@ -201,17 +201,12 @@ scripts/i18n/_pending-keys.json
.codegraph/
# Fumadocs generated source
/.source/
# Temporary local worktrees used to build unpublished npm tarballs
/.deploy-build-*/
.source/
# AI agent local settings and configs
.agents/
.antigravitycli/
.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
# PR Reviews and local feedback files
pr_reviews*.json
@@ -248,8 +243,6 @@ _artifacts/ # release-green artifacts
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
.artifacts/
# Isolated Devin bridge workspaces, evidence, and test databases
.sandbox/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
@@ -257,4 +250,3 @@ tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/
docker-compose.yml.bak
.playwright-cli/

8
.source/dynamic.ts Normal file
View File

@@ -0,0 +1,8 @@
// @ts-nocheck
import { dynamic } from 'fumadocs-mdx/runtime/dynamic';
import * as Config from '../source.config';
const create = await dynamic<typeof Config, import("fumadocs-mdx/runtime/types").InternalTypeConfig & {
DocData: {
}
}>(Config, {"configPath":"source.config.ts","environment":"next","outDir":".source"}, {"doc":{"passthroughs":["extractedReferences"]}});

22
.source/source.config.mjs Normal file
View File

@@ -0,0 +1,22 @@
// source.config.ts
import { defineDocs, defineConfig } from "fumadocs-mdx/config";
var docs = defineDocs({
dir: "docs",
docs: {
files: [
"./architecture/**/*.md",
"./guides/**/*.md",
"./reference/**/*.md",
"./frameworks/**/*.md",
"./routing/**/*.md",
"./security/**/*.md",
"./compression/**/*.md",
"./ops/**/*.md"
]
}
});
var source_config_default = defineConfig();
export {
source_config_default as default,
docs
};

View File

@@ -1,12 +1,12 @@
{
"name": "@omniroute/opencode-plugin",
"version": "0.2.1",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@omniroute/opencode-plugin",
"version": "0.2.1",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"zod": "^4.4.3"

View File

@@ -1,5 +0,0 @@
node_modules
dist
*.log
.DS_Store
.env

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 OmniRoute contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,118 +0,0 @@
# @omniroute/openhands-plugin
OpenHands integration for the **OmniRoute AI Gateway**. Generates the OpenHands
environment and Docker config that wires an OpenHands agent-server to a running
OmniRoute instance — with the integration gotchas already handled.
## Why
Running OpenHands against OmniRoute directly hits several wall:
1. **Model name mismatch** — OpenHands sends `model: "deepseek-chat"`, OmniRoute
uses provider-prefixed IDs (`ds/deepseek-v4-flash`) or combos.
2. **Python 3.13 sandbox**`socket.socketpair()` fails under Docker's default
seccomp profile; the agent-server needs `privileged: true`.
3. **Host reachability** — the sandbox can't resolve `localhost` to the OmniRoute
host; needs `host.docker.internal:host-gateway`.
4. **Lost state** — conversations die with the container unless
`OH_PERSISTENCE_DIR` is a host volume.
5. **CORS** — the dashboard origin can't reach agent-server unless
`PERMITTED_CORS_ORIGINS` allows it.
This plugin encodes all of that into one command.
## Install
```bash
npm install -g @omniroute/openhands-plugin
# or: npx @omniroute/openhands-plugin ...
```
## Quick start
Generate the OpenHands `.env`:
```bash
omniroute-openhands env \
--api-key sk-... \
--model deepseek-chat \
--url http://192.168.3.106:20128
```
Generate a `docker-compose.yml` service:
```bash
omniroute-openhands compose \
--api-key sk-... \
--model glm-5.2 \
--persistence-dir /Users/me/.openhands-state \
--cors-origins http://100.73.44.17:3000
```
Or a plain `docker run`:
```bash
omniroute-openhands docker-run \
--api-key sk-... \
--model vivanta-core \
--persistence-dir /Users/me/.openhands-state
```
## Commands
| Command | Description |
|---------|-------------|
| `env` | Print OpenHands `.env` contents |
| `compose` | Print a Docker Compose service block |
| `docker-run` | Print a full `docker run` command |
| `models` | Print the default OpenHands → OmniRoute model map |
### Common options
| Flag | Description | Default |
|------|-------------|---------|
| `--api-key` | OmniRoute API key (`sk-...`) | — |
| `--model` | OpenHands model name or OmniRoute combo | — |
| `--url` | OmniRoute base URL | `http://localhost:20128` |
| `--persistence-dir` | Host dir for conversation state | `.openhands-state` |
| `--cors-origins` | Comma-separated allowed origins | `localhost:3000,3001` |
| `--sandbox-image` | OpenHands sandbox base image | — |
## Model mapping
OpenHands-friendly names are mapped to OmniRoute IDs/combo names:
| OpenHands sends | OmniRoute resolves to |
|-----------------|----------------------|
| `deepseek-chat` | `ds/deepseek-v4-flash` |
| `deepseek-reasoner` | `ds/deepseek-v4-pro` |
| `glm-5.2` | `nvidia/z-ai/glm-5.2` |
| `gpt-4o` | `openai/gpt-4o` |
| `claude-sonnet-4.5` | `anthropic/claude-sonnet-4.5` |
| ... | ... |
Or just pass an OmniRoute combo name (e.g. `--model vivanta-core`) — the Model
Alias Resolver and combo router accept it directly.
## Library usage
```ts
import {
buildOpenHandsEnv,
serializeOpenHandsEnv,
buildOpenHandsCompose,
resolveOpenHandsModel,
} from "@omniroute/openhands-plugin";
const env = buildOpenHandsEnv({
apiKey: "sk-...",
model: resolveOpenHandsModel("deepseek-chat"),
omnirouteUrl: "http://localhost:20128",
persistenceDir: "/Users/me/.openhands-state",
});
console.log(serializeOpenHandsEnv(env));
```
## License
MIT — same as OmniRoute.

File diff suppressed because it is too large Load Diff

View File

@@ -1,79 +0,0 @@
{
"name": "@omniroute/openhands-plugin",
"version": "0.1.0",
"description": "OpenHands integration for the OmniRoute AI Gateway. Generates OpenHands env + Docker Compose config (model mapping, sandbox, CORS, persistence) so OpenHands agents talk to OmniRoute out of the box.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"omniroute-openhands": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./env": {
"types": "./dist/env.d.ts",
"import": "./dist/env.js"
},
"./docker": {
"types": "./dist/docker.d.ts",
"import": "./dist/docker.js"
},
"./model-map": {
"types": "./dist/model-map.d.ts",
"import": "./dist/model-map.js"
}
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/env.test.ts tests/model-map.test.ts tests/docker.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
"omniroute",
"openhands",
"open-hands",
"openhands-plugin",
"openai-compatible",
"docker",
"agent"
],
"author": "OmniRoute contributors",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/diegosouzapw/OmniRoute.git",
"directory": "@omniroute/openhands-plugin"
},
"bugs": {
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
},
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/openhands-plugin#readme",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@omniroute/open-sse": "*"
},
"peerDependenciesMeta": {
"@omniroute/open-sse": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^22.19.19",
"tsup": "^8.5.1",
"tsx": "^4.22.3"
}
}

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env node
/**
* @omniroute/openhands-plugin CLI — generate OpenHands .env / Docker config
* for a running OmniRoute instance.
*
* Usage:
* omniroute-openhands env --api-key sk-... --model deepseek-chat [--url http://localhost:20128]
* omniroute-openhands compose --api-key sk-... --model deepseek-chat [--persistence-dir /path]
* omniroute-openhands docker-run --api-key sk-... --model deepseek-chat
* omniroute-openhands models (print the default model map)
*/
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
import { DEFAULT_OPENHANDS_MODEL_MAP } from "./model-map.ts";
function parseArgs(argv: string[]): Record<string, string> {
const out: Record<string, string> = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
const next = argv[i + 1];
if (next !== undefined && !next.startsWith("--")) {
out[key] = next;
i++;
} else {
out[key] = "true";
}
}
return out;
}
function requireArgs(args: Record<string, string>, names: string[]): void {
for (const name of names) {
if (!args[name]) {
console.error(`Missing required --${name}`);
process.exit(2);
}
}
}
const [cmd, ...rest] = process.argv.slice(2);
const args = parseArgs(rest);
switch (cmd) {
case "env": {
requireArgs(args, ["api-key", "model"]);
const env = buildOpenHandsEnv({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"],
corsOrigins: args["cors-origins"]?.split(","),
});
process.stdout.write(serializeOpenHandsEnv(env));
break;
}
case "compose": {
requireArgs(args, ["api-key", "model"]);
process.stdout.write(
buildOpenHandsCompose({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
corsOrigins: args["cors-origins"]?.split(","),
sandboxBaseImage: args["sandbox-image"],
})
);
break;
}
case "docker-run": {
requireArgs(args, ["api-key", "model"]);
process.stdout.write(
buildOpenHandsDockerRun({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
corsOrigins: args["cors-origins"]?.split(","),
sandboxBaseImage: args["sandbox-image"],
})
);
break;
}
case "models": {
for (const [name, target] of Object.entries(DEFAULT_OPENHANDS_MODEL_MAP)) {
process.stdout.write(`${name}\t->\t${target}\n`);
}
break;
}
default:
console.error(
"Usage: omniroute-openhands <env|compose|docker-run|models> [options]\n" +
"Options:\n" +
" --api-key <sk-...> OmniRoute API key (required for env/compose/docker-run)\n" +
" --model <name> OpenHands model name or OmniRoute combo\n" +
" --url <base> OmniRoute URL (default http://localhost:20128)\n" +
" --persistence-dir <path> Host dir for conversation state\n" +
" --cors-origins <a,b,...> Allowed CORS origins\n" +
" --sandbox-image <image> OpenHands sandbox base image"
);
process.exit(1);
}

View File

@@ -1,92 +0,0 @@
/**
* OpenHands agent-server Docker Compose generator for OmniRoute.
*
* Bakes in the integration fixes that were needed to run OpenHands against
* OmniRoute reliably:
* - `privileged: true` — Python 3.13 socket.socketpair() needs it under
* Docker's default seccomp profile
* - `extra_hosts` — host.docker.internal → host-gateway so the
* sandbox can reach OmniRoute on the host
* - host volume for OH_PERSISTENCE_DIR so conversations survive `docker rm`
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to hit agent-server
*/
export interface OpenHandsDockerOptions {
/** Agent-server image (default: the official OpenHands runtime image). */
image?: string;
/** Container name (default: openhands-agent). */
containerName?: string;
/** Model name to pass via LLM_MODEL. */
model: string;
/** OmniRoute API key. */
apiKey: string;
/** OmniRoute base URL reachable from the sandbox (default http://localhost:20128). */
omnirouteUrl?: string;
/** Host directory for OH_PERSISTENCE_DIR (must match env.ts persistenceDir). */
persistenceDir: string;
/** CORS origins to permit. */
corsOrigins?: string[];
/** Sandbox base image (defaults to OpenHands default). */
sandboxBaseImage?: string;
/** Set true to use host networking instead of extra_hosts. */
hostNetwork?: boolean;
}
export function buildOpenHandsCompose(opts: OpenHandsDockerOptions): string {
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
const containerName = opts.containerName ?? "openhands-agent";
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const lines: string[] = [];
lines.push(`services:`);
lines.push(` openhands:`);
lines.push(` image: ${image}`);
lines.push(` container_name: ${containerName}`);
lines.push(` privileged: true`);
lines.push(` environment:`);
lines.push(` LLM_MODEL: "${opts.model}"`);
lines.push(` LLM_BASE_URL: "${omnirouteHost}/v1"`);
lines.push(` LLM_API_KEY: "${opts.apiKey}"`);
lines.push(` OH_PERSISTENCE_DIR: "/opt/.openhands-state"`);
lines.push(` PERMITTED_CORS_ORIGINS: "${cors.join(",")}"`);
if (opts.sandboxBaseImage) {
lines.push(` SANDBOX_BASE_IMAGE: "${opts.sandboxBaseImage}"`);
}
lines.push(` volumes:`);
lines.push(` - ${opts.persistenceDir}:/opt/.openhands-state`);
lines.push(` extra_hosts:`);
lines.push(` - "host.docker.internal:host-gateway"`);
return lines.join("\n") + "\n";
}
/**
* docker run equivalent of {@link buildOpenHandsCompose} — returns the full
* `docker run` command line.
*/
export function buildOpenHandsDockerRun(opts: OpenHandsDockerOptions): string {
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const parts = [
"docker run",
"--privileged",
"--add-host host.docker.internal:host-gateway",
`-e LLM_MODEL="${opts.model}"`,
`-e LLM_BASE_URL="${omnirouteHost}/v1"`,
`-e LLM_API_KEY="${opts.apiKey}"`,
`-e OH_PERSISTENCE_DIR=/opt/.openhands-state`,
`-e PERMITTED_CORS_ORIGINS="${cors.join(",")}"`,
`-v "${opts.persistenceDir}:/opt/.openhands-state"`,
image,
];
return parts.join(" ") + "\n";
}

View File

@@ -1,63 +0,0 @@
/**
* OpenHands `.env` generator for the OmniRoute AI Gateway.
*
* Produces the OpenHands environment that points an OpenHands agent-server at
* a running OmniRoute instance and fixes the integration gotchas found in the
* field:
* - LLM_MODEL — OpenHands-friendly model name → OmniRoute model/combo
* - LLM_BASE_URL — OmniRoute OpenAI-compatible endpoint
* - LLM_API_KEY — OmniRoute key (sk-...)
* - OH_PERSISTENCE_DIR — host-mounted SQLite/conversation persistence
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to reach agent-server
*/
export interface OpenHandsEnvOptions {
/** OmniRoute base URL as seen from the agent-server (default localhost:20128). */
omnirouteUrl?: string;
/** OmniRoute API key (sk-...). */
apiKey: string;
/** OpenHands model name (e.g. "deepseek-chat") or OmniRoute combo/model. */
model: string;
/** Host directory for OH_PERSISTENCE_DIR (default: current dir + .openhands-state). */
persistenceDir?: string;
/** CORS origins that must reach the agent-server (default dashboard origin + localhost). */
corsOrigins?: string[];
/** Optional OpenHands sandbox base image. */
sandboxBaseImage?: string;
}
export function buildOpenHandsEnv(opts: OpenHandsEnvOptions): Record<string, string> {
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const persistence = opts.persistenceDir ?? `${process.cwd()}/.openhands-state`;
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const env: Record<string, string> = {
LLM_MODEL: opts.model,
LLM_BASE_URL: `${omnirouteHost}/v1`,
LLM_API_KEY: opts.apiKey,
OH_PERSISTENCE_DIR: persistence,
PERMITTED_CORS_ORIGINS: cors.join(","),
};
if (opts.sandboxBaseImage) {
env.SANDBOX_BASE_IMAGE = opts.sandboxBaseImage;
}
return env;
}
/**
* Serialize the env record to `.env` file content (KEY=VALUE lines).
* Values are not quoted unless they contain whitespace or `#`.
*/
export function serializeOpenHandsEnv(env: Record<string, string>): string {
const lines: string[] = [];
for (const [key, value] of Object.entries(env)) {
const needsQuotes = /[\s#]/.test(value);
lines.push(needsQuotes ? `${key}="${value}"` : `${key}=${value}`);
}
return lines.join("\n") + "\n";
}

View File

@@ -1,18 +0,0 @@
/**
* @omniroute/openhands-plugin — OpenHands integration for the OmniRoute AI Gateway.
*
* Generates the OpenHands environment and Docker Compose / docker run config
* that wires an OpenHands agent-server to a running OmniRoute instance:
* model mapping, sandbox privileges, host-gateway networking, persistent
* conversation state and CORS.
*/
export { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
export type { OpenHandsEnvOptions } from "./env.ts";
export { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
export type { OpenHandsDockerOptions } from "./docker.ts";
export {
DEFAULT_OPENHANDS_MODEL_MAP,
resolveOpenHandsModel,
buildOpenHandsModel,
} from "./model-map.ts";
export type { OpenHandsModelMap } from "./model-map.ts";

View File

@@ -1,64 +0,0 @@
/**
* OpenHands → OmniRoute model mapping.
*
* OpenHands sends `model: "<LLM_MODEL>"` and expects the OpenAI-compatible
* endpoint to accept that exact string. OmniRoute uses provider-prefixed
* model IDs (`ds/deepseek-v4-flash`) and combo names. This module maps
* common OpenHands-friendly names to the OmniRoute model/combo they should
* resolve to, and back-fills the `LLM_MODEL` value for OpenHands.
*/
export interface OpenHandsModelMap {
/** OpenHands-friendly model name (e.g. "deepseek-chat") */
[openHandsName: string]: string;
}
/**
* Default mapping for the model names OpenHands and the broader ecosystem
* commonly send. Values are OmniRoute model IDs or combo names. Extend or
* override via {@link resolveOpenHandsModel}.
*/
export const DEFAULT_OPENHANDS_MODEL_MAP: OpenHandsModelMap = Object.freeze({
// DeepSeek
"deepseek-chat": "ds/deepseek-v4-flash",
"deepseek-reasoner": "ds/deepseek-v4-pro",
// Claude / Anthropic
"claude-sonnet-4.5": "anthropic/claude-sonnet-4.5",
"claude-opus-4.1": "anthropic/claude-opus-4.1",
"claude-haiku-4.5": "anthropic/claude-haiku-4.5",
// GPT / OpenAI
"gpt-4o": "openai/gpt-4o",
"gpt-4o-mini": "openai/gpt-4o-mini",
"gpt-5": "openai/gpt-5",
// Gemini
"gemini-2.5-flash": "gemini/gemini-2.5-flash",
"gemini-2.5-pro": "gemini/gemini-2.5-pro",
// GLM / Z.AI (NVIDIA NIM free endpoint)
"glm-5.2": "nvidia/z-ai/glm-5.2",
});
/**
* Resolve the OmniRoute model ID for an OpenHands-friendly model name.
* Returns the input unchanged when no mapping exists (OmniRoute will try to
* resolve it as a literal model/combo).
*/
export function resolveOpenHandsModel(
openHandsModel: string,
map: OpenHandsModelMap = DEFAULT_OPENHANDS_MODEL_MAP
): string {
if (!openHandsModel) return openHandsModel;
const mapped = map[openHandsModel];
return mapped ?? openHandsModel;
}
/**
* Build the `LLM_MODEL` value for OpenHands from an OmniRoute model ID/combo.
*
* OpenHands only surfaces the literal `LLM_MODEL` string in its UI, so for
* OmniRoute combos (e.g. "vivanta-core") that's already the right value.
* For provider-prefixed IDs, we return them as-is — the OmniRoute Model
* Alias Resolver accepts both the raw ID and aliases on the `/v1` endpoint.
*/
export function buildOpenHandsModel(omnirouteModelOrCombo: string): string {
return omnirouteModelOrCombo;
}

View File

@@ -1,76 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "../src/env.ts";
import { resolveOpenHandsModel, buildOpenHandsModel } from "../src/model-map.ts";
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "../src/docker.ts";
test("buildOpenHandsEnv produces LLM vars pointing at OmniRoute", () => {
const env = buildOpenHandsEnv({
apiKey: "sk-test-123",
model: "deepseek-chat",
omnirouteUrl: "http://192.168.3.106:20128",
persistenceDir: "/opt/state",
corsOrigins: ["http://100.73.44.17:3000"],
});
assert.equal(env.LLM_MODEL, "deepseek-chat");
assert.equal(env.LLM_BASE_URL, "http://192.168.3.106:20128/v1");
assert.equal(env.LLM_API_KEY, "sk-test-123");
assert.equal(env.OH_PERSISTENCE_DIR, "/opt/state");
assert.equal(env.PERMITTED_CORS_ORIGINS, "http://100.73.44.17:3000");
});
test("serializeOpenHandsEnv quotes values with whitespace/#", () => {
const out = serializeOpenHandsEnv({ LLM_MODEL: "deepseek-chat", LLM_BASE_URL: "http://localhost:20128/v1" });
const lines = out.trim().split("\n");
assert.ok(lines.some((l) => l.startsWith("LLM_MODEL=deepseek-chat")));
assert.ok(lines.some((l) => l.startsWith("LLM_BASE_URL=http://localhost:20128/v1")));
});
test("resolveOpenHandsModel maps known names to OmniRoute IDs", () => {
assert.equal(resolveOpenHandsModel("deepseek-chat"), "ds/deepseek-v4-flash");
assert.equal(resolveOpenHandsModel("glm-5.2"), "nvidia/z-ai/glm-5.2");
assert.equal(resolveOpenHandsModel("gpt-4o"), "openai/gpt-4o");
});
test("resolveOpenHandsModel passes unknown names through unchanged", () => {
assert.equal(resolveOpenHandsModel("vivanta-core"), "vivanta-core");
assert.equal(resolveOpenHandsModel(""), "");
});
test("resolveOpenHandsModel accepts custom map overrides", () => {
const custom = { "my-alias": "nvidia/z-ai/glm-5.2" };
assert.equal(resolveOpenHandsModel("my-alias", custom), "nvidia/z-ai/glm-5.2");
assert.equal(resolveOpenHandsModel("deepseek-chat", custom), "deepseek-chat");
});
test("buildOpenHandsModel passes combo names through", () => {
assert.equal(buildOpenHandsModel("vivanta-core"), "vivanta-core");
assert.equal(buildOpenHandsModel("ds/deepseek-v4-flash"), "ds/deepseek-v4-flash");
});
test("buildOpenHandsCompose includes privileged, extra_hosts, volume, CORS", () => {
const compose = buildOpenHandsCompose({
apiKey: "sk-x",
model: "deepseek-chat",
persistenceDir: "/Users/me/.openhands-state",
corsOrigins: ["http://localhost:3000"],
});
assert.ok(compose.includes("privileged: true"), "privileged present");
assert.ok(compose.includes("host.docker.internal:host-gateway"), "host-gateway present");
assert.ok(compose.includes("/Users/me/.openhands-state"), "persistence volume present");
assert.ok(compose.includes("LLM_BASE_URL: \"http://localhost:20128/v1\""), "base url present");
assert.ok(compose.includes("PERMITTED_CORS_ORIGINS: \"http://localhost:3000\""), "cors present");
});
test("buildOpenHandsDockerRun produces a runnable docker command", () => {
const run = buildOpenHandsDockerRun({
apiKey: "sk-x",
model: "glm-5.2",
persistenceDir: "/opt/state",
});
assert.ok(run.startsWith("docker run"));
assert.ok(run.includes("--privileged"));
assert.ok(run.includes("--add-host host.docker.internal:host-gateway"));
assert.ok(run.includes("LLM_MODEL=\"glm-5.2\""));
assert.ok(run.includes("LLM_BASE_URL=\"http://localhost:20128/v1\""));
});

View File

@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["node"],
"ignoreDeprecations": "6.0",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"declaration": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "tests"]
}

View File

@@ -1,15 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/cli.ts"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: false,
splitting: false,
treeshake: false,
target: "node18",
outDir: "dist",
minify: false,
cjsInterop: false,
});

1
AMIT Normal file
View File

@@ -0,0 +1 @@

View File

@@ -45,7 +45,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
| Database | `src/lib/db/` | SQLite domain modules (95 files, 110 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 104 tools (42 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 13-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -332,7 +332,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (13-factor scoring, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
| Auto-Combo (12-factor scoring, 18 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
@@ -461,18 +461,10 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
git fetch origin "$BASE_BRANCH"
git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
cd ".claude/worktrees/${TASK##*/}"
# Reuse the main checkout's node_modules to skip a per-worktree npm install.
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# symlink node_modules from the main checkout to skip a per-worktree npm install:
ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
```
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
runners all keep passing — the error names "filesystem root", not the worktree, so it
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
In Claude Code prefer the native `EnterWorktree` tool (it already creates worktrees under
`.claude/worktrees/`): create the worktree with the command above, then call `EnterWorktree`
with its `path`.

View File

@@ -2,11 +2,6 @@
Thank you for your interest in contributing! This guide covers everything you need to get started.
For the official per-change workflow, start with the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md). It maps provider, routing,
UI/UX, i18n, CLI, database, and build/deploy changes to their contracts, focused tests, CI
coverage, and reconciliation steps.
---
## Development Setup
@@ -203,11 +198,10 @@ Coverage notes:
### Pull Request Requirements
Before opening a PR, use the
[Contribution Golden Path](docs/dev/CONTRIBUTION_GOLDEN_PATH.md) to run the focused loop for
what you changed. The full unit suite (4 CI shards), Vitest, the **60%+** coverage gate, and
the production build are CI's responsibility — running them locally adds no signal the PR
checks will not already give you, and on smaller machines it can saturate the host (#8084):
Before opening a PR, run the focused loop for what you changed. The full unit suite
(4 CI shards), Vitest, the **60%+** coverage gate, and the production build are CI's
responsibility — running them locally adds no signal the PR checks will not already
give you, and on smaller machines it can saturate the host (#8084):
- Run the test files that cover your change: `node --import tsx/esm --test tests/unit/<file>.test.ts`
- Run `npm run lint`
@@ -277,7 +271,7 @@ src/ # TypeScript (.ts / .tsx)
│ ├── a2a/ # Agent-to-Agent v0.3 protocol server
│ ├── acp/ # Agent Communication Protocol registry
│ ├── compliance/ # Compliance policy engine
│ ├── db/ # SQLite domain modules + 130 migrations
│ ├── db/ # SQLite database layer (21 modules + 16 migrations)
│ ├── memory/ # Persistent conversational memory
│ ├── oauth/ # OAuth providers, services, and utilities
│ ├── skills/ # Extensible skill framework
@@ -287,7 +281,7 @@ src/ # TypeScript (.ts / .tsx)
├── mitm/ # MITM proxy (cert, DNS, target routing)
├── shared/
│ ├── components/ # React components (.tsx)
│ ├── constants/ # Provider definitions (290), MCP scopes, 19 routing strategies
│ ├── constants/ # Provider definitions (177), MCP scopes, 14 routing strategies
│ ├── utils/ # Circuit breaker, sanitizer, auth helpers
│ └── validation/ # Zod v4 schemas
└── sse/ # SSE proxy pipeline
@@ -295,7 +289,7 @@ src/ # TypeScript (.ts / .tsx)
open-sse/ # @omniroute/open-sse workspace
├── executors/ # 14 provider-specific request executors
├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.)
├── mcp-server/ # MCP server (104 tools, 3 transports, 31 scopes)
├── mcp-server/ # MCP server (25 tools, 3 transports, 10 scopes)
├── services/ # 36+ services (combo, autoCombo, rateLimitManager, etc.)
├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama)
├── transformer/ # Responses API transformer

View File

@@ -236,11 +236,6 @@ FROM runner-base AS runner-cli
# runner-base runs.
USER root
# The CLI image can use the internal ChatGPT Web (Codex) Chromium sidecar over
# CDP without installing a second browser in this container.
COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-core
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
# Install system dependencies required by openclaw (git+ssh references).
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \

View File

@@ -241,53 +241,12 @@ curl http://localhost:20128/v1/chat/completions \
<b>What Kimi's support powers:</b> Kimi's API credits power OmniRoute's AI-validated release pipeline — the <i>merge validation powered by Kimi K3</i> stage that reviews every pull request before it ships — plus day-to-day feature development. First-class Kimi support ships on both rails: the direct <a href="https://platform.kimi.ai?aff=omniroute">Kimi API</a> (<code>kimi-k3</code>) and the <a href="https://www.kimi.com/code?aff=omniroute">Kimi Code coding plan</a> (OAuth and API key). OmniRoute is also the first Brazilian open-source project in Kimi's support program. <a href="https://platform.kimi.ai?aff=omniroute"><b>Get a Kimi API key →</b></a>
</td>
</tr>
<tr>
<td align="center" width="150">
<a href="https://cheaperinference.com/?utm_source=omniroute">
<img src="public/providers/cheaperinference.svg" width="64" alt="Cheaper Inference"/>
</a>
<br/><b>Cheaper Inference</b><br/><sub>cheaperinference.com</sub><br/><br/>
<img src="https://img.shields.io/badge/Open_Source_Friend-31f889?style=flat-square&labelColor=04170d" alt="Open Source Friend"/>
</td>
<td>
Thanks to <b>Cheaper Inference</b>, an OmniRoute Open Source Friend, for backing this project! Cheaper Inference is a cost-ranked gateway that resells 42 frontier models — Claude, GPT-5.x, Gemini, Kimi K3, GLM, DeepSeek, Grok and MiniMax — behind one OpenAI-compatible endpoint, routing each request to the cheapest eligible provider without ever charging above the model maker's list price.
<br/><br/>
<b>First-class support in OmniRoute:</b> Chat Completions, the native <code>/v1/responses</code> endpoint, vision, tool calling and 3 image models (<code>grok-imagine</code>, <code>nano-banana-pro</code>, <code>nano-banana-2</code>, reachable as <code>cheaperinference/&lt;model&gt;</code>). <a href="https://cheaperinference.com/?utm_source=omniroute"><b>Get an API key →</b></a>
</td>
</tr>
</table>
<sub>Links tagged <code>aff=omniroute</code> are partner links. They fund the project at no extra cost to you.</sub>
<br/>
<details open>
<summary><sub><b>🎟️ Affiliates Promo</b> — free signup coupons from providers we don't sponsor (click to expand)</sub></summary>
<sub><i>This section is for referral/coupon codes only. Sponsored partnerships live in <b>🤝 Supported by our Open Source Friends</b> above. OmniRoute has no sponsorship or partnership with the providers listed here — these are public coupons anyone can use.</i></sub>
<table>
<tr>
<td align="center" width="120">
<a href="https://agentrouter.org/register?aff=70LM">
<img src="public/providers/agentrouter.png" width="32" alt="AgentRouter"/>
</a>
<br/><sub><b>AgentRouter</b></sub><br/><sub>agentrouter.org</sub>
</td>
<td>
<sub><b><a href="https://agentrouter.org/register?aff=70LM">AgentRouter</a></b> — affiliate signup · <b>$100 free credits</b> on signup (free server, expect higher latency — best for testing, not production). First-class support in OmniRoute since <b>v3.8.50</b>: Chat Completions, the Anthropic-compatible wire format and the OpenAI-compatible path. Available models include <code>claude-opus-4-8</code>, <code>claude-opus-5</code>, <code>gpt-5.6-sol</code> and more. <b><a href="https://agentrouter.org/register?aff=70LM">Grab your $100 →</a></b></sub>
<br/><br/>
<sub>⚠️ <i>Affiliate link — OmniRoute has no sponsorship or partnership with this provider.</i></sub>
</td>
</tr>
</table>
<sub>Know another provider with a generous free signup coupon that benefits OmniRoute users? Open an issue and we'll add it here.</sub>
</details>
<br/>
<div align="center">
## 🎯 Combos — The Flagship

View File

@@ -1,26 +0,0 @@
# Third-Party Notices
## codex-chatgpt-web
Parts of `open-sse/vendor/codex-chatgpt-web/` are adapted from
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit
`55592fca0ba19a27f1b769cec8fff61ff340a785`.
MIT License
Copyright (c) 2026 codex-chatgpt-web contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,56 +0,0 @@
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, "..");
export function resolveChatGptWebCodexMcpEntry(rootDir = root, exists = existsSync) {
const candidates = [
join(
rootDir,
"dist",
"open-sse",
"vendor",
"codex-chatgpt-web",
"adapters",
"chatgpt-web",
"mcp-server.js"
),
join(
rootDir,
"open-sse",
"vendor",
"codex-chatgpt-web",
"adapters",
"chatgpt-web",
"mcp-server.ts"
),
];
return candidates.find((candidate) => exists(candidate)) ?? null;
}
export async function startChatGptWebCodexMcp(args = process.argv.slice(2), rootDir = root) {
const socketIndex = args.indexOf("--broker-socket");
const brokerSocketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
if (!brokerSocketPath) throw new Error("--broker-socket is required");
const entry = resolveChatGptWebCodexMcpEntry(rootDir);
if (!entry) throw new Error("ChatGPT Web (Codex) MCP entrypoint was not found");
if (entry.endsWith(".ts")) {
const { register } = await import("node:module");
register("tsx/esm", pathToFileURL(`${rootDir}/`));
}
const module = await import(pathToFileURL(entry).href);
await module.runChatGptMcpServer({ brokerSocketPath });
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
startChatGptWebCodexMcp().catch((error) => {
console.error(
`ChatGPT Web (Codex) MCP konnte nicht gestartet werden: ${error?.message || error}`
);
process.exit(1);
});
}

View File

@@ -1 +0,0 @@
- **fix(executors):** Vertex AI now routes Claude models through the native Anthropic `rawPredict` endpoint instead of the generic OpenAI-compatible partner endpoint, and synthesizes a real streaming response so Claude-via-Vertex works with `stream: true` ([#8909](https://github.com/diegosouzapw/OmniRoute/pull/8909)) — thanks @wgordon17

View File

@@ -1,14 +0,0 @@
- **fix(sse):** Claude reasoning-effort suffix ids (`-high`/`-low`/`-medium`/`-xhigh`) now strip
correctly on any provider serving a real Claude model, not just the direct Anthropic provider
([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
- **fix(sse):** the no-thinking (`no-think/`) catalog variant's provider-qualification bug — which
made it unusable outside the direct provider, both in the discovery catalog and the dashboard
playground — is fixed ([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
- **fix(sse):** a single unrecognized model id on a Vertex connection no longer cools down every
other model on that connection for 2 minutes — Vertex 404s are now scoped to a per-model
lockout via `passthroughModels` instead of a connection-wide cooldown
([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))
- **fix(sse):** Vertex `PERMISSION_DENIED` 403s are now disambiguated using Google's own
documented error format — a genuinely connection-wide cause (API disabled, project-level IAM
denial) still cools the whole connection, while a model-specific denial locks out only that
model ([#9006](https://github.com/diegosouzapw/OmniRoute/pull/9006))

View File

@@ -1 +0,0 @@
- **fix(dashboard):** model-level allowed/blocked param edits now persist when the compatibility popover is closed by clicking outside, and a failed save no longer clears the edit or reports success ([#9013](https://github.com/diegosouzapw/OmniRoute/pull/9013))

View File

@@ -1,2 +0,0 @@
/target
Cargo.lock

View File

@@ -1,27 +0,0 @@
[workspace]
resolver = "2"
members = [
"crates/core-api",
"crates/tokenizer",
"crates/tests",
"crates/bench",
"crates/ffi",
]
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/Egorich-print/OmniRoute"
[workspace.dependencies]
core-api = { path = "crates/core-api" }
tokenizer = { path = "crates/tokenizer" }
tiktoken-rs = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
[profile.release]
lto = true
codegen-units = 1

View File

@@ -1,61 +0,0 @@
# compression-core
Standalone Rust core for AI context optimization — tokenization, compression,
hashing, translation primitives. Independent OSS library usable by OmniRoute,
OpenCode, Cline, Roo, and any AI proxy. No OmniRoute imports anywhere.
## Layout
```
compression-core/
├── Cargo.toml # workspace
├── crates/
│ ├── core-api/ # stable public API (traits + types) — no host deps
│ ├── tokenizer/ # tiktoken (cl100k_base, o200k_base) — PORTED
│ ├── tests/ # golden tests against fixtures/expected/
│ ├── bench/ # criterion benchmarks
│ └── ffi/ # N-API adapter (integration phase)
├── fixtures/
│ ├── tokenizer/ # JS-generated token counts (13 samples)
│ └── expected/ # manifests
└── scripts/
├── generate-fixtures.ts # JS reference output (source of truth)
└── verify-golden.ts # regen + cargo test
```
## Porting order (per design)
1. tiktoken (done — golden 100%)
2. ionizer
3. headroom
4. caveman
5. RTK (last — biggest, requires proven harness)
## Golden pipeline
```text
fixtures → JS implementation → expected.json → Rust → assert_eq!
```
`node scripts/verify-golden.ts` regenerates fixtures from the current JS code
and runs `cargo test -p compression-tests`. Until 100% match, JS stays in prod.
## Measured baseline
| Impl | Input | Cost |
|---|---|---|
| JS js-tiktoken (cl100k) | 230K chars | 37.9 ms |
| Rust tiktoken-rs (cl100k) | ~440K chars | 21.4 ms |
Per-char Rust is ~3x faster; golden output is byte-identical on all fixtures.
## Status
- [x] workspace + stable API (`core-api`)
- [x] tokenizer port + golden tests (100% match)
- [x] bench harness (criterion)
- [ ] ionizer
- [ ] headroom
- [ ] caveman
- [ ] RTK
- [ ] N-API adapter

View File

@@ -1,17 +0,0 @@
[package]
name = "compression-bench"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
core-api = { workspace = true }
tokenizer = { workspace = true }
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "tokenizer"
harness = false

View File

@@ -1,19 +0,0 @@
//! Criterion bench for the tokenizer. Baseline target: < 5 ms per 57K tokens
//! (JS js-tiktoken measures ~38 ms on the same input).
use core_api::TokenCounter;
use criterion::{criterion_group, criterion_main, Criterion};
use tokenizer::TiktokenCounter;
fn bench_tokenizer(c: &mut Criterion) {
let counter = TiktokenCounter::default();
// ~230K chars ≈ 57K cl100k tokens (mirrors the measured JS baseline).
let text = "Hello world! This is a test of tokenization performance. \
The quick brown fox jumps over the lazy dog. "
.repeat(4000);
c.bench_function("cl100k_57k_tokens", |b| b.iter(|| counter.count(&text)));
}
criterion_group!(benches, bench_tokenizer);
criterion_main!(benches);

View File

@@ -1,10 +0,0 @@
[package]
name = "core-api"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -1,76 +0,0 @@
//! Stable public API of the compression core.
//!
//! This crate is intentionally free of any OmniRoute-specific types.
//! It defines the contracts that every adapter (N-API, sidecar, CLI)
//! implements, so algorithms stay independent of the host project.
use serde::{Deserialize, Serialize};
/// Role of a message in a conversation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
/// One chat message. Field-compatible with OpenAI `messages[]` entries.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
/// Tokenizer encodings supported by the core.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Encoding {
#[serde(rename = "cl100k_base")]
Cl100kBase,
#[serde(rename = "o200k_base")]
O200kBase,
}
/// Configuration for a compression pass.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct CompressionConfig {
/// Target token budget for the compressed messages.
pub budget_tokens: Option<u64>,
/// Engine stack priority hint (rtk=10, ionizer=13, headroom=15, ...).
pub stack_priority: Option<u32>,
}
/// Outcome of a compression pass.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompressionResult {
pub messages: Vec<Message>,
pub compressed: bool,
pub stats: Option<CompressionStats>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CompressionStats {
pub saved_tokens: Option<u64>,
pub input_tokens: Option<u64>,
pub output_tokens: Option<u64>,
}
/// A token counter. Pure, stateless, thread-safe.
pub trait TokenCounter {
fn count(&self, text: &str) -> usize;
}
/// A compressor. Pure, deterministic, stateless per call.
pub trait Compressor {
fn compress(
&self,
messages: &[Message],
config: &CompressionConfig,
) -> CompressionResult;
}

View File

@@ -1,13 +0,0 @@
[package]
name = "compression-ffi"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
core-api = { workspace = true }
tokenizer = { workspace = true }
# napi-rs bindings are added in the integration phase. This crate exists to
# keep the N-API adapter out of the algorithm crates.

View File

@@ -1,8 +0,0 @@
//! N-API binding crate (integration phase).
//!
//! This crate is intentionally empty until the N-API phase. It will expose
//! `count_tokens` / `compress` over napi-rs using the core-api traits, so the
//! algorithms in `tokenizer` and the future `compression` crates stay free of
//! any Node bindings.
pub use core_api;

View File

@@ -1,11 +0,0 @@
[package]
name = "compression-tests"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[dependencies]
core-api = { workspace = true }
tokenizer = { workspace = true }
serde_json = { workspace = true }

View File

@@ -1,66 +0,0 @@
//! Golden tests: run the Rust implementations against fixtures and compare
//! byte-for-byte with the JS-produced `expected/` files.
//!
//! The `verify-golden.ts` script regenerates fixtures from the OmniRoute JS
//! implementation. Until this crate passes 100% of golden fixtures, the JS
//! implementation must NOT be replaced in production.
use core_api::{Encoding, TokenCounter};
use std::path::Path;
use tokenizer::TiktokenCounter;
const FIXTURES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../fixtures");
fn fixture_path(relative: &str) -> String {
Path::new(FIXTURES_DIR).join(relative).to_string_lossy().into_owned()
}
#[test]
fn tokenizer_golden_cl100k() {
let counter = TiktokenCounter::default();
let dir = fixture_path("tokenizer");
let entries = std::fs::read_dir(&dir).expect("fixtures/tokenizer must exist");
let mut checked = 0;
for entry in entries {
let path = entry.unwrap().path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
let input: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let text = input["text"].as_str().unwrap();
let expected = input["cl100k_tokens"].as_u64().unwrap() as usize;
assert_eq!(
counter.count(text),
expected,
"cl100k mismatch on {}",
path.display()
);
checked += 1;
}
}
assert!(checked > 0, "no tokenizer fixtures found");
}
#[test]
fn tokenizer_golden_o200k() {
let counter = TiktokenCounter::default();
let dir = fixture_path("tokenizer");
let entries = std::fs::read_dir(&dir).expect("fixtures/tokenizer must exist");
let mut checked = 0;
for entry in entries {
let path = entry.unwrap().path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
let input: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
let text = input["text"].as_str().unwrap();
let expected = input["o200k_tokens"].as_u64().unwrap() as usize;
assert_eq!(
counter.count_with_encoding(text, Encoding::O200kBase),
expected,
"o200k mismatch on {}",
path.display()
);
checked += 1;
}
}
assert!(checked > 0, "no tokenizer fixtures found");
}

View File

@@ -1,13 +0,0 @@
[package]
name = "tokenizer"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
core-api = { workspace = true }
tiktoken-rs = { workspace = true }
anyhow = "1"
[dev-dependencies]
serde_json = { workspace = true }

View File

@@ -1,71 +0,0 @@
//! Tiktoken token counter backed by `tiktoken-rs`.
//!
//! Port target: `src/shared/utils/tiktokenCounter.ts` in OmniRoute.
//! Encodings: cl100k_base (default), o200k_base (Codex).
use core_api::{Encoding, TokenCounter};
use tiktoken_rs::tokenizer::Tokenizer;
pub struct TiktokenCounter {
cl100k: tiktoken_rs::CoreBPE,
o200k: tiktoken_rs::CoreBPE,
}
impl TiktokenCounter {
pub fn new() -> Result<Self, anyhow::Error> {
let cl100k = tiktoken_rs::get_bpe_from_tokenizer(Tokenizer::Cl100kBase)?;
let o200k = tiktoken_rs::get_bpe_from_tokenizer(Tokenizer::O200kBase)?;
Ok(Self { cl100k, o200k })
}
pub fn count_with_encoding(&self, text: &str, encoding: Encoding) -> usize {
let bpe = match encoding {
Encoding::Cl100kBase => &self.cl100k,
Encoding::O200kBase => &self.o200k,
};
// CoreBPE::encode_with_special_tokens requires allocation; the
// plain encode is the closest equivalent to the JS byte-pair count.
bpe.encode_ordinary(text).len()
}
}
impl Default for TiktokenCounter {
fn default() -> Self {
Self::new().expect("tiktoken rank tables must load")
}
}
impl TokenCounter for TiktokenCounter {
fn count(&self, text: &str) -> usize {
self.count_with_encoding(text, Encoding::Cl100kBase)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_known_tokens_cl100k() {
let counter = TiktokenCounter::default();
// "Hello world" is 2 tokens in cl100k_base.
assert_eq!(counter.count("Hello world"), 2);
}
#[test]
fn empty_string_is_zero() {
let counter = TiktokenCounter::default();
assert_eq!(counter.count(""), 0);
}
#[test]
fn o200k_differs_from_cl100k_on_emoji() {
let counter = TiktokenCounter::default();
let emoji = "🎉";
let cl100k = counter.count_with_encoding(emoji, Encoding::Cl100kBase);
let o200k = counter.count_with_encoding(emoji, Encoding::O200kBase);
// o200k has dedicated emoji tokens; counts may differ. Just assert both are > 0.
assert!(cl100k > 0);
assert!(o200k > 0);
}
}

View File

@@ -1,80 +0,0 @@
[
{
"id": "000",
"chars": 28,
"cl100k_tokens": 8,
"o200k_tokens": 8
},
{
"id": "001",
"chars": 44,
"cl100k_tokens": 10,
"o200k_tokens": 10
},
{
"id": "002",
"chars": 40,
"cl100k_tokens": 15,
"o200k_tokens": 12
},
{
"id": "003",
"chars": 38,
"cl100k_tokens": 15,
"o200k_tokens": 15
},
{
"id": "004",
"chars": 47,
"cl100k_tokens": 15,
"o200k_tokens": 15
},
{
"id": "005",
"chars": 100,
"cl100k_tokens": 100,
"o200k_tokens": 50
},
{
"id": "006",
"chars": 100,
"cl100k_tokens": 41,
"o200k_tokens": 23
},
{
"id": "007",
"chars": 10000,
"cl100k_tokens": 1250,
"o200k_tokens": 1250
},
{
"id": "008",
"chars": 1,
"cl100k_tokens": 1,
"o200k_tokens": 1
},
{
"id": "009",
"chars": 0,
"cl100k_tokens": 0,
"o200k_tokens": 0
},
{
"id": "010",
"chars": 49,
"cl100k_tokens": 18,
"o200k_tokens": 14
},
{
"id": "011",
"chars": 69,
"cl100k_tokens": 24,
"o200k_tokens": 24
},
{
"id": "012",
"chars": 405000,
"cl100k_tokens": 90001,
"o200k_tokens": 90001
}
]

View File

@@ -1,6 +0,0 @@
{
"id": "000",
"text": "Hello world! This is a test.",
"cl100k_tokens": 8,
"o200k_tokens": 8
}

View File

@@ -1,6 +0,0 @@
{
"id": "001",
"text": "The quick brown fox jumps over the lazy dog.",
"cl100k_tokens": 10,
"o200k_tokens": 10
}

View File

@@ -1,6 +0,0 @@
{
"id": "002",
"text": "🎉🎊 party time! emoji heavy sentence 🚀",
"cl100k_tokens": 15,
"o200k_tokens": 12
}

View File

@@ -1,6 +0,0 @@
{
"id": "003",
"text": "JSON:\n{\"name\":\"test\",\"values\":[1,2,3]}",
"cl100k_tokens": 15,
"o200k_tokens": 15
}

View File

@@ -1,6 +0,0 @@
{
"id": "004",
"text": "Code:\n```rust\nfn main() { println!(\"hi\"); }\n```",
"cl100k_tokens": 15,
"o200k_tokens": 15
}

View File

@@ -1,6 +0,0 @@
{
"id": "005",
"text": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
"cl100k_tokens": 100,
"o200k_tokens": 50
}

View File

@@ -1,6 +0,0 @@
{
"id": "006",
"text": "Поддерживается ли русский текст корректно? Проверяем длинное предложение с кириллицей и пунктуацией!",
"cl100k_tokens": 41,
"o200k_tokens": 23
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
{
"id": "008",
"text": "t",
"cl100k_tokens": 1,
"o200k_tokens": 1
}

View File

@@ -1,6 +0,0 @@
{
"id": "009",
"text": "",
"cl100k_tokens": 0,
"o200k_tokens": 0
}

View File

@@ -1,6 +0,0 @@
{
"id": "010",
"text": "Mixed 🎯 unicode 中文 한국어 + english + numbers 12345",
"cl100k_tokens": 18,
"o200k_tokens": 14
}

View File

@@ -1,6 +0,0 @@
{
"id": "011",
"text": "function foo(a,b){return a+b*2;}\n\nconst x = foo(1,2);\nconsole.log(x);",
"cl100k_tokens": 24,
"o200k_tokens": 24
}

File diff suppressed because one or more lines are too long

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env node
/**
* Generates golden fixtures for compression-core from the OmniRoute JS
* implementation. Every fixture records: input text + expected token counts
* (cl100k / o200k) computed by the JS tokenizer.
*
* Usage: node --import tsx/esm scripts/generate-fixtures.ts
* Output: fixtures/tokenizer/*.json, fixtures/conversations/*.json
*
* The Rust side (crates/tests) reads these and asserts equality. Until 100%
* of fixtures pass, the JS implementation must not be replaced.
*/
import { countTextTokens } from "../../src/shared/utils/tiktokenCounter.ts";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..");
const TOKENIZER_DIR = join(ROOT, "fixtures", "tokenizer");
const EXPECTED_DIR = join(ROOT, "fixtures", "expected");
mkdirSync(TOKENIZER_DIR, { recursive: true });
mkdirSync(EXPECTED_DIR, { recursive: true });
const SAMPLES = [
"Hello world! This is a test.",
"The quick brown fox jumps over the lazy dog.",
"🎉🎊 party time! emoji heavy sentence 🚀",
"JSON:\n{\"name\":\"test\",\"values\":[1,2,3]}",
"Code:\n```rust\nfn main() { println!(\"hi\"); }\n```",
"😀".repeat(50),
"Поддерживается ли русский текст корректно? Проверяем длинное предложение с кириллицей и пунктуацией!",
"a".repeat(10000),
"t".repeat(1),
"",
"Mixed 🎯 unicode 中文 한국어 + english + numbers 12345",
"function foo(a,b){return a+b*2;}\n\nconst x = foo(1,2);\nconsole.log(x);",
];
// A longer realistic conversation-style text (~230K chars) to mirror the
// measured baseline and to stress the counter on large inputs.
const LONG = ("The quick brown fox jumps over the lazy dog. ").repeat(9000);
SAMPLES.push(LONG);
const cl100k = (t) => countTextTokens(t);
const o200k = (t) => countTextTokens(t, { provider: "codex", model: "codex/gpt-5.5" });
let count = 0;
for (const [idx, text] of SAMPLES.entries()) {
const id = String(idx).padStart(3, "0");
const record = {
id,
text,
cl100k_tokens: cl100k(text),
o200k_tokens: o200k(text),
};
writeFileSync(join(TOKENIZER_DIR, `sample-${id}.json`), JSON.stringify(record, null, 2));
count++;
}
// Also emit a combined manifest for quick scanning.
writeFileSync(
join(EXPECTED_DIR, "tokenizer-manifest.json"),
JSON.stringify(
SAMPLES.map((t, idx) => ({
id: String(idx).padStart(3, "0"),
chars: t.length,
cl100k_tokens: cl100k(t),
o200k_tokens: o200k(t),
})),
null,
2
)
);
console.log(`Generated ${count} tokenizer fixtures + manifest in fixtures/`);

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env node
/**
* Verifies golden equivalence between the JS implementation and the Rust
* implementation.
*
* Rust side: runs `cargo test -p compression-tests` which asserts byte-level
* equality against fixtures/expected/. This script:
* 1. regenerates fixtures from the current JS implementation
* 2. runs cargo tests
* 3. reports pass/fail per fixture family
*
* Usage: node scripts/verify-golden.ts [--skip-generate]
*/
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const HERE = dirname(fileURLToPath(import.meta.url));
const CORE_DIR = join(HERE, "..");
const skipGenerate = process.argv.includes("--skip-generate");
if (!skipGenerate) {
console.log("[verify-golden] regenerating fixtures from JS implementation...");
const gen = spawnSync("node", ["--import", "tsx/esm", "scripts/generate-fixtures.ts"], {
cwd: CORE_DIR,
stdio: "inherit",
});
if (gen.status !== 0) {
console.error("FAIL: fixture generation exited with", gen.status);
process.exit(1);
}
}
console.log("[verify-golden] running Rust golden tests...");
const run = spawnSync("cargo", ["test", "-p", "compression-tests"], {
cwd: CORE_DIR,
stdio: "inherit",
});
if (run.status !== 0) {
console.error("FAIL: Rust golden tests exited with", run.status);
process.exit(1);
}
console.log("[verify-golden] ALL GOLDEN TESTS PASSED ✅");

View File

@@ -127,6 +127,12 @@
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
"TS2322": 2
},
"src/app/(dashboard)/dashboard/providers/[id]/components/CustomModelsSection.tsx": {
"TS2739": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover.tsx": {
"TS2304": 5
},
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx": {
"TS2322": 3,
"TS2739": 1,
@@ -141,6 +147,9 @@
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
"TS2503": 1
},
"src/app/(dashboard)/dashboard/providers/[id]/components/__tests__/phase1d.test.tsx": {
"TS2739": 2
},
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
"TS2322": 1
},

View File

@@ -121,8 +121,6 @@
"tailwind-merge",
"tailwindcss",
"tls-client-node",
"turndown",
"turndown-plugin-gfm",
"tsup",
"tsx",
"type-coverage",

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
{
"_rebaseline_2026_07_30_9006_vertex_claude_catalog_dispatch": "PR #9006 (fix/vertex-claude-catalog-dispatch): three files, two causes. (1) src/sse/handlers/chat.ts 1845->1846 (+1): NOT this PR's own growth — this PR never touches chat.ts at all. Measured 1846 (split(\"\\n\").length) at this PR's own merge-base (before any of its 11 commits), so the drift was already inherited from already-merged PRs on release/v3.8.50 (fast-gates PR->release do not run check:file-size, same root cause as _rebaseline_2026_07_25_v3849_basered_filesize and _rebaseline_2026_07_02_5798_release_green) — no offending branch left to fix. (2) src/sse/services/auth.ts 2508->2512 (+4 net, after extraction — see below) and open-sse/handlers/chatCore.ts 5020->5023 (+3, comment-only): genuine own growth. auth.ts adds Vertex 403 PERMISSION_DENIED disambiguation (Google's google.rpc.ErrorInfo proto distinguishes a connection-wide cause — SERVICE_DISABLED, or IAM_PERMISSION_DENIED against a project-level resource — from a model-specific one scoped to a .../models/<id> resource), added mid-PR after a quality-gate reviewer flagged the plan's originally-accepted \"Vertex 403 always -> per-model lockout\" trade-off. The actual classification logic (~40 lines) was EXTRACTED into a new leaf module src/sse/services/vertexErrorClassifier.ts (mirrors the googApiKeyAuth.ts precedent, _rebaseline_2026_07_14_7034_goog_api_key), leaving only the irreducible call-site wiring in the frozen file: a 1-line import plus widening the existing #3027 per-model-403 guard condition. chatCore.ts's +3 is a pure comment expansion (no functional change) clarifying that the adjacent effort-suffix strip is no longer unconditional for every provider, requested by a separate quality-gate code-reviewer finding; not extractable (it's a comment). Auth.ts's disambiguation logic covered by 3 new test cases in tests/unit/vertex-passthrough-model-lockout.test.ts (SERVICE_DISABLED, IAM_PERMISSION_DENIED+model-resource, IAM_PERMISSION_DENIED+project-resource) plus a 4th regression test for a multi-detail-body correlation bug (reason and resource must be read from the SAME ErrorInfo detail, not independently regexed across the whole body) found by an adversarial quality-gate pass and fixed before merge.",
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
@@ -178,7 +177,7 @@
"tests/unit/account-fallback-service.test.ts": 1563,
"tests/unit/batch_api.test.ts": 1324,
"tests/unit/cc-compatible-provider.test.ts": 1217,
"tests/unit/chatcore-translation-paths.test.ts": 2776,
"tests/unit/chatcore-translation-paths.test.ts": 2769,
"tests/unit/chatgpt-web.test.ts": 3148,
"tests/unit/combo-routing-engine.test.ts": 3449,
"tests/unit/db-migration-runner.test.ts": 1499,
@@ -343,14 +342,14 @@
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"open-sse/executors/antigravity.ts": 1528,
"open-sse/executors/base.ts": 1578,
"open-sse/executors/base.ts": 1562,
"open-sse/executors/chatgpt-web.ts": 3241,
"open-sse/executors/codex.ts": 1534,
"open-sse/executors/cursor.ts": 1560,
"open-sse/executors/deepseek-web.ts": 1148,
"open-sse/executors/grok-web.ts": 1044,
"open-sse/executors/muse-spark-web.ts": 1405,
"open-sse/handlers/chatCore.ts": 5023,
"open-sse/handlers/chatCore.ts": 5020,
"open-sse/handlers/imageGeneration.ts": 3101,
"open-sse/handlers/responseSanitizer.ts": 1115,
"open-sse/handlers/search.ts": 1536,
@@ -366,7 +365,7 @@
"open-sse/services/rateLimitManager.ts": 1060,
"open-sse/translator/response/openai-responses.ts": 1174,
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
"open-sse/utils/stream.ts": 2889,
"open-sse/utils/stream.ts": 2887,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1381,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
@@ -401,8 +400,8 @@
"src/shared/components/RequestLoggerV2.tsx": 1629,
"src/shared/components/analytics/charts.tsx": 1035,
"src/shared/services/cliRuntime.ts": 1122,
"src/sse/handlers/chat.ts": 1846,
"src/sse/services/auth.ts": 2512,
"src/sse/handlers/chat.ts": 1845,
"src/sse/services/auth.ts": 2508,
"tests/unit/account-fallback-service.test.ts": 1572,
"tests/unit/provider-validation-specialty.test.ts": 2980,
"open-sse/executors/hyperagent.ts": 1026
@@ -414,6 +413,5 @@
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file.",
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests."
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts."
}

View File

@@ -46,8 +46,6 @@ services:
depends_on:
redis:
condition: service_healthy
chatgpt-web-codex-browser:
condition: service_started
build:
context: .
target: runner-cli
@@ -69,7 +67,6 @@ services:
- HOSTNAME=0.0.0.0
- DATA_DIR=/app/data
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
ports:
- "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
- "${PROD_API_PORT:-20131}:${API_PORT:-20129}"
@@ -83,19 +80,7 @@ services:
retries: 3
start_period: 15s
chatgpt-web-codex-browser:
build:
context: .
dockerfile: docker/chatgpt-web-codex-browser/Dockerfile
image: omniroute:chatgpt-web-codex-browser
restart: unless-stopped
shm_size: "2gb"
volumes:
- chatgpt-web-codex-browser-prod-data:/browser-profile
volumes:
chatgpt-web-codex-browser-prod-data:
name: omniroute-chatgpt-web-codex-browser-prod-data
omniroute-prod-data:
name: omniroute-prod-data
redis-prod-data:

View File

@@ -98,21 +98,6 @@ services:
args:
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
image: omniroute:web
depends_on:
chatgpt-web-codex-browser:
condition: service_started
environment:
- DATA_DIR=/app/data
- PORT=${PORT:-20128}
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
- API_PORT=${API_PORT:-20129}
- API_HOST=${API_HOST:-0.0.0.0}
- LIVE_WS_PORT=${LIVE_WS_PORT:-20132}
- LIVE_WS_HOST=${LIVE_WS_HOST:-0.0.0.0}
- LIVE_WS_ALLOWED_ORIGINS=${LIVE_WS_ALLOWED_ORIGINS:-http://localhost:20128,http://127.0.0.1:20128}
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
ports:
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
- "${API_PORT:-20129}:${API_PORT:-20129}"
@@ -120,20 +105,6 @@ services:
profiles:
- web
# Internal-only Chromium runtime for ChatGPT Web (Codex). No CDP or browser
# UI port is published to the host.
chatgpt-web-codex-browser:
build:
context: .
dockerfile: docker/chatgpt-web-codex-browser/Dockerfile
image: omniroute:chatgpt-web-codex-browser
restart: unless-stopped
shm_size: "2gb"
volumes:
- chatgpt-web-codex-browser-data:/browser-profile
profiles:
- web
# ── Profile: cli (CLIs installed inside container) ─────────────────
omniroute-cli:
<<: *common
@@ -281,8 +252,6 @@ services:
- cliproxyapi
volumes:
chatgpt-web-codex-browser-data:
name: omniroute-chatgpt-web-codex-browser-data
cliproxyapi-data:
name: cliproxyapi-data
redis-data:

View File

@@ -1,10 +0,0 @@
FROM mcr.microsoft.com/playwright:v1.62.0-noble
USER root
RUN mkdir -p /browser-profile && chown -R pwuser:pwuser /browser-profile
COPY --chown=pwuser:pwuser docker/chatgpt-web-codex-browser/cdp-proxy.mjs /opt/cdp-proxy.mjs
USER pwuser
EXPOSE 9223
CMD ["/bin/sh", "-lc", "node /opt/cdp-proxy.mjs & exec $(find /ms-playwright -path '*/chrome-linux/chrome' -type f | head -n 1) --headless=new --no-sandbox --disable-dev-shm-usage --remote-debugging-port=9222 --user-data-dir=/browser-profile about:blank"]

View File

@@ -1,72 +0,0 @@
import http from "node:http";
import net from "node:net";
const listenPort = 9223;
const upstreamHost = "127.0.0.1";
const upstreamPort = 9222;
function proxyHeaders(headers) {
const next = { ...headers, host: `${upstreamHost}:${upstreamPort}` };
delete next.connection;
delete next.upgrade;
return next;
}
const server = http.createServer((request, response) => {
const upstream = http.request(
{
host: upstreamHost,
port: upstreamPort,
method: request.method,
path: request.url,
headers: proxyHeaders(request.headers),
},
(upstreamResponse) => {
const chunks = [];
upstreamResponse.on("data", (chunk) => chunks.push(chunk));
upstreamResponse.on("end", () => {
let body = Buffer.concat(chunks);
const contentType = String(upstreamResponse.headers["content-type"] || "");
if (contentType.includes("application/json")) {
body = Buffer.from(
body
.toString("utf8")
.replaceAll(`ws://${upstreamHost}:${upstreamPort}`, `ws://${request.headers.host}`)
);
}
const headers = { ...upstreamResponse.headers, "content-length": String(body.length) };
response.writeHead(upstreamResponse.statusCode || 502, headers);
response.end(body);
});
}
);
upstream.on("error", () => {
response.writeHead(503, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "CDP browser is starting" }));
});
request.pipe(upstream);
});
server.on("upgrade", (request, socket, head) => {
const upstream = net.connect(upstreamPort, upstreamHost, () => {
const upgradeHeaders = {
...request.headers,
host: `${upstreamHost}:${upstreamPort}`,
connection: "Upgrade",
upgrade: "websocket",
};
const headers = Object.entries(upgradeHeaders)
.flatMap(([name, value]) =>
Array.isArray(value) ? value.map((item) => `${name}: ${item}`) : [`${name}: ${value}`]
)
.join("\r\n");
upstream.write(
`${request.method} ${request.url} HTTP/${request.httpVersion}\r\n${headers}\r\n\r\n`
);
if (head.length > 0) upstream.write(head);
socket.pipe(upstream).pipe(socket);
});
upstream.on("error", () => socket.destroy());
});
server.listen(listenPort, "0.0.0.0");

View File

@@ -1,57 +0,0 @@
FROM node:26.0.0-bookworm-slim
ARG CLAUDE_CODE_VERSION=2.1.220
ARG DEVIN_CLI_VERSION=3000.2.17
ARG TARGETARCH
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl git bash python3 make g++ tini \
&& rm -rf /var/lib/apt/lists/* \
&& npm install --global "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
RUN set -eu; \
case "${TARGETARCH}" in \
amd64) devin_arch=x86_64-unknown-linux; devin_sha=f0e1e9363afc6ee68c4ef87bab4aeb7ff5cc08a5fa838350ef3ceefdbb2a2be2 ;; \
arm64) devin_arch=aarch64-unknown-linux; devin_sha=116dc71ef085a922bc3ff0ea0377d4b26c529a431d58246e36572913e2d25624 ;; \
*) echo "Unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://static.devin.ai/cli/${DEVIN_CLI_VERSION}/devin-${DEVIN_CLI_VERSION}-${devin_arch}.tar.gz" -o /tmp/devin.tar.gz; \
echo "${devin_sha} /tmp/devin.tar.gz" | sha256sum -c -; \
tar -xzf /tmp/devin.tar.gz -C /tmp; \
install -m 0755 "$(find /tmp -type f -name devin | head -1)" /usr/local/bin/devin; \
rm -rf /tmp/devin.tar.gz /tmp/devin-*
RUN groupadd --gid 10001 bridge \
&& useradd --uid 10001 --gid bridge --create-home --home-dir /home/bridge --shell /bin/bash bridge \
&& mkdir -p /opt/omniroute /workspace \
&& chown -R bridge:bridge /opt/omniroute /workspace
WORKDIR /opt/omniroute
USER bridge
COPY --chown=bridge:bridge package.json package-lock.json .npmrc ./
RUN npm ci --ignore-scripts --no-audit --fund=false
COPY --chown=bridge:bridge . .
RUN npm rebuild better-sqlite3 || true
ENV HOME=/home/bridge \
CLAUDE_CONFIG_DIR=/home/bridge/.claude-devin-isolated \
DEVIN_AGENTIC_HOME=/home/bridge \
DATA_DIR=/home/bridge/.omniroute-isolated \
SQLITE_FILE=/home/bridge/.omniroute-isolated/storage.sqlite \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
DISABLE_TELEMETRY=1 \
DISABLE_ERROR_REPORTING=1 \
DISABLE_AUTOUPDATER=1 \
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 \
NEXT_TELEMETRY_DISABLED=1
RUN mkdir -p /home/bridge/.claude-devin-isolated /home/bridge/.local/share/devin \
/home/bridge/.omniroute-isolated
RUN DATA_DIR=/tmp/omniroute-build-data \
SQLITE_FILE=/tmp/omniroute-build-data/storage.sqlite \
npm run build \
&& rm -rf /tmp/omniroute-build-data
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["bash"]

View File

@@ -1,218 +0,0 @@
name: omniroute-devin-bridge
x-isolated-environment: &isolated-environment
HOME: /home/bridge
CLAUDE_CONFIG_DIR: /home/bridge/.claude-devin-isolated
DEVIN_AGENTIC_HOME: /home/bridge
DATA_DIR: /home/bridge/.omniroute-isolated
SQLITE_FILE: /home/bridge/.omniroute-isolated/storage.sqlite
ANTHROPIC_BASE_URL: http://omniroute:20128
ANTHROPIC_AUTH_TOKEN: sk-local-devin-gateway
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
DISABLE_TELEMETRY: "1"
DISABLE_ERROR_REPORTING: "1"
DISABLE_AUTOUPDATER: "1"
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1"
DEVIN_BRIDGE_MODEL: ${DEVIN_BRIDGE_MODEL:-devin-cli-agentic/swe-1-7}
ANTHROPIC_MODEL: ${DEVIN_BRIDGE_MODEL:-devin-cli-agentic/swe-1-7}
ANTHROPIC_DEFAULT_SONNET_MODEL: ${DEVIN_BRIDGE_SONNET_MODEL:-devin-cli-agentic/swe-1-7}
ANTHROPIC_DEFAULT_OPUS_MODEL: ${DEVIN_BRIDGE_OPUS_MODEL:-devin-cli-agentic/swe-1-7}
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${DEVIN_BRIDGE_HAIKU_MODEL:-devin-cli-agentic/swe-1-7}
CLAUDE_CODE_SUBAGENT_MODEL: ${DEVIN_BRIDGE_SUBAGENT_MODEL:-devin-cli-agentic/swe-1-7}
REQUIRE_API_KEY: "true"
OMNIROUTE_API_KEY: sk-local-devin-gateway
x-runtime: &runtime
image: omniroute-devin-bridge:local
build:
context: ../..
dockerfile: docker/devin-bridge/Dockerfile
args:
CLAUDE_CODE_VERSION: 2.1.220
DEVIN_CLI_VERSION: 3000.2.17
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=256m
- /opt/omniroute/.source:rw,nosuid,nodev,size=16m,uid=10001,gid=10001
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
environment: *isolated-environment
networks: [bridge-internal]
services:
omniroute:
<<: *runtime
profiles: [offline]
hostname: omniroute
environment:
<<: *isolated-environment
CLI_DEVIN_AGENTIC_BIN: /opt/omniroute/docker/devin-bridge/mock-devin.mjs
DEVIN_BRIDGE_MOCK_LOG: /evidence/mock-acp.jsonl
command: ["npm", "run", "start"]
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:20128/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 2s
timeout: 2s
retries: 60
volumes:
- omniroute-offline-data:/home/bridge/.omniroute-isolated
- ../../.sandbox/evidence:/evidence
- ./mock-devin.mjs:/opt/omniroute/docker/devin-bridge/mock-devin.mjs:ro
claude:
<<: *runtime
profiles: [offline]
depends_on:
omniroute:
condition: service_healthy
claude-egress-guard:
condition: service_healthy
working_dir: /workspace
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-e2e.sh"]
environment:
<<: *isolated-environment
NODE_USE_ENV_PROXY: "1"
HTTP_PROXY: http://claude-egress-guard:8080
HTTPS_PROXY: http://claude-egress-guard:8080
NO_PROXY: omniroute
volumes:
- claude-isolated-config:/home/bridge/.claude-devin-isolated
- ../../.sandbox/e2e-workspace:/workspace
- ../../.sandbox/evidence:/evidence
- ./run-claude-e2e.sh:/opt/omniroute/docker/devin-bridge/run-claude-e2e.sh:ro
contract:
<<: *runtime
profiles: [offline]
depends_on:
omniroute:
condition: service_healthy
command: ["node", "/opt/omniroute/docker/devin-bridge/run-contract.mjs"]
volumes:
- ./run-contract.mjs:/opt/omniroute/docker/devin-bridge/run-contract.mjs:ro
claude-egress-guard:
image: node:26.0.0-bookworm-slim
profiles: [offline, live-devin]
user: "10001:10001"
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
command: ["node", "/guard/proxy.mjs"]
environment:
GUARD_LISTEN: 0.0.0.0:8080
GUARD_POLICY: deny-all
GUARD_LOG: /guard-audit/egress.jsonl
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('net').connect(8080,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))",
]
interval: 1s
timeout: 1s
retries: 15
volumes:
- ./network-guard:/guard:ro
- ../../.sandbox/guard-audit/claude:/guard-audit
networks: [bridge-internal]
network-guard:
image: node:26.0.0-bookworm-slim
profiles: [live-devin]
user: "10001:10001"
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
command: ["node", "/guard/proxy.mjs"]
environment:
GUARD_LISTEN: 0.0.0.0:8080
GUARD_POLICY: devin
GUARD_LOG: /guard-audit/egress.jsonl
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('net').connect(8080,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))",
]
interval: 1s
timeout: 1s
retries: 15
volumes:
- ./network-guard:/guard:ro
- ../../.sandbox/guard-audit/devin:/guard-audit
networks: [devin-guard-internal, guard-egress]
omniroute-live:
<<: *runtime
profiles: [live-devin]
hostname: omniroute
depends_on:
network-guard:
condition: service_healthy
environment:
<<: *isolated-environment
CLI_DEVIN_AGENTIC_BIN: /usr/local/bin/devin
DEVIN_BRIDGE_PROXY_URL: http://network-guard:8080
networks: [bridge-internal, devin-guard-internal]
command: ["npm", "run", "start"]
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:20128/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 2s
timeout: 2s
retries: 60
volumes:
- devin-auth:/home/bridge/.local/share/devin
- omniroute-live-data:/home/bridge/.omniroute-isolated
claude-live:
<<: *runtime
profiles: [live-devin]
depends_on:
omniroute-live:
condition: service_healthy
claude-egress-guard:
condition: service_healthy
working_dir: /workspace
command: ["bash", "/opt/omniroute/docker/devin-bridge/run-claude-live-e2e.sh"]
environment:
<<: *isolated-environment
NODE_USE_ENV_PROXY: "1"
HTTP_PROXY: http://claude-egress-guard:8080
HTTPS_PROXY: http://claude-egress-guard:8080
NO_PROXY: omniroute
volumes:
- claude-isolated-config:/home/bridge/.claude-devin-isolated
- ../../.sandbox/live-workspace:/workspace
- ../../.sandbox/evidence:/evidence
- ./run-claude-live-e2e.sh:/opt/omniroute/docker/devin-bridge/run-claude-live-e2e.sh:ro
networks:
bridge-internal:
internal: true
devin-guard-internal:
internal: true
guard-egress: {}
volumes:
claude-isolated-config: {}
devin-auth: {}
omniroute-offline-data: {}
omniroute-live-data: {}

View File

@@ -1,229 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs";
import readline from "node:readline";
if (
process.argv[2] !== "acp" ||
process.argv[3] !== "--agent-type" ||
process.argv[4] !== "summarizer" ||
process.argv.length !== 5
) {
process.exit(64);
}
const logFile = process.env.DEVIN_BRIDGE_MOCK_LOG || "/evidence/mock-acp.jsonl";
const rl = readline.createInterface({ input: process.stdin });
const send = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
const log = (value) => fs.appendFileSync(logFile, `${JSON.stringify(value)}\n`);
const actions = [
{
name: "Skill",
arguments: { skill: "bridge-proof" },
},
{
name: "Bash",
arguments: {
command: "find . -maxdepth 2 -type f -print",
description: "Locate the fixture files",
},
},
{
name: "Read",
arguments: { file_path: "/workspace/math.js" },
},
{
name: "Edit",
arguments: {
file_path: "/workspace/math.js",
old_string: "return a - b;",
new_string: "return a * b;",
},
},
{
name: "Bash",
arguments: { command: "npm test", description: "Run the fixture tests" },
},
{
name: "Edit",
arguments: {
file_path: "/workspace/math.js",
old_string: "return a * b;",
new_string: "return a + b;",
},
},
{
name: "Bash",
arguments: { command: "npm test", description: "Confirm the corrected fixture" },
},
];
rl.on("line", (line) => {
const message = JSON.parse(line);
if (message.method === "initialize") {
if (message.params?.protocolVersion !== 1) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32602, message: "ACP v1 required" } });
return;
}
send({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: 1 } });
} else if (message.method === "session/new") {
if (message.params?.cwd !== "/home/bridge" || !Array.isArray(message.params?.mcpServers)) {
send({ jsonrpc: "2.0", id: message.id, error: { code: -32602, message: "unsafe session" } });
return;
}
send({
jsonrpc: "2.0",
id: message.id,
result: { sessionId: "offline" },
});
} else if (message.method === "session/set_config_option") {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32602, message: "summarizer mode must not be mutated" },
});
} else if (message.method === "session/prompt") {
const prompt = String(message.params?.prompt?.[0]?.text || "");
if (!prompt.includes("[Devin Summarizer Bridge]") || !prompt.includes("[Execution Trace]")) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32602, message: "summarizer bridge framing required" },
});
return;
}
if (prompt.includes("CONTRACT_AFTER_TOOL")) {
log({ provider: "devin-cli-agentic", scenario: "after-tool" });
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "offline",
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "contract continued" },
},
},
});
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
return;
}
if (prompt.includes("CONTRACT_EXIT")) {
log({ provider: "devin-cli-agentic", scenario: "exit" });
process.exit(7);
}
if (prompt.includes("CONTRACT_ERROR")) {
log({ provider: "devin-cli-agentic", scenario: "error" });
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32000, message: "deterministic upstream failure" },
});
return;
}
if (prompt.includes("CONTRACT_TEXT")) {
log({ provider: "devin-cli-agentic", scenario: "text" });
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "offline",
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "contract text" },
},
},
});
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
return;
}
if (prompt.includes("CONTRACT_NARRATIVE_REPAIR")) {
const isRepair = prompt.includes("[Single Repair Attempt]");
log({
provider: "devin-cli-agentic",
scenario: "narrative-repair",
stage: isRepair ? "repair" : "initial",
});
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "offline",
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: isRepair
? '<tool>{"name":"Read","arguments":{"file_path":"/workspace/math.js"}}</tool>'
: "I'll start by reading the math.js file, then run the tests.",
},
},
},
});
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
return;
}
if (prompt.includes("CONTRACT_TOOL")) {
log({ provider: "devin-cli-agentic", scenario: "tool" });
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "offline",
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: '<tool>{"name":"Read","arguments":{"file_path":"/workspace/math.js"}}</tool>',
},
},
},
});
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
return;
}
const resultCount = (prompt.match(/\[Tool Result\]/g) || []).length;
if (!prompt.includes("CLAUDE_MD_BRIDGE_ACTIVE") || !prompt.includes("COMMAND_BRIDGE_ACTIVE")) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32602, message: "Claude project context missing" },
});
return;
}
const action = actions[resultCount];
const text = action
? `<tool>${JSON.stringify(action)}</tool>`
: "BRIDGE_E2E_COMPLETE CLAUDE_MD_BRIDGE_ACTIVE SKILL_BRIDGE_ACTIVE COMMAND_BRIDGE_ACTIVE";
if (!action && !prompt.includes("SKILL_BRIDGE_ACTIVE")) {
send({
jsonrpc: "2.0",
id: message.id,
error: { code: -32602, message: "Skill result missing" },
});
return;
}
log({
provider: "devin-cli-agentic",
model: message.params?.model || "swe-1-7",
resultCount,
action: action?.name || "final",
});
const midpoint = Math.max(1, Math.floor(text.length / 2));
for (const chunk of [text.slice(0, midpoint), text.slice(midpoint)]) {
send({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: "offline",
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: chunk },
},
},
});
}
send({ jsonrpc: "2.0", id: message.id, result: { stopReason: "end_turn" } });
}
});

View File

@@ -1,130 +0,0 @@
export const DEVIN_ALLOWED_SUFFIXES = Object.freeze([".devin.ai", ".cognition.ai"]);
export const DEVIN_ALLOWED_EXACT_HOSTS = Object.freeze([
"server.codeium.com",
"unleash.codeium.com",
]);
function normalizeHostname(hostname) {
return String(hostname || "")
.trim()
.toLowerCase()
.replace(/\.$/, "");
}
export function isAllowedGuardHostname(hostname, policy = "deny-all") {
if (policy !== "devin") return false;
const value = normalizeHostname(hostname);
if (!value) return false;
if (DEVIN_ALLOWED_EXACT_HOSTS.includes(value)) return true;
return DEVIN_ALLOWED_SUFFIXES.some(
(suffix) => value === suffix.slice(1) || value.endsWith(suffix)
);
}
const HOP_BY_HOP_HEADERS = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"proxy-connection",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
export function sanitizeForwardHeaders(headers, target) {
const connectionTokens = String(headers.connection || "")
.split(",")
.map((value) => value.trim().toLowerCase())
.filter(Boolean);
const blocked = new Set([...HOP_BY_HOP_HEADERS, ...connectionTokens]);
const sanitized = {};
for (const [name, value] of Object.entries(headers)) {
if (value === undefined || blocked.has(name.toLowerCase()) || name.toLowerCase() === "host") {
continue;
}
sanitized[name] = value;
}
sanitized.host = target.host;
return sanitized;
}
export function parseConnectAuthority(authority) {
const value = String(authority || "");
const match = value.match(/^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/);
if (!match) return null;
const hostname = normalizeHostname(match[1] || match[2]);
const port = Number(match[3]);
if (!hostname || port !== 443) return null;
return { hostname, port };
}
function readUint24(buffer, offset) {
return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2];
}
export function parseTlsClientHelloSni(buffer) {
if (!Buffer.isBuffer(buffer)) return { status: "invalid", reason: "not_buffer" };
let offset = 0;
const handshakeParts = [];
while (offset < buffer.length) {
if (buffer.length - offset < 5) return { status: "need-more" };
if (buffer[offset] !== 22) return { status: "invalid", reason: "not_handshake_record" };
const recordLength = buffer.readUInt16BE(offset + 3);
if (recordLength <= 0 || recordLength > 18432) {
return { status: "invalid", reason: "invalid_record_length" };
}
if (buffer.length - offset - 5 < recordLength) return { status: "need-more" };
handshakeParts.push(buffer.subarray(offset + 5, offset + 5 + recordLength));
offset += 5 + recordLength;
}
const handshake = Buffer.concat(handshakeParts);
if (handshake.length < 4) return { status: "need-more" };
if (handshake[0] !== 1) return { status: "invalid", reason: "not_client_hello" };
const helloLength = readUint24(handshake, 1);
if (helloLength > 65531) return { status: "invalid", reason: "client_hello_too_large" };
if (handshake.length - 4 < helloLength) return { status: "need-more" };
const hello = handshake.subarray(4, 4 + helloLength);
let cursor = 34;
if (hello.length < cursor + 1) return { status: "invalid", reason: "truncated_hello" };
const sessionLength = hello[cursor++];
cursor += sessionLength;
if (hello.length < cursor + 2) return { status: "invalid", reason: "truncated_ciphers" };
const cipherLength = hello.readUInt16BE(cursor);
cursor += 2 + cipherLength;
if (hello.length < cursor + 1) return { status: "invalid", reason: "truncated_compression" };
const compressionLength = hello[cursor++];
cursor += compressionLength;
if (hello.length < cursor + 2) return { status: "invalid", reason: "missing_extensions" };
const extensionsLength = hello.readUInt16BE(cursor);
cursor += 2;
const extensionsEnd = cursor + extensionsLength;
if (extensionsEnd > hello.length) return { status: "invalid", reason: "truncated_extensions" };
while (cursor < extensionsEnd) {
if (extensionsEnd - cursor < 4) return { status: "invalid", reason: "truncated_extension" };
const type = hello.readUInt16BE(cursor);
const length = hello.readUInt16BE(cursor + 2);
cursor += 4;
if (cursor + length > extensionsEnd) {
return { status: "invalid", reason: "invalid_extension_length" };
}
if (type === 0) {
const data = hello.subarray(cursor, cursor + length);
if (data.length < 5 || data.readUInt16BE(0) !== data.length - 2 || data[2] !== 0) {
return { status: "invalid", reason: "invalid_server_name" };
}
const nameLength = data.readUInt16BE(3);
if (nameLength !== data.length - 5) {
return { status: "invalid", reason: "invalid_server_name_length" };
}
const serverName = normalizeHostname(data.subarray(5).toString("ascii"));
if (!/^[a-z0-9.-]+$/.test(serverName)) {
return { status: "invalid", reason: "invalid_server_name_value" };
}
return { status: "ok", serverName };
}
cursor += length;
}
return { status: "invalid", reason: "missing_sni" };
}

View File

@@ -1,136 +0,0 @@
import fs from "node:fs";
import http from "node:http";
import net from "node:net";
import { pathToFileURL } from "node:url";
import {
isAllowedGuardHostname,
parseConnectAuthority,
parseTlsClientHelloSni,
sanitizeForwardHeaders,
} from "./policy.mjs";
const MAX_CLIENT_HELLO_BYTES = 64 * 1024;
const CLIENT_HELLO_TIMEOUT_MS = 3000;
export function createGuardProxy({
policy = "deny-all",
logPath = "/tmp/egress.jsonl",
allowHostname = (hostname) => isAllowedGuardHostname(hostname, policy),
connectSocket = (port, hostname, onConnect) => net.connect(port, hostname, onConnect),
} = {}) {
if (!new Set(["deny-all", "devin"]).has(policy)) {
throw new Error(`Unknown network guard policy: ${policy}`);
}
function audit(hostname, decision, reason) {
fs.appendFileSync(
logPath,
`${JSON.stringify({ at: new Date().toISOString(), hostname, decision, reason })}\n`
);
}
const server = http.createServer((req, res) => {
let target;
try {
target = new URL(req.url);
} catch {
res.writeHead(400).end("invalid proxy target\n");
return;
}
if (target.protocol !== "http:" || target.username || target.password) {
audit(target.hostname, "deny", "invalid_http_target");
res.writeHead(403).end("egress denied\n");
return;
}
if (!allowHostname(target.hostname)) {
audit(target.hostname, "deny", "host_policy");
res.writeHead(403).end("egress denied\n");
return;
}
audit(target.hostname, "allow", "host_policy");
const upstream = http.request(
target,
{
method: req.method,
headers: sanitizeForwardHeaders(req.headers, target),
},
(reply) => {
res.writeHead(reply.statusCode || 502, reply.headers);
reply.pipe(res);
}
);
req.pipe(upstream);
upstream.on("error", () => res.writeHead(502).end("upstream error\n"));
});
server.on("connect", (req, client, head) => {
const authority = parseConnectAuthority(req.url);
if (!authority) {
audit(req.url, "deny", "invalid_connect_authority");
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
return;
}
const { hostname, port } = authority;
if (!allowHostname(hostname)) {
audit(hostname, "deny", "host_policy");
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
return;
}
let buffer = Buffer.from(head);
let settled = false;
const timer = setTimeout(() => fail("client_hello_timeout"), CLIENT_HELLO_TIMEOUT_MS);
timer.unref?.();
const cleanup = () => {
clearTimeout(timer);
client.removeListener("data", onData);
};
const fail = (reason) => {
if (settled) return;
settled = true;
cleanup();
audit(hostname, "deny", reason);
client.destroy();
};
const inspect = () => {
if (buffer.length > MAX_CLIENT_HELLO_BYTES) return fail("client_hello_too_large");
const parsed = parseTlsClientHelloSni(buffer);
if (parsed.status === "need-more") return;
if (parsed.status !== "ok") return fail(parsed.reason || "invalid_client_hello");
if (parsed.serverName !== hostname) return fail("sni_mismatch");
settled = true;
cleanup();
client.pause();
const upstream = connectSocket(port, hostname, () => {
audit(hostname, "allow", "sni_match");
if (buffer.length) upstream.write(buffer);
upstream.pipe(client);
client.pipe(upstream);
client.resume();
});
upstream.on("error", () => client.destroy());
};
const onData = (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
inspect();
};
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
client.on("data", onData);
if (buffer.length) inspect();
client.resume();
});
return server;
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const [host, portText] = (process.env.GUARD_LISTEN || "0.0.0.0:8080").split(":");
const server = createGuardProxy({
policy: process.env.GUARD_POLICY || "deny-all",
logPath: process.env.GUARD_LOG || "/tmp/egress.jsonl",
});
server.listen(Number(portText), host);
}

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BEDROCK_BASE_URL ANTHROPIC_VERTEX_BASE_URL
unset CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY
set -o pipefail
check() {
"$@"
printf 'E2E check passed: %s\n' "$*"
}
claude -p --output-format stream-json --verbose --max-turns 12 \
--permission-mode bypassPermissions \
"/bridge-check" | tee /evidence/claude-stream.jsonl
if grep -Eqi 'log[ -]?in|authenticate.*anthropic|claude\.ai' /evidence/claude-stream.jsonl; then
echo "Claude Code requested forbidden authentication" >&2
exit 1
fi
check grep -q 'return a + b;' /workspace/math.js
npm test
check grep -q 'Skill' /workspace/.e2e-hook.log
check grep -q 'Read' /workspace/.e2e-hook.log
check grep -q 'Edit' /workspace/.e2e-hook.log
check grep -q 'Bash' /workspace/.e2e-hook.log
check grep -q 'BRIDGE_E2E_COMPLETE' /evidence/claude-stream.jsonl

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BEDROCK_BASE_URL ANTHROPIC_VERTEX_BASE_URL
unset CLAUDE_CODE_USE_BEDROCK CLAUDE_CODE_USE_VERTEX CLAUDE_CODE_USE_FOUNDRY
bridge_system_prompt="You are a coding agent inside Claude Code. Use only the client-owned tools supplied in the request. Never execute or request a Devin-owned tool. When work requires a tool, select the appropriate client tool and wait for its result before continuing."
scenario_cooldown_seconds="${DEVIN_BRIDGE_LIVE_SCENARIO_COOLDOWN_SECONDS:-15}"
run_scenario() {
local evidence_file="$1"
local prompt="$2"
claude -p --output-format stream-json --verbose --max-turns 12 \
--tools Read,Edit,Bash \
--system-prompt "$bridge_system_prompt" \
--permission-mode bypassPermissions "$prompt" | tee "$evidence_file"
if grep -Eqi 'log[ -]?in|authenticate.*anthropic|claude\.ai' "$evidence_file"; then
echo "Claude Code requested forbidden authentication" >&2
exit 1
fi
}
validate_scenario() {
local evidence_file="$1"
local marker="$2"
local required_tools="$3"
local require_npm_test="$4"
local required_slash_command="${5:-}"
local required_skill="${6:-}"
local accept_explicit_completion="${7:-false}"
node /opt/omniroute/scripts/devin-bridge/validate-claude-evidence.mjs \
"$evidence_file" "$marker" "$required_tools" "$require_npm_test" \
"$required_slash_command" "$required_skill" "$accept_explicit_completion"
}
run_scenario /evidence/live-analysis.jsonl \
"Read /workspace/CLAUDE.md, /workspace/math.js, and /workspace/math.test.js directly without searching or editing. Explain the defect, then end with LIVE_ANALYSIS_COMPLETE."
validate_scenario /evidence/live-analysis.jsonl LIVE_ANALYSIS_COMPLETE Read false
sleep "$scenario_cooldown_seconds"
run_scenario /evidence/live-fix.jsonl \
"Use Edit now to replace 'return a - b;' with 'return a + b;' in /workspace/math.js. Then use Bash to run npm test. Do not summarize before npm test succeeds. End with LIVE_FIX_COMPLETE only after the test passes."
grep -q 'return a + b;' /workspace/math.js
npm test
validate_scenario /evidence/live-fix.jsonl LIVE_FIX_COMPLETE Edit,Bash true
sleep "$scenario_cooldown_seconds"
run_scenario /evidence/live-command.jsonl "/bridge-check"
validate_scenario /evidence/live-command.jsonl BRIDGE_E2E_COMPLETE Bash true \
bridge-check bridge-proof true
printf 'PASS: three live Devin-backed Claude Code scenarios completed\n'

View File

@@ -1,135 +0,0 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
const endpoint = "http://omniroute:20128/v1/messages";
const headers = {
"anthropic-version": "2023-06-01",
"content-type": "application/json",
"x-api-key": "sk-local-devin-gateway",
};
const model = process.env.DEVIN_BRIDGE_MODEL || "devin-cli-agentic/swe-1-7";
async function request(prompt, extra = {}) {
return fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
model,
max_tokens: 256,
messages: [{ role: "user", content: prompt }],
...extra,
}),
});
}
const textReply = await request("CONTRACT_TEXT");
assert.equal(textReply.status, 200);
assert.match(textReply.headers.get("content-type") || "", /application\/json/);
const textBody = await textReply.json();
assert.equal(textBody.type, "message");
assert.equal(textBody.role, "assistant");
assert.equal(textBody.stop_reason, "end_turn");
assert.deepEqual(textBody.content, [{ type: "text", text: "contract text" }]);
const toolReply = await request("CONTRACT_TOOL", {
stream: true,
tools: [
{
name: "Read",
description: "Read a file",
input_schema: {
type: "object",
properties: { file_path: { type: "string" } },
required: ["file_path"],
additionalProperties: false,
},
},
],
});
assert.equal(toolReply.status, 200);
assert.match(toolReply.headers.get("content-type") || "", /text\/event-stream/);
const toolStream = await toolReply.text();
const eventNames = toolStream
.split("\n")
.filter((line) => line.startsWith("event: "))
.map((line) => line.slice(7));
assert.deepEqual(eventNames, [
"message_start",
"content_block_start",
"content_block_delta",
"content_block_stop",
"message_delta",
"message_stop",
]);
const toolEvents = toolStream
.split("\n")
.filter((line) => line.startsWith("data: "))
.map((line) => JSON.parse(line.slice(6)));
const toolUse = toolEvents.find((event) => event.type === "content_block_start")?.content_block;
assert.equal(toolUse?.type, "tool_use");
assert.equal(toolUse?.name, "Read");
assert.match(toolUse?.id || "", /^tool_devin_/);
const repairedNarrativeReply = await request("CONTRACT_NARRATIVE_REPAIR", {
tools: [
{
name: "Read",
description: "Read a file",
input_schema: {
type: "object",
properties: { file_path: { type: "string" } },
required: ["file_path"],
additionalProperties: false,
},
},
],
});
assert.equal(repairedNarrativeReply.status, 200);
const repairedNarrativeBody = await repairedNarrativeReply.json();
assert.equal(repairedNarrativeBody.stop_reason, "tool_use");
assert.equal(repairedNarrativeBody.content?.[0]?.type, "tool_use");
assert.equal(repairedNarrativeBody.content?.[0]?.name, "Read");
const continuationReply = await fetch(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
model,
max_tokens: 256,
tools: [
{
name: "Read",
description: "Read a file",
input_schema: { type: "object", properties: {}, additionalProperties: true },
},
],
messages: [
{ role: "user", content: "CONTRACT_TOOL" },
{ role: "assistant", content: [toolUse] },
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: "CONTRACT_AFTER_TOOL",
},
],
},
],
}),
});
assert.equal(continuationReply.status, 200);
const continuationBody = await continuationReply.json();
assert.equal(continuationBody.stop_reason, "end_turn");
assert.deepEqual(continuationBody.content, [{ type: "text", text: "contract continued" }]);
for (const marker of ["CONTRACT_ERROR", "CONTRACT_EXIT"]) {
const failedReply = await request(marker);
assert.equal(failedReply.status, 502);
const failedBody = await failedReply.json();
assert.equal(failedBody.error?.type, "server_error");
assert.doesNotMatch(JSON.stringify(failedBody), /stack|anthropic|openai/i);
}
console.log("PASS: Anthropic Messages wire contracts and fail-closed errors passed");

Some files were not shown because too many files have changed in this diff Show More