Compare commits

..

15 Commits

Author SHA1 Message Date
Apostol Apostolov
01c6373bcd fix: rebaseline file-size-baseline + quality ratchet doc
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-04 11:39:01 -03:00
Apostol Apostolov
aa0d99e0a5 test: add structural guard for PR #8916 provider quota compact layout
Adds node:test-based AST checks ensuring:
- QuotaCardGrid compact prop + auto-fill compact grid class survive refactors
- ProviderQuotaWidget compact prop + 3-column responsive grid survive refactors
2026-08-04 11:37:02 -03:00
Apostol Apostolov
3c483dd906 feat: improve provider quota layouts 2026-08-04 11:37:02 -03:00
Diego Rodrigues de Sa e Souza
f11d883f22 fix(cli-tools): enable Apply for compatible providers (#9250)
* fix(cli-tools): resolve models for compatible providers

Keep the CLI tools Apply flow usable when a dynamic OpenAI-compatible or
Anthropic-compatible connection has no static catalog entry. Resolve its
public prefix, connection default model, and prefix-backed catalog entries
before gating the cards.

Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2995

* chore(changelog): fragment for #9250

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: lazysaltyfish <7127935+lazysaltyfish@users.noreply.github.com>
2026-08-04 10:06:37 -03:00
Diego Rodrigues de Sa e Souza
2cb77bbca7 fix(translator): harden Claude format detection for model validation (#9253)
* fix(translator): harden Claude format detection for model validation

Co-authored-by: Ervareza Naurian <rianskp644@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2949

* chore(changelog): fragment for #9253

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: Ervareza Naurian <rianskp644@gmail.com>
2026-08-04 10:06:31 -03:00
Shixi Li
a8216c92fe fix(sse): preserve error-only stream diagnostics (#9022)
* fix(sse): preserve error-only stream diagnostics

* test(ci): register stream readiness mutation coverage

* chore(changelog): finalize PR 9022 fragment
2026-08-04 10:06:25 -03:00
ikelvingo
c790b57af8 fix(translator): pass output_config.effort=max through verbatim (#9053)
The claude->openai translator was unconditionally rewriting max to xhigh, which broke any OpenAI-shape upstream that accepts max literally (e.g. ollama-cloud, opencode-go deepseek, moonshot k3, native Claude). Provider-aware effort policy is owned by sanitizeReasoningEffortForProvider in the executor; the translator should only do form conversion.

Regression guard: tests/unit/base-executor-sanitize-effort.test.ts end-to-end case (claude -> ollama-cloud preserves max).
2026-08-04 10:06:18 -03:00
dependabot[bot]
9acf79f04f chore(deps): bump github/codeql-action from 4 to 4.37.3 (#9082)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v4...v4.37.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 10:06:10 -03:00
dependabot[bot]
09665ab455 chore(deps): bump docker/login-action from 4 to 4.5.2 (#9081)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4 to 4.5.2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4...v4.5.2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-04 10:06:03 -03:00
Bob.Hou
9ee6435f0e fix(classify): recognize Modal 'usage limit reached' as quota exhausted (#9079)
Modal-hosted OpenAI-compatible endpoints (self-hosted Kimi K3 via
Modal free tier) return HTTP 429 with body {"error":"usage limit
reached"} when the account's credit is exhausted. Previously no
QUOTA_PATTERNS regex matched this bare-string error shape, so the 429
fell through to rate_limit (60s short cooldown). Combined with combo
round-robin's per-conversation session stickiness (#3825), this kept
re-targeting the same exhausted connection every turn instead of
locking it out and failing over to an account with remaining credit.

Add a substring pattern matching the JSON key/value pair
"error":"usage limit reached" with tolerance for trailing
punctuation and whitespace. Only the exact "error" key matches;
different keys or qualified transient messages like "Per-minute usage
limit reached" stay classified as rate_limit.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-04 10:05:56 -03:00
Bob.Hou
edd9b0d664 fix(combos): include id column in getCombos query (#8905)
getCombos() SELECT was missing the id column, so returned combo objects
had their id come only from the JSON data blob. If the data blob lacked
an id field, callers (including the Dashboard) saw null — making the
combo appear to have no primary key and impossible to delete.

Add id to the SELECT so the database column value is always available.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-04 10:05:48 -03:00
Jade Guo
ba353aa3d6 docs(db): specify MySQL conformance semantics (#8947)
* docs(db): specify MySQL conformance semantics

* docs(db): deepen MySQL conformance specification

* docs(db): close MySQL conformance gaps
2026-08-04 10:05:41 -03:00
Bob.Hou
224bc0a5a5 docs(guides): add Antigravity (Google One AI) onboarding guide (#8904)
Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-04 10:05:35 -03:00
MumuTW
2e5854906d docs: slim AGENTS.md (#8839) 2026-08-04 10:05:29 -03:00
Will Gordon
2e4268003a fix(ci): merge-queue tolerance for Build (advisory), drops paid-tier batching (#9233)
* fix(ci): restores dast-smoke queue tolerance, drops batching

PR #7329 (an unrelated cliproxy feature PR) silently reverted two prior
Mergify fixes when it touched .mergify.yml from a stale branch:

- #7225's tolerance for the advisory dast-smoke check, which hangs
  recurrently on GitHub-hosted runners (issue #7226) and had been
  dequeuing every queue attempt it touched.
- #7220's removal of batch_size/batch_max_wait_time, which is a paid
  Mergify tier feature this repo's free plan does not have (the queue
  command fails outright with it set).

Restores both fixes verbatim. No PR has used the queue label since
#7329 landed two weeks ago, so this had gone unnoticed.

* fix(ci): restores the auto-enqueue merge_protections_settings block

The first pass of this fix missed a second piece #7329 clobbered in
the same diff hunk: merge_protections_settings.auto_merge_conditions,
the actual mechanism that puts a queue-labeled PR into the queue (the
older rules-based autoqueue path it replaced is EOL). Without it, the
queue label was a no-op even after restoring the check-failure
tolerance and dropping batching.

.mergify.yml now matches commit 9875ccf4e (the last known-good state
before #7329) byte-for-byte, confirmed via sha256.

* fix(ci): retargets queue tolerance from dast-smoke to Build (advisory)

Evidence review found the prior fix's dast-smoke exception is stale:
dast-smoke has failed only twice ever, none since 2026-07-13 (0/30 in
the last ~3.3h across many PRs). Meanwhile Build (advisory), added to
quality.yml 2026-07-27, has a 100% failure rate on every sampled PR
since — confirmed via job logs to be the same class of runner hang
(dies mid "Creating an optimized production build", never a real
compile error), just in a check dast-smoke's tolerance never covered.

Retargets the merge_conditions exception accordingly so the queue can
actually tolerate the failure mode it faces today, instead of one
that's been dormant for weeks.
2026-08-04 08:57:11 -03:00
367 changed files with 5881 additions and 32738 deletions

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

@@ -1839,18 +1839,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 +2300,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

@@ -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) || '' }}
@@ -155,13 +155,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -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) || '' }}
@@ -255,13 +255,13 @@ jobs:
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
uses: docker/login-action@v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -390,7 +390,7 @@ jobs:
- name: Upload Trivy SARIF to Security tab
if: needs.prepare.outputs.version != 'main'
continue-on-error: true
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@v4.37.3
with:
sarif_file: trivy-results.sarif
category: trivy-image

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
@@ -265,7 +279,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 +343,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 +385,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 +396,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 +406,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,7 +437,7 @@ 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
@@ -407,9 +446,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
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
@@ -462,17 +505,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:

5
.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
@@ -210,8 +209,6 @@ scripts/i18n/_pending-keys.json
.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 +245,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

View File

@@ -17,6 +17,13 @@
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
# Auto-enqueue (current Mergify model, 2026): auto_merge_conditions in
# merge_protections_settings — the rules-based queue action / autoqueue path is
# deprecated (EOL 2026-07-16). The owner-applied `queue` label IS the approval.
merge_protections_settings:
auto_merge_conditions:
- label = queue
queue_rules:
- name: release
# Any current or future release branch — the reason GitHub's native queue was
@@ -34,14 +41,26 @@ queue_rules:
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
# approval in this repo's single-maintainer model (see governance header).
merge_conditions:
- "#check-failure=0"
# "Zero failures" — EXCEPT the advisory "Build (advisory)" job (quality.yml):
# continue-on-error by design, and its GH-hosted Turbopack build hangs
# recurrently mid-"Creating an optimized production build" (100% failure rate
# across every sampled PR since the job was added 2026-07-27, always killed by
# a runner timeout/shutdown signal, never a real compile error). Any OTHER
# failure still blocks (anti-fail-open kept). The prior dast-smoke exception
# (#7225) was dropped here: dast-smoke's hang (#7226) has been dormant for
# weeks (0 failures in the last 30 runs; 2 all-time, none since 2026-07-13) —
# carrying its tolerance forward would mask problems it no longer causes.
- or:
- "#check-failure=0"
- and:
- "#check-failure=1"
- check-failure=Build (advisory)
- "#check-pending=0"
- "#check-success>=1"
- check-success=Merge integrity (changelog + generated skills)
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
# don't hold a lone PR hostage waiting for siblings.
batch_size: 10
batch_max_wait_time: 5 min
# NO batching: 'Merge Queue Batch' requires a paid Mergify tier (live finding
# 2026-07-15 — the queue command fails with "Cannot use Merge Queue batch" on
# the free plan). Serial queue (1 PR at a time) still automates the train.
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
merge_method: squash

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,
});

695
AGENTS.md
View File

@@ -1,600 +1,117 @@
# omniroute — Agent Guidelines
# OmniRoute agent guide
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **290 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (104 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.49)**: providers 290 · MCP tools 104 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
## Doc Accuracy Discipline (read before writing any doc)
> **If `grep -rn "name" src/ open-sse/ bin/` returns nothing, the name does not exist. Do not document it.**
The recurring failure mode in AI-generated docs is _plausible-but-unverified specifics_.
Every claim in a `.md` file under `docs/` should be verifiable against the source.
**Rules (enforced by `npm run check:fabricated-docs`):**
1. **Never state an API name, endpoint, path, CLI command, or env var without grepping for it first.**
```bash
grep -rn "theName" src/ open-sse/ bin/
# 0 hits → do not document
```
2. **Never write a line count, file size, migration count, provider count, or strategy count from memory.**
```bash
wc -l <file> # exact line count
ls <dir>/*.ts | wc -l # file count
```
3. **Every code example should be copy-pasted from real usage or actually run** — not synthesized.
Link to a real call site (`path:line`) instead of inventing a signature.
4. **Prefer citing real source (`file.ts:line`) over paraphrasing behavior** — verifiable and self-correcting.
5. **A shorter doc that is 100% accurate beats a comprehensive one with fabrications.**
Wrong docs cost more than missing docs, because people trust and act on them.
The script `scripts/check/check-fabricated-docs.mjs` extracts every route path, env var, hook
name, function name, and file reference from `docs/**/*.md` and verifies each one against the
codebase. Run it locally before pushing docs; it runs in CI via `npm run check:docs-all`.
## Stack
- **Runtime**: Next.js 16 (App Router), Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Language**: TypeScript 6.0 (`src/`) + JavaScript (`open-sse/`, `electron/`)
- **Database**: better-sqlite3 (SQLite) — `DATA_DIR` configurable, default `~/.omniroute/`
- **Streaming**: SSE via `open-sse` internal workspace package
- **Styling**: Tailwind CSS v4
- **i18n**: next-intl with 42 locales (`src/i18n/messages/`) — refresh with `ls src/i18n/messages/*.json | wc -l`
- **Desktop**: Electron (cross-platform: Windows, macOS, Linux)
- **Schemas**: Zod v4 for all API / MCP input validation
---
## Build, Lint, and Test Commands
| Command | Description |
| ----------------------------------- | ------------------------------------------------------------------ |
| `npm run dev` | Start Next.js dev server |
| `npm run build` | Production build: `next build` → `.build/next/` + assemble `dist/` |
| `npm run build:release` | Clean rebuild + HEAD sentinel (`dist/BUILD_SHA`) — use for deploy |
| `npm run start` | Run production build |
| `npm run build:cli` | Build CLI package |
| `npm run lint` | ESLint on all source files |
| `npm run typecheck:core` | TypeScript core type checking |
| `npm run typecheck:noimplicit:core` | Strict checking (no implicit any) |
| `npm run check` | Run lint + test |
| `npm run check:cycles` | Check for circular dependencies |
| `npm run electron:dev` | Run Electron app in dev mode |
| `npm run electron:build` | Build Electron app for current OS |
**Build output layout:**
| Directory | Purpose | Gitignored |
| --------- | -------------------------------------------------- | ---------- |
| `src/` | Application source (TypeScript / TSX) | No |
| `.build/` | Build intermediates (`distDir = .build/next`) | Yes |
| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes |
The pipeline is a single `next build` pass — intermediates land in `.build/next/`, the
assembled bundle in `dist/`. VPS deploys rsync `dist/` into the remote
`/usr/lib/node_modules/omniroute/app/` directory (VPS image path is unchanged).
### Running Tests
```bash
# All tests (unit + vitest + ecosystem + e2e)
npm run test:all
# Single test file (Node.js native test runner — most tests use this)
node --import tsx/esm --test tests/unit/your-file.test.ts
node --import tsx/esm --test tests/unit/plan3-p0.test.ts
node --import tsx/esm --test tests/unit/fixes-p1.test.ts
node --import tsx/esm --test tests/unit/security-fase01.test.ts
# Integration tests
node --import tsx/esm --test tests/integration/*.test.ts
# Vitest (MCP server, autoCombo)
npm run test:vitest
# E2E with Playwright
npm run test:e2e
# Protocol clients E2E (MCP transports, A2A)
npm run test:protocols:e2e
# Ecosystem compatibility tests
npm run test:ecosystem
# Coverage (see CONTRIBUTING.md)
npm run test:coverage
```
**For authoritative coverage requirements, test execution, and PR gates, see [`CONTRIBUTING.md`](CONTRIBUTING.md#running-tests).**
---
## Code Style Guidelines
### Formatting (Prettier — enforced via lint-staged)
2 spaces · semicolons required · double quotes (`"`) · 100 char width · es5 trailing commas.
Always run `prettier --write` on changed files.
### TypeScript
- **Target**: ES2022 · **Module**: `esnext` · **Resolution**: `bundler`
- `strict: false` — prefer explicit types, don't rely on inference
- Path aliases: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
### ESLint Rules
- **Security (error, everywhere)**: `no-eval`, `no-implied-eval`, `no-new-func`
- **Relaxed in `open-sse/` and `tests/`**: `@typescript-eslint/no-explicit-any` = warn
- React hooks rules and `@next/next/no-assign-module-variable` disabled in `open-sse/` and `tests/`
### Naming
| Element | Convention | Example |
| ------------------- | -------------------------------- | ------------------------------------ |
| Files | camelCase / kebab-case | `chatCore.ts`, `tokenHealthCheck.ts` |
| React components | PascalCase | `Dashboard.tsx`, `ProviderCard.tsx` |
| Functions/variables | camelCase | `getHealth()`, `switchCombo()` |
| Constants | UPPER_SNAKE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
| Interfaces | PascalCase (`I` prefix optional) | `ProviderConfig` |
| Enums | PascalCase (members too) | `LogLevel.Error` |
### Imports
- **Order**: external → internal (`@/`, `@omniroute/open-sse`) → relative (`./`, `../`)
- **No barrel imports** from `localDb.ts` — import from the specific `db/` module instead
### Error Handling
- try/catch with specific error types; always log with context (pino logger)
- Never silently swallow errors in SSE streams — use abort signals for cleanup
- Return proper HTTP status codes (4xx client, 5xx server)
### Security
- **NEVER** commit API keys, secrets, or credentials
- Validate all user inputs with Zod schemas
- Auth middleware required on all API routes
- Never log SQLite encryption keys
- Sanitize user content (dompurify for HTML)
- **Public upstream OAuth identifiers** (Gemini / Antigravity / Windsurf-style client_id/secret + Firebase Web keys extracted from public CLIs): use `resolvePublicCred()` from `open-sse/utils/publicCreds.ts`, **never** as string literals. Full pattern in `docs/security/PUBLIC_CREDS.md`.
- **Error responses** (HTTP / SSE / executor / MCP): use `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts`, **never** put raw `err.stack` / `err.message` in a Response body. Full pattern in `docs/security/ERROR_SANITIZATION.md`.
- **`exec()` / `spawn()` with runtime values**: pass via the `env` option, **never** string-interpolate paths/values into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
- Prefer secure-by-default libraries when available — see [tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults) for the curated list (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink, etc.).
---
## Architecture
### Data Layer (`src/lib/db/`)
All persistence uses SQLite through **95 domain-specific modules** in `src/lib/db/`. Top modules:
- Core: `core.ts`, `migrationRunner.ts`, `encryption.ts`, `stateReset.ts`
- Providers / catalog: `providers.ts`, `models.ts`, `providerLimits.ts`, `compressionAnalytics.ts`
- Routing: `combos.ts`, `modelComboMappings.ts`, `domainState.ts`, `commandCodeAuth.ts`
- Auth: `apiKeys.ts`, `secrets.ts`, `registeredKeys.ts`, `sessionAccountAffinity.ts`
- Usage / billing: `quotaSnapshots.ts`, `creditBalance.ts`, `usage*.ts`, `compressionCacheStats.ts`
- Storage: `backup.ts`, `cleanup.ts`, `jsonMigration.ts`, `healthCheck.ts`, `databaseSettings.ts`
- Extension modules: `evals.ts`, `webhooks.ts`, `reasoningCache.ts`, `readCache.ts`, `tierConfig.ts`, `compressionCombos.ts`, `compressionScheduler.ts`, `batches.ts`, `files.ts`, `syncTokens.ts`, `proxies.ts`, `oneproxy.ts`, `upstreamProxy.ts`, `versionManager.ts`, `cliToolState.ts`, `prompts.ts`, `detailedLogs.ts`, `contextHandoffs.ts`, `compression.ts`, `stats.ts`
Live count: `ls src/lib/db/*.ts | wc -l` (currently 95). Drift detection: `npm run check:docs-counts`.
Schema migrations live in `db/migrations/` (**110 files** as of v3.8.43) and run via `migrationRunner.ts`.
`src/lib/localDb.ts` is a **re-export layer only** — never add logic there.
#### DB Internals
- **`core.ts`**: `getDbInstance()` returns a singleton `better-sqlite3` instance with WAL
journaling. `SCHEMA_SQL` defines **17 base tables** (verify with `grep -c "CREATE TABLE" src/lib/db/core.ts` minus 1 for the bookkeeping `_omniroute_migrations` table). Helpers: `rowToCamel`, `encryptConnectionFields`.
- **`migrationRunner.ts`**: Applies versioned SQL files from `db/migrations/` inside transactions.
Tracks applied migrations in `_omniroute_migrations` table.
- **Migrations**: 110 files (`001_initial_schema.sql` → `110_*.sql`).
Each migration is idempotent and runs in a transaction. Live count: `ls src/lib/db/migrations/*.sql | wc -l`.
- **Domain modules** import `getDbInstance()` from `core.ts` for all CRUD operations.
Each module owns a specific table/set of tables (e.g., `providers.ts` → `provider_connections`,
`combos.ts` → `combos`). Encryption helpers protect sensitive fields at rest.
- **`localDb.ts`** re-exports all domain modules — consumers import from here for convenience.
### API Route Layer (`src/app/api/v1/`)
Next.js App Router routes — each follows a consistent pattern:
```
Route → CORS preflight → Body validation (Zod) → Optional auth (extractApiKey/isValidApiKey)
→ API key policy enforcement (enforceApiKeyPolicy) → Handler delegation (open-sse)
```
| Route | Handler | Notes |
| ------------------------------- | ------------------------- | ------------------------------------------------------------- |
| `chat/completions/route.ts` | `handleChat()` | + prompt injection guard (clones request) |
| `responses/route.ts` | `handleChat()` (unified) | Responses API format |
| `embeddings/route.ts` | `handleEmbedding()` | Model listing + creation |
| `images/generations/route.ts` | `handleImageGeneration()` | Model listing + creation |
| `audio/transcriptions/route.ts` | audio handler | Multipart form data |
| `audio/speech/route.ts` | TTS handler | Binary audio response |
| `videos/generations/route.ts` | video handler | ComfyUI/SD WebUI |
| `music/generations/route.ts` | music handler | ComfyUI workflows |
| `moderations/route.ts` | moderation handler | Content safety |
| `rerank/route.ts` | rerank handler | Document relevance |
| `search/route.ts` | search handler | Web search (12 providers per `open-sse/handlers/search.ts:6`) |
**No global Next.js middleware file** — interception is route-specific. Auth is optional
(controlled by `REQUIRE_API_KEY` env). Prompt injection guard is unique to chat completions.
### Request Pipeline (`open-sse/`)
The `open-sse/` workspace is the core streaming engine. Full request flow:
```
Client Request
→ src/app/api/v1/.../route.ts (Next.js route)
→ open-sse/handlers/chatCore.ts::handleChatCore()
→ Semantic/signature cache check
→ Rate limit check (rateLimitManager)
→ Combo routing? → open-sse/services/combo.ts::handleComboChat()
→ resolveComboTargets() → ordered ResolvedComboTarget[]
→ For each target: handleSingleModel() (wraps chatCore)
→ translateRequest() (open-sse/translator/)
→ Convert source format (e.g., OpenAI) → target format (e.g., Claude)
→ getExecutor() → provider-specific executor instance
→ executor.execute() (BaseExecutor → DefaultExecutor or provider-specific)
→ buildUrl() + buildHeaders() + transformRequest()
→ fetch() to upstream provider
→ Retry logic with exponential backoff
→ Response translation back to client format
→ If Responses API: responsesTransformer.ts TransformStream
→ SSE stream or JSON response to client
```
**Handlers** (`open-sse/handlers/`): `chatCore.ts`, `responsesHandler.ts`, `embeddings.ts`,
`imageGeneration.ts`, `videoGeneration.ts`, `musicGeneration.ts`, `audioSpeech.ts`,
`audioTranscription.ts`, `moderations.ts`, `rerank.ts`, `search.ts`.
**Upstream headers**: merged after default auth; same header name replaces executor value.
**T5 intra-family fallback** recomputes headers using only the fallback model id.
Forbidden header names: `src/shared/constants/upstreamHeaders.ts` — keep sanitize,
Zod schemas, and unit tests aligned when editing.
### Provider Categories
- **Free** (2): Qoder AI, Kiro AI
- **OAuth** (13): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf (v3.8), GitLab Duo (v3.8)
- **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Perplexity,
Together, Fireworks, Cerebras, Cohere, NVIDIA, Nebius, SiliconFlow, Hyperbolic,
HuggingFace, OpenRouter, Vertex AI, Cloudflare AI, Scaleway, AI/ML API, Pollinations,
Puter, Longcat, Alibaba, Kimi, Minimax, Blackbox, Synthetic, Kilo Gateway,
Z.AI, GLM, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld,
NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper, Brave, Exa,
Tavily, OpenCode Zen/Go, Bailian Coding Plan, DeepInfra, Vercel AI Gateway,
Lambda AI, SambaNova, nScale, OVHcloud AI, Baseten, PublicAI, Moonshot AI,
Meta Llama API, v0 (Vercel), Morph, Featherless AI, FriendliAI, LlamaGate,
Galadriel, Weights & Biases Inference, Volcengine, AI21 Labs, Venice.ai,
Codestral, Upstage, Maritalk, Xiaomi MiMo, Inference.net, NanoGPT, Predibase,
Bytez, Heroku AI, Databricks, Snowflake Cortex, GigaChat (Sber), CrofAI,
AgentRouter, ChatGPT Web, Baidu Qianfan, AWS Polly, RunwayML, GitLab Duo,
Amazon Q, Empower, Poe, and many more.
- **Self-Hosted** (8+): LM Studio, vLLM, Lemonade, Llamafile, Triton, Docker Model Runner, Xinference, Oobabooga
- **Custom**: OpenAI-compatible (`openai-compatible-*`) and Anthropic-compatible (`anthropic-compatible-*`) prefixes
Providers are registered in `src/shared/constants/providers.ts` with Zod validation at module load.
### Executors (`open-sse/executors/`)
Provider-specific request executors: `base.ts`, `default.ts`, `cursor.ts`, `codex.ts`,
`antigravity.ts`, `github.ts`, `kiro.ts`, `qoder.ts`, `vertex.ts`,
`cloudflare-ai.ts`, `opencode.ts`, `pollinations.ts`, `puter.ts`.
#### Executor Internals
- **`base.ts`** (`BaseExecutor`): Abstract base with `buildUrl()`, `buildHeaders()`,
`transformRequest()`, retry logic (exponential backoff), and `execute()`. Subclasses
override URL/header/transform methods for provider-specific behavior.
- **`default.ts`** (`DefaultExecutor extends BaseExecutor`): Handles most OpenAI-compatible
providers. Reads provider config from `providerRegistry.ts` to resolve base URL, auth
header format, and request transformations.
- **`getExecutor()`** (`executors/index.ts`): Factory that returns the correct executor
instance based on provider ID. Provider-specific executors (Cursor, Codex, Vertex, etc.)
override only what differs from the default.
### Translator (`open-sse/translator/`)
Translates between API formats (OpenAI-format ↔ Anthropic, Gemini, etc.).
Includes request/response translators with helpers for image handling.
#### Translator Internals
- **`translator/index.ts`**: Exports `translateRequest()` and format constants. Called by
`chatCore.ts` before executor dispatch.
- **Flow**: `translateRequest(body, sourceFormat, targetFormat)` → detects source format
(OpenAI, Anthropic, Gemini) → applies the matching translator module → returns
transformed body ready for the target provider.
- **Response translation** runs in reverse after upstream response, converting back to
the client's expected format.
### Transformer (`open-sse/transformer/`)
`responsesTransformer.ts` — transforms Responses API format to/from Chat Completions format.
#### Transformer Internals
- **`createResponsesApiTransformStream()`**: Returns a `TransformStream` that converts
Chat Completions SSE chunks (`data: {"choices":[...]}`) into Responses API SSE events
(`response.output_item.added`, `response.output_text.delta`, etc.).
- Used when the client sends a Responses API request: the request is internally converted
to Chat Completions format, dispatched normally, and the response is piped through this
transform stream before reaching the client.
### Services (`open-sse/services/`)
134 service modules in `open-sse/services/` (top-level only; more including sub-dirs like `autoCombo/` and `compression/`). Refresh: `ls open-sse/services/*.ts | wc -l`. Key modules:
`combo.ts` (routing engine), `usage.ts`, `tokenRefresh.ts`,
`rateLimitManager.ts`, `accountFallback.ts`, `sessionManager.ts`, `wildcardRouter.ts`,
`autoCombo/`, `intentClassifier.ts`, `taskAwareRouter.ts`, `thinkingBudget.ts`,
`contextManager.ts`, `modelDeprecation.ts`, `modelFamilyFallback.ts`,
`emergencyFallback.ts`, `workflowFSM.ts`, `backgroundTaskDetector.ts`, `ipFilter.ts`,
`signatureCache.ts`, `volumeDetector.ts`, `contextHandoff.ts`, `compression/` (prompt
compression pipeline), and more.
#### Prompt Compression Pipeline (`compression/`)
Modular prompt compression that runs proactively before the existing reactive context manager.
- **`strategySelector.ts`**: Selects compression mode based on config, compression combo assignments,
combo overrides, auto-trigger thresholds, and defaults. Priority: assigned compression combo >
combo override > auto-trigger > default mode > off.
- **`lite.ts`**: 5 lite-mode techniques: `collapseWhitespace`, `dedupSystemPrompt`,
`compressToolResults`, `removeRedundantContent`, `replaceImageUrls`. Target: 10-15% savings at
<1ms latency.
- **`caveman.ts` / `cavemanRules.ts`**: Caveman-style semantic condensation backed by built-in
rules plus file-loaded language packs under `compression/rules/`.
- **`engines/rtk/`**: Rule-based terminal/tool-output compression inspired by RTK patterns. Detects
command output classes, applies JSON filter packs, deduplicates repeated lines, strips ANSI/code
noise, and preserves errors/actionable context. The RTK JSON DSL supports replace,
match-output short-circuit, strip/keep, per-line truncation, head/tail/max-line truncation,
inline tests, trust-gated project/global custom filters, and optional redacted raw-output
retention for authenticated recovery.
- **`engines/registry.ts`**: Registers engines (`caveman`, `rtk`) and powers stacked pipelines.
- **`stats.ts`**: Per-request compression stats tracking (original tokens, compressed tokens,
savings %, techniques used, engine breakdown, compression combo id).
- **`types.ts`**: `CompressionMode` (off/lite/standard/aggressive/ultra/rtk/stacked),
`CompressionConfig`, `CompressionStats`, `CompressionResult`.
- DB settings in `src/lib/db/compression.ts`, compression combos in
`src/lib/db/compressionCombos.ts`, API routes under `src/app/api/settings/compression/`,
`src/app/api/context/*`, and preview/language-pack routes under `src/app/api/compression/*`.
#### Combo Routing Engine (`combo.ts`)
- **`handleComboChat()`**: Entry point for combo-routed requests. Receives the combo config
and iterates through targets in order until one succeeds or all fail.
- **`resolveComboTargets()`**: Expands a combo configuration into an ordered array of
`ResolvedComboTarget[]`, each specifying provider + model + account + credentials.
- **Strategies** (17): priority, weighted, fill-first, round-robin, P2C, random, least-used, reset-aware (v3.8),
reset-window, cost-optimized, strict-random, auto, lkgp, context-optimized, context-relay, headroom, fusion. Source: `ROUTING_STRATEGY_VALUES` in `src/shared/constants/routingStrategies.ts`.
- Each target calls **`handleSingleModel()`** which wraps `handleChatCore()` with
per-target error handling and circuit breaker checks.
### Domain Layer (`src/domain/`)
Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
`degradation.ts`, `fallbackPolicy.ts`, `lockoutPolicy.ts`, `modelAvailability.ts`,
`providerExpiration.ts`, `quotaCache.ts`, `responses.ts`, `configAudit.ts`.
### MCP Server (`open-sse/mcp-server/`)
**104 tools** total (`TOTAL_MCP_TOOL_COUNT`, `open-sse/mcp-server/server.ts`): a 42-entry base registry (`MCP_TOOLS` in `schemas/tools.ts`, bundling the core / cache / compression / 1proxy / advanced tools) **plus** standalone module sets — memory (3), skill (4), agentSkill (3), pool (6), gamification (8), plugin (8), notion (6), obsidian (22). 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (31 scopes — see `OMNIROUTE_MCP_SCOPES`), Zod schemas. See [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md).
**Core tools** (20): get_health, list_combos, get_combo_metrics, switch_combo, check_quota,
route_request, cost_report, list_models_catalog, web_search, simulate_route, set_budget_guard,
set_routing_strategy, set_resilience_profile, test_combo, get_provider_metrics,
best_combo_for_task, explain_route, get_session_snapshot, db_health_check, sync_pricing.
**Cache tools** (2): cache_stats, cache_flush.
**Compression tools** (5): compression_status, compression_configure, set_compression_engine,
list_compression_combos, compression_combo_stats.
**1proxy tools** (3): oneproxy_fetch, oneproxy_rotate, oneproxy_stats.
**Memory tools** (3): memory_search, memory_add, memory_clear.
**Skill tools** (4): skills_list, skills_enable, skills_execute, skills_executions.
**Agent-skill tools** (3): A2A skill discovery / invocation bridges.
**Gamification tools** (8): levels, badges, leaderboard, and community-federation queries.
**Plugin tools** (8): plugin marketplace listing, install/enable/disable, and runtime inspection.
**Notion tools** (6) + **Obsidian tools** (22): knowledge-base read/write integrations (the largest tool family — vault search, note CRUD, WebDAV-backed file ops).
#### MCP Internals
- **Tool registration**: Each tool is an object with `{ name, description, inputSchema: ZodSchema,
handler: async (args) => {...} }`. Zod validates inputs before the handler fires.
- **`createMcpServer()`** and **`startMcpStdio()`** exported from `mcp-server/index.ts`.
`createMcpServer()` wires all tool sets; `startMcpStdio()` launches the stdio transport.
- **Transports**: stdio (CLI `omniroute --mcp`), SSE (`/api/mcp/sse`), Streamable HTTP
(`/api/mcp/stream`). All share the same tool/scope engine.
- **Scopes** (30): Control which tool categories an API key can access. Enforcement happens
before handler dispatch.
- **Audit**: Every tool invocation is logged to SQLite (`mcp_audit` table) with tool name,
args, success/failure, API key attribution, and timestamp.
### A2A Server (`src/lib/a2a/`)
JSON-RPC 2.0, SSE streaming, Task Manager with TTL cleanup.
Agent Card at `/.well-known/agent.json`.
Skills (6): `smartRouting.ts`, `quotaManagement.ts`, `providerDiscovery.ts`, `costAnalysis.ts`, `healthReport.ts`, `listCapabilities.ts`.
#### A2A Internals
- **`taskManager.ts`**: State machine lifecycle for tasks: `submitted → working →
completed | failed | canceled`. Tasks have TTL and are cleaned up automatically.
- **JSON-RPC methods**: `message/send` (sync), `message/stream` (SSE), `tasks/get`,
`tasks/cancel`. Dispatched via `POST /a2a`.
- **Skills**: Registered in a DB-backed registry. Each skill receives task context
(messages, metadata) and returns structured results. `quotaManagement.ts` summarizes
quota; `smartRouting.ts` recommends routing decisions.
- **Agent Card**: `/.well-known/agent.json` exposes capabilities, skills, and metadata
for client auto-discovery.
### ACP Module (`src/lib/acp/`)
Agent Communication Protocol registry and manager.
### Memory System (`src/lib/memory/`)
Extraction, injection, retrieval, summarization, and store modules for persistent
conversational memory across sessions.
### Skills System (`src/lib/skills/`)
Extensible skill framework: registry, executor, sandbox, built-in skills,
custom skill support, interception, and injection.
#### Skills Internals
- **`registry.ts`**: DB-backed skill registration and discovery. Skills have metadata
(name, description, version, enabled status) stored in SQLite.
- **`executor.ts`**: Execution engine with configurable timeout and retry logic.
Receives skill name + input, looks up the skill, runs it in the sandbox.
- **`sandbox.ts`**: Isolation layer for custom (user-provided) skills. Limits resource
access and execution time.
- **Built-in skills**: Ship with OmniRoute (e.g., quota management, routing). Located
alongside the registry.
- **Interception/Injection**: Skills can intercept requests in the pipeline (pre/post
processing) or inject context into prompts.
### Compliance (`src/lib/compliance/`)
Policy index for compliance enforcement.
### MITM Proxy (`src/mitm/`)
MITM proxy capability with certificate management, DNS handling, and target routing.
### Middleware (`src/middleware/`)
Request middleware including `promptInjectionGuard.ts`.
### Guardrails (`src/lib/guardrails/`)
Hot-reloadable guardrails framework (3 built-in: pii-masker, prompt-injection, vision-bridge). Fail-open. The `pii-masker` guardrail is registered and runs on every request, but its data-mutating logic is **opt-in** and OFF by default — it only redacts when `PII_REDACTION_ENABLED` (request) / `PII_RESPONSE_SANITIZATION` (response + streaming) are enabled (both `defaultValue: "false"`); with them off, payloads pass through untouched. A request can additionally opt OUT of any guardrail via header (`x-omniroute-disabled-guardrails`). Never make PII default-on (Hard Rule #20). See [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md).
### Cloud Agents (`src/lib/cloudAgent/`)
`CloudAgentBase` abstract class + 3 agents (codex-cloud, devin, jules). Tasks persisted in `cloud_agent_tasks`; management auth required. See [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md).
### Evals (`src/lib/evals/`)
Generic eval framework: `evalRunner.ts`, `runtime.ts`. Targets: combo / model / suite-default. See [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md).
### Webhooks (`src/lib/webhookDispatcher.ts`)
HMAC-signed delivery, exponential backoff, auto-disable after 10 failures. 7 event types. See [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md).
### Authorization Pipeline (`src/server/authz/`)
`classify → policies → enforce`. 3 route classes (PUBLIC / CLIENT_API / MANAGEMENT). See [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md).
### Reasoning Replay (`src/lib/db/reasoningCache.ts` + `open-sse/services/reasoningCache.ts`)
Hybrid in-memory + SQLite cache for `reasoning_content`. Re-injects on multi-turn for strict providers (DeepSeek V4, Kimi K2, Qwen-Thinking, GLM, xiaomi-mimo). See [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md).
### Tunnels (`src/lib/{cloudflaredTunnel,ngrokTunnel}.ts` + `src/app/api/tunnels/`)
Cloudflare Quick/Named, ngrok, Tailscale Funnel. See [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md).
### Adding a New Provider
1. Register in `src/shared/constants/providers.ts`
2. Add executor in `open-sse/executors/` (if custom logic needed)
3. Add translator in `open-sse/translator/` (if non-OpenAI format)
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` (if OAuth-based)
5. Add models in `open-sse/config/providerRegistry.ts`
---
## Subdirectory AGENTS.md Files
- **[`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md)** — SQLite persistence, domain modules, migrations
- **[`open-sse/services/AGENTS.md`](open-sse/services/AGENTS.md)** — Routing engine, combo resolution, strategy selection
## Reference Documentation (docs/)
For any non-trivial change, read the matching deep-dive first:
| Area | Doc |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 18 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Cloud agents | [`docs/frameworks/CLOUD_AGENT.md`](docs/frameworks/CLOUD_AGENT.md) |
| Guardrails | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md) |
| Evals | [`docs/frameworks/EVALS.md`](docs/frameworks/EVALS.md) |
| Compliance | [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md) |
| Webhooks | [`docs/frameworks/WEBHOOKS.md`](docs/frameworks/WEBHOOKS.md) |
| Authz | [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Stealth | [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Reasoning replay | [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Agent protocols (A2A / ACP / Cloud) | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md) |
| MCP server | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md) |
| A2A server | [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| API reference | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) + [`docs/openapi.yaml`](docs/openapi.yaml) |
| Provider catalog (auto-generated) | [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md) |
| Tunnels | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md) |
| Electron desktop | [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
| Release flow | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md) |
| Quality gates (35 gates, allowlist policy) | [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
| Cluster opt-in profiles (memory, bifrost) | [`docs/architecture/cluster-decisions.md`](docs/architecture/cluster-decisions.md) |
---
## Fork / Upstream Workflow
This repository is a fork of `diegosouzapw/OmniRoute`. Keep fork-only operational
changes (for example GHCR image publishing, personal deployment workflows, or local
automation) out of upstream contribution PRs.
When preparing a PR for upstream, always start the work branch from the upstream
**default branch** — the active `release/vX.Y.Z` line (today `release/v3.8.49`).
Never branch from `main`: `main` only receives release squash-merges, so a branch
cut there is weeks behind and produces conflict-heavy PRs
(see `CONTRIBUTING.md` and `docs/ops/BRANCHING_MODEL.md`):
OmniRoute is a unified AI proxy/router. The repository contains the Next.js application
(`src/`), streaming engine workspace (`open-sse/`), Electron desktop app (`electron/`),
CLI (`bin/`), and tests (`tests/`).
## Setup and focused checks
- Runtime: Node.js `>=22.22.3 <23` or `>=24.0.0 <27`; npm 10+.
- Install dependencies: `npm install`.
- Start development: `npm run dev`.
- Build: `npm run build`; release build: `npm run build:release`.
- Lint: `npm run lint`.
- Core type check: `npm run typecheck:core`.
- Run the most focused test for changed code first:
`node --import tsx/esm --test tests/unit/<file>.test.ts`.
- Other suites: `npm run test:vitest`, `npm run test:e2e`,
`npm run test:protocols:e2e`, and `npm run test:ecosystem`.
- Run `npm run check:docs-all` after changing documentation.
For the complete test matrix, coverage requirements, and pull-request gates, read
[`CONTRIBUTING.md`](CONTRIBUTING.md#running-tests).
## Documentation accuracy
Documentation must describe verified behavior, not plausible behavior.
1. Before documenting an API name, endpoint, path, CLI command, or environment variable,
search for it: `rg -n "name" src/ open-sse/ bin/`. If it has no source match, do not
document it.
2. Measure mutable counts instead of writing them from memory: use `wc -l <file>` or a
directory-specific count command.
3. Copy code examples from working usage or run them. Prefer a source link such as
`path/to/file.ts:line` to an invented signature.
4. Run `npm run check:docs-all` for edits under `docs/`; it includes the fabricated-docs
validation.
## Code conventions
- Format with Prettier: two spaces, semicolons, double quotes, 100-character line width,
and ES5 trailing commas. Run Prettier on changed files.
- TypeScript target is ES2022 with bundler module resolution. Prefer explicit types.
- Import order: external, internal (`@/` and `@omniroute/open-sse`), then relative.
- Do not add logic to `src/lib/localDb.ts`; import from the owning `src/lib/db/` module.
- Use specific errors and contextual logging. Do not silently swallow SSE-stream failures;
use abort signals for cleanup and return appropriate HTTP status codes.
## Security requirements
- Never commit credentials or log SQLite encryption keys.
- Validate API inputs with Zod and use the route's required authentication path.
- Sanitize user HTML with DOMPurify.
- Use `resolvePublicCred()` for public upstream OAuth identifiers; never add them as string
literals. See [`docs/security/PUBLIC_CREDS.md`](docs/security/PUBLIC_CREDS.md).
- Use `buildErrorBody()` or `sanitizeErrorMessage()` for HTTP, SSE, executor, and MCP errors;
do not return raw `err.stack` or `err.message`. See
[`docs/security/ERROR_SANITIZATION.md`](docs/security/ERROR_SANITIZATION.md).
- Pass runtime values to `exec()` or `spawn()` through `env`, not interpolation into a script.
## Repository map
Read the nearest `AGENTS.md` and the linked deep-dive before making a non-trivial change.
| Area | Location | Start here |
| ---------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| API routes | `src/app/api/v1/` | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Streaming request handling | `open-sse/handlers/` | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Provider execution and translation | `open-sse/executors/`, `open-sse/translator/` | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Routing and resilience | `open-sse/services/` | [`open-sse/services/AGENTS.md`](open-sse/services/AGENTS.md), [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Database and migrations | `src/lib/db/`, `db/migrations/` | [`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md) |
| Domain policy | `src/domain/` | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| MCP and A2A | `open-sse/mcp-server/`, `src/lib/a2a/` | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md), [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
| Agent features | `src/lib/{acp,memory,skills,cloudAgent}/` | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md), [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Safety and governance | `src/lib/{guardrails,compliance}/`, `src/server/authz/` | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md), [`docs/architecture/AUTHZ_GUIDE.md`](docs/architecture/AUTHZ_GUIDE.md) |
| Operations | `src/mitm/`, tunnel modules, `electron/` | [`docs/ops/TUNNELS_GUIDE.md`](docs/ops/TUNNELS_GUIDE.md), [`docs/guides/ELECTRON_GUIDE.md`](docs/guides/ELECTRON_GUIDE.md) |
## Review focus
- Keep database operations in `src/lib/db/`; do not issue raw SQL from routes.
- Send provider requests through `open-sse/handlers/`.
- Keep MCP and A2A pages as tabs inside `/dashboard/endpoint`.
- Preserve SSE cleanup, rate-limit header parsing, Zod validation, and provider-schema
validation.
- Treat Memory and Skills as cross-cutting changes that can affect MCP tools, the request
pipeline, and A2A skills.
- Do not close a contributor pull request after using its code; merge it through GitHub so
the contributor receives credit.
## Upstream contributions
This checkout is a fork of `diegosouzapw/OmniRoute`. Keep fork-only deployment and personal
automation changes out of upstream PRs.
Start upstream work from the active upstream default branch, not `main`:
```bash
git fetch upstream
# the default branch is the active release line, e.g. release/v3.8.49
git switch -c <branch-name> upstream/release/vX.Y.Z
git switch -c <branch-name> upstream/<default-branch>
```
Only cherry-pick or reapply the changes intended for the upstream PR.
Target that same release branch in the pull request. Stage only the intended files, run the
focused checks, and use a Conventional Commit message (for example, `docs: slim AGENTS.md`).
---
## Reference documentation
## Review Focus
Use the source of truth for the area you are changing:
- **DB ops** go through `src/lib/db/` modules, never raw SQL in routes
- **Provider requests** flow through `open-sse/handlers/`
- **MCP/A2A pages** are tabs inside `/dashboard/endpoint`, not standalone routes
- **No memory leaks** in SSE streams (abort signals, cleanup)
- **Rate limit headers** must be parsed correctly
- All API inputs validated with **Zod schemas**
- **Provider constants** validated at module load via Zod (`src/shared/validation/providerSchema.ts`)
- **Pricing data** syncs from LiteLLM via `src/lib/pricingSync.ts`
- **Memory/Skills** are cross-cutting: affect MCP tools, request pipeline, and A2A skills
- **⛔ NEVER close a contributor's PR** after using their code — always merge via GitHub so they get credit. See `.agents/workflows/review-prs.md` for full policy.
| Area | Reference |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Repository navigation and architecture | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md), [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| API and providers | [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md), [`docs/reference/PROVIDER_REFERENCE.md`](docs/reference/PROVIDER_REFERENCE.md), [`docs/openapi.yaml`](docs/openapi.yaml) |
| Routing, resilience, and reasoning | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md), [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md), [`docs/routing/REASONING_REPLAY.md`](docs/routing/REASONING_REPLAY.md) |
| Security | [`docs/security/GUARDRAILS.md`](docs/security/GUARDRAILS.md), [`docs/security/COMPLIANCE.md`](docs/security/COMPLIANCE.md), [`docs/security/STEALTH_GUIDE.md`](docs/security/STEALTH_GUIDE.md) |
| Platform features | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md), [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md), [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md), [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
| Releases and quality | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md), [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |

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

@@ -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

@@ -0,0 +1 @@
- **fix(sse):** error-only streams now preserve sanitized executor diagnostics for operators without changing stream-readiness fallback classification ([#9022](https://github.com/diegosouzapw/OmniRoute/pull/9022)) — thanks @shixi-li

View File

@@ -0,0 +1 @@
- **fix(translator):** pass `output_config.effort="max"` through verbatim instead of unconditionally rewriting it to `xhigh`, so Anthropic → OpenAI-shape upstream calls reach `sanitizeReasoningEffortForProvider` with the carrier intact and providers that accept `max` literally (Ollama Cloud, opencode-go DeepSeek, Moonshot K3, native Claude) no longer 400 on `invalid reasoning value: 'xhigh'`. Regression guard: end-to-end test in `tests/unit/base-executor-sanitize-effort.test.ts`.

View File

@@ -0,0 +1 @@
- **fix(cli-tools):** keep Apply enabled for active OpenAI-compatible and Anthropic-compatible providers without static catalog entries. (thanks @lazysaltyfish)

View File

@@ -0,0 +1 @@
- **fix(translator):** harden Claude format detection for relative message endpoints and kebab-case version metadata. (thanks @ervareza)

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",

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).",
@@ -350,7 +349,7 @@
"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,
@@ -386,7 +385,8 @@
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1109,
"_rebaseline_2026_07_30_8916_quota_compact_layout": "PR #8916 (apoapostolov, feat/improve-provider-quota-layouts) own growth: src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx 1109->1153 (+44) adds Full/Compact layout toggle — LS_LAYOUT_MODE constant, LayoutMode type, layoutMode state, toggleLayoutMode callback, toggle button with icon. At existing filter/settings chokepoint. Not extractable without splitting state + toolbar away from data-fetching. Covered by tests/unit/quota-card-grid-compact-layout-8916.test.ts.",
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1153,
"src/app/api/providers/[id]/models/route.ts": 2250,
"src/app/api/v1/models/catalog.ts": 1549,
"src/lib/db/apiKeys.ts": 1529,
@@ -401,8 +401,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 +414,6 @@
"_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_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."
}

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");

View File

@@ -1,181 +0,0 @@
# Devin Claude Bridge
`devin-cli-agentic` lets the real Claude Code runtime use OmniRoute's local Anthropic
Messages endpoint while the official Devin CLI supplies model responses over ACP stdio. It
does not modify the existing Anthropic, Claude OAuth, Claude Web, or `devin-cli` providers.
> **Current status: offline and live validated.** The pinned Claude Code `2.1.220` completed
> three isolated scenarios through Devin CLI `3000.2.17` and model
> `swe-1-7-lightning`. The final live run proved client-owned `Read`, `Edit`, and `Bash`
> turns, successful `npm test` results, project command and skill discovery, Devin-only
> routing, and zero Claude egress.
## Architecture
```text
Claude Code 2.1.220 (isolated non-root Linux container)
-> http://omniroute:20128/v1/messages
-> devin-cli-agentic (Claude-format, no-auth provider)
-> devin acp --agent-type summarizer (official ACP stdio, no Devin tools)
-> Devin account in the dedicated devin-auth volume
```
The official CLI's default ACP agent can execute its own tools, so this bridge does not use
it. It starts the fixed `summarizer` ACP agent, whose official CLI mode has no tools, and
frames the serialized Anthropic request as an execution trace. When another Claude-owned
action is needed, the response must contain exactly one client tool envelope. Any ACP
`tool_call` or `tool_call_update` is rejected before a response can be reported as
successful.
The serializer in `open-sse/executors/devin-agentic/serializer.ts` preserves `system`,
`text`, `tool_use`, `tool_result`, `thinking`, `redacted_thinking`, `tool_choice`, and the
tools supplied by Claude Code. Images and unknown blocks fail explicitly. Large tool results
use a visible truncation marker.
The parser accepts one standalone `<tool>{...}</tool>` envelope per model turn. It checks
the name against the request's tool list, validates arguments against that tool's JSON
Schema, rejects mixed narrative/actions, and permits one bounded repair. Claude Code then
executes the resulting Anthropic `tool_use` locally and sends the `tool_result` back through
OmniRoute.
## Isolation and threat model
The host's Claude installation, account, and configuration are out of scope and treated as
forbidden. The Compose services:
- run as UID/GID `10001:10001`, with a read-only root filesystem, dropped capabilities, and
`no-new-privileges`;
- use a private `/home/bridge`, a dedicated Claude config volume, isolated OmniRoute data,
and a separate `devin-auth` volume;
- mount only disposable `.sandbox` workspaces/evidence;
- do not mount the host home, Keychain, SSH, cloud credentials, or Docker socket;
- construct explicit environments and remove Anthropic API/OAuth/routing variables;
- direct Claude Code inference only to `http://omniroute:20128` with a local-only key.
The offline profile uses an internal network. In the live profile, OmniRoute reaches the
official Devin endpoints only through `network-guard`; unrelated destinations are denied.
Claude Code has a separate deny-all egress guard and can reach only the local OmniRoute
service through `NO_PROXY`. Guard audit files are mounted only by their guard process. The
scripts verify file ownership, mode, link count, and every decision before exporting
token-free evidence.
Run the isolation proof independently:
```bash
./scripts/devin-bridge/verify-anthropic-isolation
```
It validates topology, named mounts, non-root/read-only settings, explicit local routing,
absence of sensitive environment variables, absence of the Docker socket, blocked access to
`api.anthropic.com` and `claude.ai`, Devin-only provider selection, and explicit failure when
the ACP backend is unavailable.
## First-time setup and normal use
Build the pinned image:
```bash
./scripts/devin-bridge/build
```
Authenticate only the isolated Devin volume:
```bash
ENABLE_LIVE_DEVIN_TESTS=1 ./scripts/devin-bridge/login-devin
```
The login command uses the official manual-token flow intended for remote/container
environments. The value is entered directly into the CLI prompt; it is not passed as a
process argument, written to Git, or copied from the host.
Launch the isolated Claude Code runtime:
```bash
./scripts/devin-bridge/launch
```
`launch` rechecks isolation, Devin authentication, and model discovery before starting the
containerized Claude Code. It never runs the host's Claude executable. Model aliases can be
set in `.env.devin-bridge`; every configured value must keep the
`devin-cli-agentic/` prefix.
## Validation commands
The reproducible offline path requires no Devin account and has no runtime Internet:
```bash
./scripts/devin-bridge/test-unit
./scripts/devin-bridge/test-contract
./scripts/devin-bridge/test-e2e-mock
./scripts/devin-bridge/verify-anthropic-isolation
```
The authenticated opt-in live path is:
```bash
ENABLE_LIVE_DEVIN_TESTS=1 ./scripts/devin-bridge/test-live-devin
```
The live runner waits between scenarios to avoid opening ACP sessions in a burst and
validates structured Claude stream events instead of trusting textual claims. Its three
scenarios prove:
1. direct project reads and defect analysis;
2. a real `Edit`, a client-owned `Bash` `npm test`, and a terminal result;
3. `/bridge-check` plus `bridge-proof` discovery, project reads, another successful
client-owned `npm test`, and completion without pending work.
The final gate also checks the Devin network audit and requires the Claude egress audit to
remain empty.
## Updating pinned tools
The image pins Node, Claude Code, and Devin CLI in
`docker/devin-bridge/Dockerfile`. To update:
1. change the explicit versions;
2. replace both architecture-specific Devin archive checksums with values for the official
artifact;
3. rebuild and run every offline validation command;
4. confirm the versions inside the image;
5. rerun the authenticated three-scenario live suite.
Do not install either CLI globally on the host or replace checksum verification with an
unverified download.
## Diagnosis and cleanup
- `docker compose -f docker/devin-bridge/compose.yml --profile offline logs omniroute`
shows local routing and sanitized executor errors.
- `.sandbox/evidence/mock-acp.jsonl` records deterministic mock ACP actions.
- `.sandbox/evidence/claude-stream.jsonl` records the real Claude Code offline run.
- `.sandbox/evidence/live-*.jsonl` records the three validated live streams.
- `.sandbox/evidence/egress.jsonl` and `.sandbox/evidence/claude-egress.jsonl` are validated,
token-free copies of the guard audits.
Stop owned containers and networks while preserving login/config volumes:
```bash
./scripts/devin-bridge/clean
```
Remove the complete bridge-owned environment, including named volumes:
```bash
./scripts/devin-bridge/clean --all
```
## Limits
- The bridge relies on the fixed no-tools `summarizer` role because Devin CLI `3000.2.17`
does not expose a neutral no-tools ACP agent. The adapter compensates for summary-shaped
intermediate responses, but one bounded repair can still fail explicitly.
- Live ACP calls can return transient `502`/`504` responses. The harness spaces scenarios;
persistent failure remains fail-closed and never selects another provider.
- ACP context is reconstructed from each Anthropic request; there is no process/session
affinity.
- One tool call is supported per model response; parallel calls are rejected.
- Images are explicitly unsupported. Vision, thinking output, effort controls, and a 1M
context window are not advertised.
- SSE uses valid Anthropic lifecycle events but is emitted after the bounded ACP turn is
collected; ACP chunks are not forwarded incrementally.

View File

@@ -1,115 +0,0 @@
# Devin Claude Bridge Progress
Updated: 2026-07-28
## Baseline
- Fork version: `3.8.49`.
- Starting branch: `release/v3.8.49`.
- Starting commit: `ed7db3ee5f89a144b2d931d8605534522f83de30`.
- Fixed runtime artifacts: Node `26.0.0`, Claude Code `2.1.220`, Devin CLI `3000.2.17`.
- Existing `devin-cli` remains unchanged; the new path is the separate
`devin-cli-agentic` provider.
## Implemented architecture
- Claude Code runs only inside the non-root bridge container with its own empty config
volume and local OmniRoute base URL.
- `devin-cli-agentic` preserves Anthropic messages, tool schemas, `tool_use`, and
`tool_result`, then calls the official Devin CLI over ACP stdio.
- The executor starts `devin acp --agent-type summarizer`. This is the only fixed official
ACP role in the pinned CLI that has no Devin-owned tools.
- The request is framed as an execution trace. Devin can return one strict client tool
envelope; Claude Code executes that tool locally.
- Internal ACP `tool_call` events, unsupported blocks, invalid schemas, narrative actions,
timeouts, cancellation, and process failure all fail closed.
- Provider and network policy prevent combo/auto/Anthropic fallback.
## Offline proof
- Focused serializer, parser, executor, ACP lifecycle, wire-format, environment, and audit
tests pass (39/39).
- The contract suite covers Anthropic JSON/SSE, `tool_use`, `tool_result` continuation,
fragmented ACP frames, stderr, early exit, timeout, cancellation, and fail-closed provider
loss.
- The production bridge image builds with the pinned CLIs.
- Real Claude Code offline E2E loads `CLAUDE.md`, the project skill and slash command, fires
hooks, executes local tools over multiple turns, observes a failed test, repairs the file,
reruns the test, and completes.
- The isolation verifier proves non-root/read-only execution, isolated mounts and config,
blocked Anthropic/Claude access, no host credential mounts, local-only inference, and no
fallback.
Evidence is generated under `.sandbox/evidence` and ignored by Git.
## Regression status
- `typecheck:core`, focused ESLint, Prettier, shell/Node syntax, and the complete documentation
accuracy suite pass.
- The broad `npm run check` is not reported as passed: after its lint phase, the repository
test runner remained alive while an existing `ioredis` client repeatedly retried an
unavailable local Redis endpoint after `quota-redis-store.test.ts`. The bridge-focused
suites, production image build, offline E2E, isolation proof, and live gate do not use that
Redis service and all pass.
## Live Devin proof
Passed with the official in-container login and discovered model
`swe-1-7-lightning`. The terminal live run completed all three scenarios:
1. Claude Code loaded the fixture instructions, issued client-owned `Read` calls, and
returned a correct defect analysis.
2. Claude Code issued a real `Edit` changing subtraction to addition, then a client-owned
`Bash` call running `npm test`; the test reported one pass and zero failures.
3. Claude Code initialization listed `bridge-check` and `bridge-proof`, read the corrected
source and test, executed another client-owned `npm test`, and completed successfully.
The live evidence validator parses stream JSON and requires successful tool results. It does
not accept a textual claim that a tool ran. It also rejects terminal summaries that report a
blocker, incomplete work, or required next steps.
The final live gate reported:
```text
PASS: validated Claude evidence for LIVE_ANALYSIS_COMPLETE
PASS: validated Claude evidence for LIVE_FIX_COMPLETE
PASS: validated Claude evidence for BRIDGE_E2E_COMPLETE
PASS: three live Devin-backed Claude Code scenarios completed
PASS: live model swe-1-7-lightning was discovered and validated by three scenarios
```
The same gate validated the network audit: only the Devin guard path was used, no internal
Devin tool event was accepted, and the Claude egress audit remained empty.
## Investigation conclusion
The initial default-agent hypothesis failed because ACP permission modes do not turn the
default Devin agent into a raw inference backend. Even `ask` mode can emit Devin-owned
`tool_call` events. A discovered `allowed-tools: []` agent configuration was not consumed by
`devin acp` in CLI `3000.2.17`.
The working adaptation uses the official `summarizer` agent because it is structurally
no-tools. Its fixed summarization behavior can produce intermediate prose, so the bridge
frames requests as execution traces, detects future-action narration, performs at most one
strict repair, and otherwise fails. Live validation also exposed transient ACP timeouts;
the harness now spaces independent scenarios rather than weakening routing or retrying into
another provider.
## Safety record
No host Claude executable, configuration, login, OAuth token, Keychain, or Anthropic API was
used. The dedicated Docker volumes remain role-separated. No credential value is written to
the repository or evidence output.
During the early baseline, a focused test without isolated `DATA_DIR` initialized the
repository's normal OmniRoute database at `/Users/lucasisrael/.omniroute/storage.sqlite`.
It was not rolled back or touched again. Every bridge command now pins database and temporary
paths under the worktree's `.sandbox` directory.
## Remaining limits
- The no-tools backend has a summarizer system role rather than a neutral generation role.
- One client tool call per response is supported; parallel tool calls are rejected.
- ACP processes are per-turn and stateless.
- Live Devin availability can still produce explicit `502`/`504` failures.
- Images and unadvertised vision/effort/large-context capabilities remain unsupported.

View File

@@ -64,8 +64,6 @@ How the system is put together — read these to understand the runtime, code la
- [QUALITY_GATES.md](architecture/QUALITY_GATES.md) — quality-gate scripts and CI jobs inventory.
- [MONITORING_SECTIONS.md](architecture/MONITORING_SECTIONS.md) — monitoring/costs dashboard navigation.
- [cluster-decisions.md](architecture/cluster-decisions.md) — optional sidecar/cluster profile decisions.
- [rust-port-research.md](rust-port-research.md) — feasibility study for porting the CPU-bound compression/tokenization core to a standalone Rust library (fork research, 2026-07-31).
- [infrastructure.md](infrastructure.md) — deployment topology (Proxmox / LXC 101 / LXC 106 / Forgejo / Docker) and push flows (fork research, 2026-07-31).
## reference/

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