mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
Compare commits
79 Commits
codex/quot
...
compressio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52842b926 | ||
|
|
3d444c2209 | ||
|
|
944178475c | ||
|
|
2a3adca1cb | ||
|
|
52775bc958 | ||
|
|
a076376490 | ||
|
|
1c3f6dcc90 | ||
|
|
9d495f7c44 | ||
|
|
904d45e011 | ||
|
|
3eca8d9c0c | ||
|
|
aefab44503 | ||
|
|
c605cabf08 | ||
|
|
9f67e5cc74 | ||
|
|
f579f9b096 | ||
|
|
17c76cf5d6 | ||
|
|
0975bc8ca9 | ||
|
|
2baf2f4820 | ||
|
|
0be4243d2e | ||
|
|
4826385f19 | ||
|
|
988f76c9bf | ||
|
|
439f13b210 | ||
|
|
0515e69a3f | ||
|
|
96b31e3142 | ||
|
|
00059f7bdd | ||
|
|
71750799eb | ||
|
|
b4f0d97f10 | ||
|
|
bf83381794 | ||
|
|
71a8f6f98e | ||
|
|
64dba26398 | ||
|
|
19b58a99ab | ||
|
|
26b058207b | ||
|
|
ea801bbca2 | ||
|
|
77eb184f9d | ||
|
|
869f07b3b0 | ||
|
|
3780d45d62 | ||
|
|
da3c3c9f67 | ||
|
|
a48f256f51 | ||
|
|
b2ce078c92 | ||
|
|
8a3888e510 | ||
|
|
368d1c0e87 | ||
|
|
b21f2d91e7 | ||
|
|
c2c622ad82 | ||
|
|
796b4fefd1 | ||
|
|
cf2055ce3e | ||
|
|
4ef44a53a7 | ||
|
|
2a1c946aa6 | ||
|
|
0d2678360f | ||
|
|
a8fb526e9c | ||
|
|
dcddbe11cd | ||
|
|
0e66f7e566 | ||
|
|
4f97f84dea | ||
|
|
8aa9c6f710 | ||
|
|
5ead198eb4 | ||
|
|
a0f9ad852d | ||
|
|
8941eafcb9 | ||
|
|
b02c586cb4 | ||
|
|
e26fc0a774 | ||
|
|
ecd1114894 | ||
|
|
53066a592a | ||
|
|
6c309c5e5f | ||
|
|
59d5f43d7b | ||
|
|
152a3e13f7 | ||
|
|
ae3d026ad0 | ||
|
|
347bfe257c | ||
|
|
0dcac8bfc3 | ||
|
|
71af74bee4 | ||
|
|
ccbe6bc288 | ||
|
|
98c98856b8 | ||
|
|
3899495f60 | ||
|
|
35797deeab | ||
|
|
c3a024d3be | ||
|
|
b0d4d64e6c | ||
|
|
aa5856376e | ||
|
|
a52d5fb31f | ||
|
|
9ca5a1ad42 | ||
|
|
44ba571521 | ||
|
|
cc8d790262 | ||
|
|
ad94ec491e | ||
|
|
1e629721b9 |
@@ -18,6 +18,7 @@ coverage
|
|||||||
# Runtime data and logs
|
# Runtime data and logs
|
||||||
data
|
data
|
||||||
logs
|
logs
|
||||||
|
.sandbox
|
||||||
|
|
||||||
# Local env files (inject at runtime via --env-file or -e)
|
# Local env files (inject at runtime via --env-file or -e)
|
||||||
.env
|
.env
|
||||||
|
|||||||
6
.env.devin-bridge.example
Normal file
6
.env.devin-bridge.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
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
|
||||||
24
.env.example
24
.env.example
@@ -1839,6 +1839,18 @@ APP_LOG_TO_FILE=true
|
|||||||
# ── Devin CLI binary path ──
|
# ── Devin CLI binary path ──
|
||||||
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
|
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
|
||||||
# CLI_DEVIN_BIN=devin
|
# 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 ──
|
# ── Command Code (custom CLI) callback ──
|
||||||
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
||||||
@@ -2300,6 +2312,18 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# HYPERAGENT_USAGE_URL=https://hyperagent.com/api/settings/billing/usage
|
# 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)
|
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
|
||||||
# Containerized Chromium+VNC used for interactive browser-login credential
|
# Containerized Chromium+VNC used for interactive browser-login credential
|
||||||
|
|||||||
166
.github/workflows/ci.yml
vendored
166
.github/workflows/ci.yml
vendored
@@ -27,7 +27,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
name: Change Classification
|
name: Change Classification
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
outputs:
|
outputs:
|
||||||
code: ${{ steps.classify.outputs.code }}
|
code: ${{ steps.classify.outputs.code }}
|
||||||
docs: ${{ steps.classify.outputs.docs }}
|
docs: ${{ steps.classify.outputs.docs }}
|
||||||
@@ -35,13 +35,10 @@ jobs:
|
|||||||
workflow: ${{ steps.classify.outputs.workflow }}
|
workflow: ${{ steps.classify.outputs.workflow }}
|
||||||
testsOnly: ${{ steps.classify.outputs.testsOnly }}
|
testsOnly: ${{ steps.classify.outputs.testsOnly }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
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
|
# 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
|
# 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.
|
# a full check board attached on every push to that branch. One field comparison.
|
||||||
@@ -77,7 +74,7 @@ jobs:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||||
@@ -91,13 +88,9 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
- run: npm run audit:deps
|
- run: npm run audit:deps
|
||||||
@@ -171,7 +164,7 @@ jobs:
|
|||||||
|
|
||||||
quality-gate:
|
quality-gate:
|
||||||
name: Quality Ratchet
|
name: Quality Ratchet
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# needs lint so eslint-results artifact is available (same inventory as the
|
# needs lint so eslint-results artifact is available (same inventory as the
|
||||||
# blocking lint step). Allow lint failure so other ratchets still run.
|
# blocking lint step). Allow lint failure so other ratchets still run.
|
||||||
needs: [changes, test-coverage, lint]
|
needs: [changes, test-coverage, lint]
|
||||||
@@ -191,13 +184,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: read
|
security-events: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- name: Restore ESLint file cache
|
- name: Restore ESLint file cache
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
@@ -289,7 +278,7 @@ jobs:
|
|||||||
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
|
# SonarQube needs SONAR_TOKEN/SONAR_HOST_URL secrets.
|
||||||
quality-extended:
|
quality-extended:
|
||||||
name: Quality Gates (Extended)
|
name: Quality Gates (Extended)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||||
@@ -299,14 +288,10 @@ jobs:
|
|||||||
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
|
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
|
||||||
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
|
# 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).
|
# would lack the base ref and the gate would self-skip (base-unresolved).
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
# Dead-code, cognitive-complexity, type-coverage foram promovidos ao job
|
# 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.
|
# quality-gate (bloqueante) na Fase 7 INT — não rodam aqui para evitar duplo custo.
|
||||||
@@ -409,20 +394,16 @@ jobs:
|
|||||||
|
|
||||||
docs-sync-strict:
|
docs-sync-strict:
|
||||||
name: Docs Sync (Strict)
|
name: Docs Sync (Strict)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# 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.
|
# 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')) }}
|
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:docs-all
|
- run: npm run check:docs-all
|
||||||
# Previously-orphaned contract gates (existed as files, never wired anywhere).
|
# Previously-orphaned contract gates (existed as files, never wired anywhere).
|
||||||
@@ -442,7 +423,7 @@ jobs:
|
|||||||
|
|
||||||
docs-lint:
|
docs-lint:
|
||||||
name: Docs Lint (prose — advisory)
|
name: Docs Lint (prose — advisory)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||||
@@ -452,13 +433,9 @@ jobs:
|
|||||||
# existing doc corpus is brought up to style. Promote to blocking once it converges.
|
# existing doc corpus is brought up to style. Promote to blocking once it converges.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- name: markdownlint (docs + root, advisory)
|
- name: markdownlint (docs + root, advisory)
|
||||||
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
|
run: npx --yes markdownlint-cli2 "docs/**/*.md" "*.md" "!docs/i18n" "!docs/research" || true
|
||||||
- name: Vale prose lint (Microsoft style, advisory)
|
- name: Vale prose lint (Microsoft style, advisory)
|
||||||
@@ -473,7 +450,7 @@ jobs:
|
|||||||
|
|
||||||
i18n-ui-coverage:
|
i18n-ui-coverage:
|
||||||
name: i18n UI Coverage
|
name: i18n UI Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||||
@@ -483,14 +460,10 @@ jobs:
|
|||||||
# fetch-depth: 0 — the value-drift gate diffs en.json against the merge base to
|
# 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
|
# 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.
|
# the gate self-skips (base-unresolved), so it would never actually run.
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
|
||||||
# #8463: a rewritten English value used to leave its 39 translations behind
|
# #8463: a rewritten English value used to leave its 39 translations behind
|
||||||
@@ -506,17 +479,13 @@ jobs:
|
|||||||
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
|
# without needing app-boot/Playwright infra. Same gating as i18n-ui-coverage.
|
||||||
i18n-glossary-zhcn:
|
i18n-glossary-zhcn:
|
||||||
name: i18n Glossary (zh-CN)
|
name: i18n Glossary (zh-CN)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- 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-CN
|
||||||
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
|
- run: node scripts/i18n/check-glossary-consistency.mjs --locale=zh-TW
|
||||||
@@ -528,7 +497,7 @@ jobs:
|
|||||||
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
|
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
|
||||||
i18n:
|
i18n:
|
||||||
name: i18n Validation (all languages)
|
name: i18n Validation (all languages)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: changes
|
needs: changes
|
||||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
# 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).
|
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||||
@@ -536,7 +505,7 @@ jobs:
|
|||||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
|
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v7
|
- uses: actions/setup-python@v7
|
||||||
@@ -571,15 +540,12 @@ jobs:
|
|||||||
pr-test-policy:
|
pr-test-policy:
|
||||||
name: PR Test Policy
|
name: PR Test Policy
|
||||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
|
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
- name: Fetch base branch
|
- name: Fetch base branch
|
||||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
|
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
|
||||||
- name: Validate source changes include tests
|
- name: Validate source changes include tests
|
||||||
@@ -606,17 +572,17 @@ jobs:
|
|||||||
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
|
# 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.
|
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
|
||||||
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
|
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
|
||||||
# back to ubuntu-latest unless the PR head repo is this repository (push /
|
# back to ubuntu-26.04 unless the PR head repo is this repository (push /
|
||||||
# dispatch events are own-origin by definition). Any failure path (VM down,
|
# dispatch events are own-origin by definition). Any failure path (VM down,
|
||||||
# var unset/false) also falls back to ubuntu-latest.
|
# 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-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-26.04' }}
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
node-version: ${{ env.CI_NODE_VERSION }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -656,18 +622,14 @@ jobs:
|
|||||||
|
|
||||||
package-artifact:
|
package-artifact:
|
||||||
name: Package Artifact
|
name: Package Artifact
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: build
|
needs: build
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
- name: Download Next.js build artifact
|
- name: Download Next.js build artifact
|
||||||
@@ -703,15 +665,15 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-26.04, windows-latest]
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
node-version: ${{ env.CI_NODE_VERSION }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -750,14 +712,14 @@ jobs:
|
|||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
name: Unit Tests (${{ matrix.shard }}/8)
|
name: Unit Tests (${{ matrix.shard }}/8)
|
||||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
|
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
|
||||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
# 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
|
# 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 —
|
# .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
|
# 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
|
# 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.
|
# self-hosted is strictly worse here and there is nothing to configure.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
# needs: changes (not build) — this job never downloads the next-build artifact;
|
# 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
|
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
|
||||||
@@ -775,13 +737,9 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
|
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
|
||||||
@@ -811,31 +769,27 @@ jobs:
|
|||||||
|
|
||||||
test-bun-sqlite:
|
test-bun-sqlite:
|
||||||
name: Bun SQLite Compatibility
|
name: Bun SQLite Compatibility
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run test:bun:db
|
- run: npm run test:bun:db
|
||||||
|
|
||||||
test-vitest:
|
test-vitest:
|
||||||
name: Vitest (MCP / autoCombo / UI components)
|
name: Vitest (MCP / autoCombo / UI components)
|
||||||
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
|
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-26.04).
|
||||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
# 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
|
# 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 —
|
# .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
|
# 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
|
# 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.
|
# self-hosted is strictly worse here and there is nothing to configure.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||||
needs: changes
|
needs: changes
|
||||||
@@ -845,13 +799,9 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
|
# 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).
|
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
|
||||||
@@ -879,7 +829,7 @@ jobs:
|
|||||||
# the release gate can still exercise them via workflow_dispatch when needed).
|
# the release gate can still exercise them via workflow_dispatch when needed).
|
||||||
test-coverage:
|
test-coverage:
|
||||||
name: Coverage
|
name: Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
# 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
|
# 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).
|
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
||||||
@@ -890,13 +840,9 @@ jobs:
|
|||||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- name: Download all shard coverage
|
- name: Download all shard coverage
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
@@ -980,14 +926,14 @@ jobs:
|
|||||||
|
|
||||||
sonarqube:
|
sonarqube:
|
||||||
name: SonarQube
|
name: SonarQube
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: test-coverage
|
needs: test-coverage
|
||||||
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
|
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -1032,7 +978,7 @@ jobs:
|
|||||||
|
|
||||||
coverage-pr-comment:
|
coverage-pr-comment:
|
||||||
name: PR Coverage Comment
|
name: PR Coverage Comment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
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' }}
|
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:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
@@ -1111,7 +1057,7 @@ jobs:
|
|||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
name: E2E Tests (${{ matrix.shard }}/9)
|
name: E2E Tests (${{ matrix.shard }}/9)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# Build artifact from the `build` job is downloaded instead of rebuilding
|
# 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
|
# (~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).
|
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
|
||||||
@@ -1133,13 +1079,9 @@ jobs:
|
|||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
|
OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
- name: Cache Playwright browsers
|
- name: Cache Playwright browsers
|
||||||
@@ -1188,7 +1130,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
name: Integration Tests (${{ matrix.shard }}/2)
|
name: Integration Tests (${{ matrix.shard }}/2)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||||
needs: changes
|
needs: changes
|
||||||
@@ -1204,13 +1146,9 @@ jobs:
|
|||||||
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
|
DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }}
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
|
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
|
||||||
@@ -1218,7 +1156,7 @@ jobs:
|
|||||||
|
|
||||||
test-security:
|
test-security:
|
||||||
name: Security Tests
|
name: Security Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
# needs: changes (not build) — no artifact consumed; see test-unit note.
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
|
||||||
@@ -1227,20 +1165,16 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
- run: npm run test:security
|
- run: npm run test:security
|
||||||
|
|
||||||
ci-summary:
|
ci-summary:
|
||||||
name: CI Dashboard
|
name: CI Dashboard
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
needs:
|
needs:
|
||||||
- changes
|
- changes
|
||||||
|
|||||||
4
.github/workflows/claude.yml
vendored
4
.github/workflows/claude.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
(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 == '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')))
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
actions: read # Required for Claude to read CI results on PRs
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -13,7 +13,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze (javascript-typescript)
|
name: Analyze (javascript-typescript)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
actions: read
|
actions: read
|
||||||
|
|||||||
6
.github/workflows/dast-smoke.yml
vendored
6
.github/workflows/dast-smoke.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
dast-smoke:
|
dast-smoke:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
|
# 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.
|
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -33,10 +33,6 @@ jobs:
|
|||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Build CLI bundle
|
- name: Build CLI bundle
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/deploy-vps.yml
vendored
2
.github/workflows/deploy-vps.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
(github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
|
(github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
|
||||||
&& vars.DEPLOY_ENABLED == 'true'
|
&& vars.DEPLOY_ENABLED == 'true'
|
||||||
name: Deploy OmniRoute to VPS
|
name: Deploy OmniRoute to VPS
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check VPS SSH reachability from runner
|
- name: Check VPS SSH reachability from runner
|
||||||
id: reach
|
id: reach
|
||||||
|
|||||||
20
.github/workflows/docker-publish.yml
vendored
20
.github/workflows/docker-publish.yml
vendored
@@ -33,7 +33,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
prepare:
|
prepare:
|
||||||
name: Resolve Docker release metadata
|
name: Resolve Docker release metadata
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
promote_latest: ${{ steps.version.outputs.promote_latest }}
|
promote_latest: ${{ steps.version.outputs.promote_latest }}
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
IMAGE_NAME: diegosouzapw/omniroute
|
IMAGE_NAME: diegosouzapw/omniroute
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
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
|
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||||
@@ -155,13 +155,13 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4.5.2
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4.5.2
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -233,7 +233,7 @@ jobs:
|
|||||||
- prepare
|
- prepare
|
||||||
- build
|
- build
|
||||||
if: needs.prepare.outputs.skip != 'true'
|
if: needs.prepare.outputs.skip != 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -245,7 +245,7 @@ jobs:
|
|||||||
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
|
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||||
@@ -255,13 +255,13 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v4.5.2
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v4.5.2
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -390,7 +390,7 @@ jobs:
|
|||||||
- name: Upload Trivy SARIF to Security tab
|
- name: Upload Trivy SARIF to Security tab
|
||||||
if: needs.prepare.outputs.version != 'main'
|
if: needs.prepare.outputs.version != 'main'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: github/codeql-action/upload-sarif@v4.37.3
|
uses: github/codeql-action/upload-sarif@v4
|
||||||
with:
|
with:
|
||||||
sarif_file: trivy-results.sarif
|
sarif_file: trivy-results.sarif
|
||||||
category: trivy-image
|
category: trivy-image
|
||||||
|
|||||||
16
.github/workflows/electron-release.yml
vendored
16
.github/workflows/electron-release.yml
vendored
@@ -20,14 +20,14 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
name: Validate version
|
name: Validate version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.validate.outputs.version }}
|
version: ${{ steps.validate.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -78,17 +78,17 @@ jobs:
|
|||||||
target: mac-arm64
|
target: mac-arm64
|
||||||
ext: -arm64.dmg
|
ext: -arm64.dmg
|
||||||
- platform: linux
|
- platform: linux
|
||||||
runner: ubuntu-latest
|
runner: ubuntu-26.04
|
||||||
target: linux
|
target: linux
|
||||||
ext: .AppImage
|
ext: .AppImage
|
||||||
deb_ext: .deb
|
deb_ext: .deb
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -239,12 +239,12 @@ jobs:
|
|||||||
# Now: attach everything that did build, then fail the job loudly (see the last
|
# Now: attach everything that did build, then fail the job loudly (see the last
|
||||||
# step) so an incomplete channel is visible instead of silent.
|
# step) so an incomplete channel is visible instead of silent.
|
||||||
if: ${{ !cancelled() && needs.validate.result == 'success' }}
|
if: ${{ !cancelled() && needs.validate.result == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # softprops/action-gh-release creates the GitHub Release
|
contents: write # softprops/action-gh-release creates the GitHub Release
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -329,7 +329,7 @@ jobs:
|
|||||||
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
|
# of passing unnoticed — the v3.8.49 release had ZERO assets and every gate was
|
||||||
# green, because nothing ever asserted the release HAS binaries.
|
# green, because nothing ever asserted the release HAS binaries.
|
||||||
if: ${{ !cancelled() && needs.release.result == 'success' }}
|
if: ${{ !cancelled() && needs.release.result == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
4
.github/workflows/lock-released-branch.yml
vendored
4
.github/workflows/lock-released-branch.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
lock-branch:
|
lock-branch:
|
||||||
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- name: Lock release/<tag> branch
|
- name: Lock release/<tag> branch
|
||||||
env:
|
env:
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
guard-no-push-after-release:
|
guard-no-push-after-release:
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- name: Reject push if matching release tag exists
|
- name: Reject push if matching release tag exists
|
||||||
env:
|
env:
|
||||||
|
|||||||
8
.github/workflows/mutation-redundancy.yml
vendored
8
.github/workflows/mutation-redundancy.yml
vendored
@@ -22,7 +22,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
stryker-nobail:
|
stryker-nobail:
|
||||||
name: Stryker disableBail (batch ${{ matrix.batch.name }})
|
name: Stryker disableBail (batch ${{ matrix.batch.name }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -41,13 +41,9 @@ jobs:
|
|||||||
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
|
mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts"
|
||||||
timeout-minutes: 300
|
timeout-minutes: 300
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Run Stryker (disableBail)
|
- name: Run Stryker (disableBail)
|
||||||
env:
|
env:
|
||||||
|
|||||||
20
.github/workflows/nightly-compat.yml
vendored
20
.github/workflows/nightly-compat.yml
vendored
@@ -28,11 +28,11 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
resolve-branch:
|
resolve-branch:
|
||||||
name: Resolve active release branch
|
name: Resolve active release branch
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
outputs:
|
outputs:
|
||||||
target: ${{ steps.branch.outputs.target }}
|
target: ${{ steps.branch.outputs.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -58,15 +58,15 @@ jobs:
|
|||||||
|
|
||||||
compat-build-26:
|
compat-build-26:
|
||||||
name: Node 26 Compatibility Build
|
name: Node 26 Compatibility Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
needs: resolve-branch
|
needs: resolve-branch
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.resolve-branch.outputs.target }}
|
ref: ${{ needs.resolve-branch.outputs.target }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "26"
|
node-version: "26"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
# CI_NODE_VERSION=24). It failed every nightly with the runner-reclaimed
|
# CI_NODE_VERSION=24). It failed every nightly with the runner-reclaimed
|
||||||
# signature ("The runner has received a shutdown signal" / "The operation was
|
# signature ("The runner has received a shutdown signal" / "The operation was
|
||||||
# canceled", no exit code) always at the same Turbopack compile phase — a
|
# canceled", no exit code) always at the same Turbopack compile phase — a
|
||||||
# classic OOM kill on the memory-constrained ubuntu-latest runner. Turbopack's
|
# classic OOM kill on the memory-constrained 16 GB hosted runner. Turbopack's
|
||||||
# native (Rust, off-V8-heap) allocation is NOT bounded by --max-old-space-size
|
# 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
|
# 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
|
# heavier still under Node 26. Use the documented webpack fallback here: it still
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
compat-tests:
|
compat-tests:
|
||||||
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
|
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
needs: resolve-branch
|
needs: resolve-branch
|
||||||
strategy:
|
strategy:
|
||||||
@@ -102,11 +102,11 @@ jobs:
|
|||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
TEST_SHARD: ${{ matrix.shard }}/4
|
TEST_SHARD: ${{ matrix.shard }}/4
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.resolve-branch.outputs.target }}
|
ref: ${{ needs.resolve-branch.outputs.target }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
report:
|
report:
|
||||||
name: Open / update tracking issue on failure
|
name: Open / update tracking issue on failure
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
|
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
|
||||||
needs: [resolve-branch, compat-build-26, compat-tests]
|
needs: [resolve-branch, compat-build-26, compat-tests]
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
13
.github/workflows/nightly-llm-security.yml
vendored
13
.github/workflows/nightly-llm-security.yml
vendored
@@ -10,13 +10,11 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
promptfoo-guard:
|
promptfoo-guard:
|
||||||
name: promptfoo — injection guard (block mode, no secret)
|
name: promptfoo — injection guard (block mode, no secret)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with: { node-version: "24", cache: npm }
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Build CLI bundle
|
- name: Build CLI bundle
|
||||||
env:
|
env:
|
||||||
@@ -46,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
garak:
|
garak:
|
||||||
name: garak probes (skip without provider secret)
|
name: garak probes (skip without provider secret)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# NOTE: the `secrets` context is NOT available in a job-level `if:` — referencing
|
# 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).
|
# 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
|
# Map the secret into a job-level env and gate each step on a presence check, so
|
||||||
@@ -63,13 +61,10 @@ jobs:
|
|||||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
|
echo "::notice::PROMPTFOO_PROVIDER_KEY not set — skipping garak probes (advisory)."
|
||||||
fi
|
fi
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
if: steps.gate.outputs.run == 'true'
|
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
|
- run: npm ci
|
||||||
if: steps.gate.outputs.run == 'true'
|
if: steps.gate.outputs.run == 'true'
|
||||||
- name: Build CLI bundle
|
- name: Build CLI bundle
|
||||||
|
|||||||
15
.github/workflows/nightly-mutation.yml
vendored
15
.github/workflows/nightly-mutation.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
stryker:
|
stryker:
|
||||||
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
|
name: Stryker mutation (batch ${{ matrix.batch.name }} — advisory)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# Mutation testing is expensive. History of the budget:
|
# Mutation testing is expensive. History of the budget:
|
||||||
# - Full 8-module set TIMED OUT at the 180min cap (run 27705123780 = exactly 180min).
|
# - 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
|
# The two god-files chatCore.ts/combo.ts dominated ~2/3 of the mutants and were
|
||||||
@@ -104,13 +104,9 @@ jobs:
|
|||||||
# scripts/quality/mutation-radiography.mjs both merge per file).
|
# scripts/quality/mutation-radiography.mjs both merge per file).
|
||||||
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
|
timeout-minutes: ${{ matrix.batch.timeout || 180 }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Restore Stryker incremental cache
|
- name: Restore Stryker incremental cache
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||||
@@ -145,15 +141,12 @@ jobs:
|
|||||||
name: Mutation score ratchet (blocking)
|
name: Mutation score ratchet (blocking)
|
||||||
needs: stryker
|
needs: stryker
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
- name: Download all mutation reports
|
- name: Download all mutation reports
|
||||||
uses: actions/download-artifact@v8
|
uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
|
|||||||
8
.github/workflows/nightly-property.yml
vendored
8
.github/workflows/nightly-property.yml
vendored
@@ -8,15 +8,11 @@ permissions:
|
|||||||
issues: write
|
issues: write
|
||||||
jobs:
|
jobs:
|
||||||
property-random-seed:
|
property-random-seed:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: fast-check random seed (high runs)
|
- name: fast-check random seed (high runs)
|
||||||
id: prop
|
id: prop
|
||||||
|
|||||||
21
.github/workflows/nightly-release-green.yml
vendored
21
.github/workflows/nightly-release-green.yml
vendored
@@ -68,13 +68,13 @@ jobs:
|
|||||||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
# 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.
|
# 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.
|
# 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-latest' }}
|
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
git checkout "$TARGET"
|
git checkout "$TARGET"
|
||||||
git log -1 --oneline
|
git log -1 --oneline
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -217,19 +217,19 @@ jobs:
|
|||||||
# On a push, only run for a push to main — a push to release/* is handled by
|
# 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).
|
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||||||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-26.04' }}
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -331,12 +331,12 @@ jobs:
|
|||||||
bank-ratchet-shrinks:
|
bank-ratchet-shrinks:
|
||||||
name: Bank ratchet shrinks
|
name: Bank ratchet shrinks
|
||||||
if: ${{ github.event_name != 'push' }}
|
if: ${{ github.event_name != 'push' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -371,11 +371,6 @@ jobs:
|
|||||||
git checkout "$TARGET"
|
git checkout "$TARGET"
|
||||||
git log -1 --oneline
|
git log -1 --oneline
|
||||||
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
|
|
||||||
- name: Ratchet the baselines down
|
- name: Ratchet the baselines down
|
||||||
|
|||||||
32
.github/workflows/nightly-resilience.yml
vendored
32
.github/workflows/nightly-resilience.yml
vendored
@@ -10,43 +10,31 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
heap:
|
heap:
|
||||||
name: Heap-growth gate
|
name: Heap-growth gate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run test:heap
|
- run: npm run test:heap
|
||||||
|
|
||||||
chaos:
|
chaos:
|
||||||
name: Resilience chaos (fault injection)
|
name: Resilience chaos (fault injection)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run test:chaos
|
- run: npm run test:chaos
|
||||||
|
|
||||||
k6-soak:
|
k6-soak:
|
||||||
name: k6 load/soak
|
name: k6 load/soak
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Build CLI bundle
|
- name: Build CLI bundle
|
||||||
env:
|
env:
|
||||||
@@ -78,7 +66,7 @@ jobs:
|
|||||||
|
|
||||||
a11y:
|
a11y:
|
||||||
name: A11y axe (nightly, freeze-and-alert)
|
name: A11y axe (nightly, freeze-and-alert)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and
|
# 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
|
# 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,
|
# timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact,
|
||||||
@@ -92,13 +80,9 @@ jobs:
|
|||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
REQUIRE_AXE: "1"
|
REQUIRE_AXE: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Cache Playwright browsers
|
- name: Cache Playwright browsers
|
||||||
uses: actions/cache@v6.1.0
|
uses: actions/cache@v6.1.0
|
||||||
|
|||||||
6
.github/workflows/nightly-schemathesis.yml
vendored
6
.github/workflows/nightly-schemathesis.yml
vendored
@@ -10,14 +10,12 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
schemathesis:
|
schemathesis:
|
||||||
name: Schemathesis — OpenAPI contract fuzz (advisory)
|
name: Schemathesis — OpenAPI contract fuzz (advisory)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with: { node-version: "24", cache: npm }
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Build CLI bundle
|
- name: Build CLI bundle
|
||||||
env:
|
env:
|
||||||
|
|||||||
12
.github/workflows/npm-publish.yml
vendored
12
.github/workflows/npm-publish.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
# mid-"Creating an optimized production build" while v3.8.48 had still fit in 16min.
|
# 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;
|
# 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.
|
# 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-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-26.04' }}
|
||||||
permissions:
|
permissions:
|
||||||
actions: read # find + download the CI run's next-build artifact for this SHA
|
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)
|
contents: write # gh release upload (attach SBOM to the GitHub Release)
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
packages: write # publish to npm.pkg.github.com
|
packages: write # publish to npm.pkg.github.com
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
# Need full tag history to compare against highest semver when
|
# Need full tag history to compare against highest semver when
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
@@ -339,20 +339,20 @@ jobs:
|
|||||||
echo "✅ Action finished for GitHub Packages"
|
echo "✅ Action finished for GitHub Packages"
|
||||||
|
|
||||||
publish-opencode-plugin:
|
publish-opencode-plugin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
id-token: write # npm provenance
|
id-token: write # npm provenance
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# Full history needed for auto-bump: git diff against previous release tag
|
# Full history needed for auto-bump: git diff against previous release tag
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
|
|||||||
12
.github/workflows/opencode-plugin-ci.yml
vendored
12
.github/workflows/opencode-plugin-ci.yml
vendored
@@ -26,16 +26,16 @@ defaults:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Test (Node ${{ matrix.node }})
|
name: Test (Node ${{ matrix.node }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node: ["22", "24"]
|
node: ["22", "24"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -46,13 +46,13 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
12
.github/workflows/opencode-provider-ci.yml
vendored
12
.github/workflows/opencode-provider-ci.yml
vendored
@@ -26,16 +26,16 @@ defaults:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Test (Node ${{ matrix.node }})
|
name: Test (Node ${{ matrix.node }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node: ["20", "22", "24"]
|
node: ["20", "22", "24"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -45,13 +45,13 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
105
.github/workflows/quality.yml
vendored
105
.github/workflows/quality.yml
vendored
@@ -25,20 +25,17 @@ jobs:
|
|||||||
# path filters share existence reasons: code / docs / i18n / workflow.
|
# path filters share existence reasons: code / docs / i18n / workflow.
|
||||||
changes:
|
changes:
|
||||||
name: Change Classification
|
name: Change Classification
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
outputs:
|
outputs:
|
||||||
code: ${{ steps.classify.outputs.code }}
|
code: ${{ steps.classify.outputs.code }}
|
||||||
docs: ${{ steps.classify.outputs.docs }}
|
docs: ${{ steps.classify.outputs.docs }}
|
||||||
i18n: ${{ steps.classify.outputs.i18n }}
|
i18n: ${{ steps.classify.outputs.i18n }}
|
||||||
workflow: ${{ steps.classify.outputs.workflow }}
|
workflow: ${{ steps.classify.outputs.workflow }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
- id: classify
|
- id: classify
|
||||||
env:
|
env:
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
@@ -61,19 +58,16 @@ jobs:
|
|||||||
name: Build (advisory)
|
name: Build (advisory)
|
||||||
needs: changes
|
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') }}
|
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 — same fork-safe rule as ci.yml / fast-gates.
|
# 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-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-26.04' }}
|
||||||
# #7307: advisory for the first week of release-PR runs; remove
|
# #7307: advisory for the first week of release-PR runs; remove
|
||||||
# continue-on-error after the production-build signal is stable.
|
# continue-on-error after the production-build signal is stable.
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
- run: node -e 'if (process.versions.node.split(".")[0] !== process.env.CI_NODE_VERSION) throw new Error("Expected Node " + process.env.CI_NODE_VERSION)'
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- uses: ./.github/actions/npm-ci-retry
|
- uses: ./.github/actions/npm-ci-retry
|
||||||
- run: npm run check:node-runtime
|
- run: npm run check:node-runtime
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
@@ -88,15 +82,11 @@ jobs:
|
|||||||
name: Docs Gates (fast-path)
|
name: Docs Gates (fast-path)
|
||||||
needs: changes
|
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')) }}
|
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-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
|
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
|
||||||
- run: npm run check:api-docs-refs
|
- run: npm run check:api-docs-refs
|
||||||
@@ -111,7 +101,7 @@ jobs:
|
|||||||
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
|
# 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
|
# 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
|
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
|
||||||
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
|
# or a fork PR falls back to ubuntu-26.04, so this is inert until the flag flips.
|
||||||
# PINNED to hosted (gap 19). This job carried the USE_VPS_RUNNER expression, and that
|
# 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
|
# 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
|
# a self-hosted runner — every non-skipped sample is `GitHub Actions NNNN`. The classifier is
|
||||||
@@ -125,7 +115,7 @@ jobs:
|
|||||||
#
|
#
|
||||||
# With this pinned, USE_VPS_RUNNER governs ONLY build-like jobs — one variable, one coherent
|
# 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.
|
# purpose. That is what gap 19 asked for; a second variable turned out to be unnecessary.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
# tsx gates (known-symbols, route-guard-membership) import modules that open
|
# tsx gates (known-symbols, route-guard-membership) import modules that open
|
||||||
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
||||||
env:
|
env:
|
||||||
@@ -133,14 +123,10 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Restore ESLint file cache
|
- name: Restore ESLint file cache
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
@@ -279,7 +265,7 @@ jobs:
|
|||||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
# 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
|
# `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
|
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
|
||||||
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
|
# doubled wall time (~16 min extra on the hosted runner) without extra coverage.
|
||||||
#
|
#
|
||||||
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
|
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
|
||||||
# coverage remains required via `Unit Tests fast-path` (fast-unit).
|
# coverage remains required via `Unit Tests fast-path` (fast-unit).
|
||||||
@@ -343,36 +329,15 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
fast-vitest:
|
# Share fast-gates' checkout + npm ci instead of spending ~80s preparing a
|
||||||
name: Vitest (fast-path)
|
# separate runner for a ~13s Vitest invocation. !cancelled() preserves the
|
||||||
needs: changes
|
# independent test signal when an earlier fast gate fails.
|
||||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
- name: Vitest
|
||||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
if: ${{ !cancelled() }}
|
||||||
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
|
||||||
# governed the build and the test jobs, which want OPPOSITE machines: the build needs the
|
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast path runs on every PR.
|
||||||
# .113's RAM, the tests need the hosted runner's link. Measured on 2026-07-29 —
|
# Advisory upload, own-origin only.
|
||||||
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
|
- name: Upload Vitest results to Trunk (advisory)
|
||||||
# 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) }}
|
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||||
@@ -385,9 +350,9 @@ jobs:
|
|||||||
name: Unit Tests fast-path (${{ matrix.shard }}/4)
|
name: Unit Tests fast-path (${{ matrix.shard }}/4)
|
||||||
needs: changes
|
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') }}
|
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).
|
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-26.04).
|
||||||
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
|
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
|
||||||
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
|
# critical path again (~8.5min → ~4.5min hosted; ~2min on the 8-slot
|
||||||
# runner box). Node's native --test-shard=N/total takes any denominator — only
|
# 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.
|
# 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
|
# PINNED to hosted, deliberately not on the USE_VPS_RUNNER switch (gap 19). One variable
|
||||||
@@ -396,7 +361,7 @@ jobs:
|
|||||||
# actions/setup-node took 20m06s on .113 with 4 concurrent runners versus 16s hosted (npm
|
# 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
|
# 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.
|
# self-hosted is strictly worse here and there is nothing to configure.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -406,13 +371,9 @@ jobs:
|
|||||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
|
# 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
|
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
|
||||||
@@ -437,7 +398,7 @@ jobs:
|
|||||||
name: No new ESLint warnings
|
name: No new ESLint warnings
|
||||||
needs: changes
|
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') }}
|
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-latest
|
runs-on: ubuntu-26.04
|
||||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
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
|
# 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
|
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
|
||||||
@@ -446,13 +407,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
security-events: read
|
security-events: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Restore ESLint file cache
|
- name: Restore ESLint file cache
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
@@ -505,21 +462,17 @@ jobs:
|
|||||||
name: Merge integrity (changelog + generated skills)
|
name: Merge integrity (changelog + generated skills)
|
||||||
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
|
# 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/')) }}
|
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
||||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.CI_NODE_VERSION }}
|
|
||||||
cache: npm
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
|
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
|
||||||
run: npm run check:changelog-integrity
|
run: npm run check:changelog-integrity
|
||||||
|
|||||||
4
.github/workflows/scorecard.yml
vendored
4
.github/workflows/scorecard.yml
vendored
@@ -11,7 +11,7 @@ permissions: read-all
|
|||||||
jobs:
|
jobs:
|
||||||
analysis:
|
analysis:
|
||||||
name: Scorecard analysis
|
name: Scorecard analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
permissions:
|
permissions:
|
||||||
# security-events: write removed — Scorecard findings are advisory and no longer
|
# security-events: write removed — Scorecard findings are advisory and no longer
|
||||||
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
|
# uploaded to the code-scanning Security tab (they are supply-chain/posture scores,
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/semgrep.yml
vendored
2
.github/workflows/semgrep.yml
vendored
@@ -14,7 +14,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
jobs:
|
jobs:
|
||||||
semgrep:
|
semgrep:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
container:
|
container:
|
||||||
image: semgrep/semgrep
|
image: semgrep/semgrep
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
9
.github/workflows/wiki-sync.yml
vendored
9
.github/workflows/wiki-sync.yml
vendored
@@ -34,15 +34,10 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
sync-wiki:
|
sync-wiki:
|
||||||
name: Sync wiki with docs
|
name: Sync wiki with docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-26.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
|
|
||||||
- name: Clone wiki
|
- name: Clone wiki
|
||||||
env:
|
env:
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -72,6 +72,7 @@ yarn-error.log*
|
|||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.devin-bridge.example
|
||||||
!.env.homolog.example
|
!.env.homolog.example
|
||||||
# Provider API keys (never commit)
|
# Provider API keys (never commit)
|
||||||
*.api-key
|
*.api-key
|
||||||
@@ -209,6 +210,8 @@ scripts/i18n/_pending-keys.json
|
|||||||
.agents/
|
.agents/
|
||||||
.antigravitycli/
|
.antigravitycli/
|
||||||
.claude/
|
.claude/
|
||||||
|
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
|
||||||
|
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
|
||||||
|
|
||||||
# PR Reviews and local feedback files
|
# PR Reviews and local feedback files
|
||||||
pr_reviews*.json
|
pr_reviews*.json
|
||||||
@@ -245,6 +248,8 @@ _artifacts/ # release-green artifacts
|
|||||||
|
|
||||||
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
|
||||||
.artifacts/
|
.artifacts/
|
||||||
|
# Isolated Devin bridge workspaces, evidence, and test databases
|
||||||
|
.sandbox/
|
||||||
|
|
||||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||||
.env.homolog
|
.env.homolog
|
||||||
|
|||||||
29
.mergify.yml
29
.mergify.yml
@@ -17,13 +17,6 @@
|
|||||||
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
|
# • 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.
|
# 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:
|
queue_rules:
|
||||||
- name: release
|
- name: release
|
||||||
# Any current or future release branch — the reason GitHub's native queue was
|
# Any current or future release branch — the reason GitHub's native queue was
|
||||||
@@ -41,26 +34,14 @@ queue_rules:
|
|||||||
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
|
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
|
||||||
# approval in this repo's single-maintainer model (see governance header).
|
# approval in this repo's single-maintainer model (see governance header).
|
||||||
merge_conditions:
|
merge_conditions:
|
||||||
# "Zero failures" — EXCEPT the advisory "Build (advisory)" job (quality.yml):
|
- "#check-failure=0"
|
||||||
# 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-pending=0"
|
||||||
- "#check-success>=1"
|
- "#check-success>=1"
|
||||||
- check-success=Merge integrity (changelog + generated skills)
|
- check-success=Merge integrity (changelog + generated skills)
|
||||||
# NO batching: 'Merge Queue Batch' requires a paid Mergify tier (live finding
|
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
|
||||||
# 2026-07-15 — the queue command fails with "Cannot use Merge Queue batch" on
|
# don't hold a lone PR hostage waiting for siblings.
|
||||||
# the free plan). Serial queue (1 PR at a time) still automates the train.
|
batch_size: 10
|
||||||
|
batch_max_wait_time: 5 min
|
||||||
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
|
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
|
||||||
merge_method: squash
|
merge_method: squash
|
||||||
|
|
||||||
|
|||||||
4
@omniroute/opencode-plugin/package-lock.json
generated
4
@omniroute/opencode-plugin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@omniroute/opencode-plugin",
|
"name": "@omniroute/opencode-plugin",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@omniroute/opencode-plugin",
|
"name": "@omniroute/opencode-plugin",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
5
@omniroute/openhands-plugin/.gitignore
vendored
Normal file
5
@omniroute/openhands-plugin/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
21
@omniroute/openhands-plugin/LICENSE
Normal file
21
@omniroute/openhands-plugin/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
118
@omniroute/openhands-plugin/README.md
Normal file
118
@omniroute/openhands-plugin/README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# @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.
|
||||||
2033
@omniroute/openhands-plugin/package-lock.json
generated
Normal file
2033
@omniroute/openhands-plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
@omniroute/openhands-plugin/package.json
Normal file
79
@omniroute/openhands-plugin/package.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
104
@omniroute/openhands-plugin/src/cli.ts
Normal file
104
@omniroute/openhands-plugin/src/cli.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/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);
|
||||||
|
}
|
||||||
92
@omniroute/openhands-plugin/src/docker.ts
Normal file
92
@omniroute/openhands-plugin/src/docker.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
}
|
||||||
63
@omniroute/openhands-plugin/src/env.ts
Normal file
63
@omniroute/openhands-plugin/src/env.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
}
|
||||||
18
@omniroute/openhands-plugin/src/index.ts
Normal file
18
@omniroute/openhands-plugin/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* @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";
|
||||||
64
@omniroute/openhands-plugin/src/model-map.ts
Normal file
64
@omniroute/openhands-plugin/src/model-map.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
76
@omniroute/openhands-plugin/tests/env.test.ts
Normal file
76
@omniroute/openhands-plugin/tests/env.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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\""));
|
||||||
|
});
|
||||||
22
@omniroute/openhands-plugin/tsconfig.json
Normal file
22
@omniroute/openhands-plugin/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
15
@omniroute/openhands-plugin/tsup.config.ts
Normal file
15
@omniroute/openhands-plugin/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
659
AGENTS.md
659
AGENTS.md
@@ -1,117 +1,600 @@
|
|||||||
# OmniRoute agent guide
|
# omniroute — Agent Guidelines
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
OmniRoute is a unified AI proxy/router. The repository contains the Next.js application
|
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
|
||||||
(`src/`), streaming engine workspace (`open-sse/`), Electron desktop app (`electron/`),
|
with **290 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||||
CLI (`bin/`), and tests (`tests/`).
|
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**.
|
||||||
|
|
||||||
## Setup and focused checks
|
> **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`.**
|
||||||
|
|
||||||
- Runtime: Node.js `>=22.22.3 <23` or `>=24.0.0 <27`; npm 10+.
|
## Doc Accuracy Discipline (read before writing any doc)
|
||||||
- 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
|
> **If `grep -rn "name" src/ open-sse/ bin/` returns nothing, the name does not exist. Do not document it.**
|
||||||
[`CONTRIBUTING.md`](CONTRIBUTING.md#running-tests).
|
|
||||||
|
|
||||||
## Documentation accuracy
|
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.
|
||||||
|
|
||||||
Documentation must describe verified behavior, not plausible behavior.
|
**Rules (enforced by `npm run check:fabricated-docs`):**
|
||||||
|
|
||||||
1. Before documenting an API name, endpoint, path, CLI command, or environment variable,
|
1. **Never state an API name, endpoint, path, CLI command, or env var without grepping for it first.**
|
||||||
search for it: `rg -n "name" src/ open-sse/ bin/`. If it has no source match, do not
|
```bash
|
||||||
document it.
|
grep -rn "theName" src/ open-sse/ bin/
|
||||||
2. Measure mutable counts instead of writing them from memory: use `wc -l <file>` or a
|
# 0 hits → do not document
|
||||||
directory-specific count command.
|
```
|
||||||
3. Copy code examples from working usage or run them. Prefer a source link such as
|
2. **Never write a line count, file size, migration count, provider count, or strategy count from memory.**
|
||||||
`path/to/file.ts:line` to an invented signature.
|
```bash
|
||||||
4. Run `npm run check:docs-all` for edits under `docs/`; it includes the fabricated-docs
|
wc -l <file> # exact line count
|
||||||
validation.
|
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.
|
||||||
|
|
||||||
## Code conventions
|
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`.
|
||||||
|
|
||||||
- Format with Prettier: two spaces, semicolons, double quotes, 100-character line width,
|
## Stack
|
||||||
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
|
- **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
|
||||||
|
|
||||||
- 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
|
## Build, Lint, and Test Commands
|
||||||
|
|
||||||
Read the nearest `AGENTS.md` and the linked deep-dive before making a non-trivial change.
|
| 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 |
|
||||||
|
|
||||||
| Area | Location | Start here |
|
**Build output layout:**
|
||||||
| ---------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| 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
|
| Directory | Purpose | Gitignored |
|
||||||
|
| --------- | -------------------------------------------------- | ---------- |
|
||||||
|
| `src/` | Application source (TypeScript / TSX) | No |
|
||||||
|
| `.build/` | Build intermediates (`distDir = .build/next`) | Yes |
|
||||||
|
| `dist/` | Shippable bundle assembled by `assembleStandalone` | Yes |
|
||||||
|
|
||||||
- Keep database operations in `src/lib/db/`; do not issue raw SQL from routes.
|
The pipeline is a single `next build` pass — intermediates land in `.build/next/`, the
|
||||||
- Send provider requests through `open-sse/handlers/`.
|
assembled bundle in `dist/`. VPS deploys rsync `dist/` into the remote
|
||||||
- Keep MCP and A2A pages as tabs inside `/dashboard/endpoint`.
|
`/usr/lib/node_modules/omniroute/app/` directory (VPS image path is unchanged).
|
||||||
- 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
|
### Running Tests
|
||||||
|
|
||||||
This checkout is a fork of `diegosouzapw/OmniRoute`. Keep fork-only deployment and personal
|
```bash
|
||||||
automation changes out of upstream PRs.
|
# All tests (unit + vitest + ecosystem + e2e)
|
||||||
|
npm run test:all
|
||||||
|
|
||||||
Start upstream work from the active upstream default branch, not `main`:
|
# 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`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git fetch upstream
|
git fetch upstream
|
||||||
git switch -c <branch-name> upstream/<default-branch>
|
# the default branch is the active release line, e.g. release/v3.8.49
|
||||||
|
git switch -c <branch-name> upstream/release/vX.Y.Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Target that same release branch in the pull request. Stage only the intended files, run the
|
Only cherry-pick or reapply the changes intended for the upstream PR.
|
||||||
focused checks, and use a Conventional Commit message (for example, `docs: slim AGENTS.md`).
|
|
||||||
|
|
||||||
## Reference documentation
|
---
|
||||||
|
|
||||||
Use the source of truth for the area you are changing:
|
## Review Focus
|
||||||
|
|
||||||
| Area | Reference |
|
- **DB ops** go through `src/lib/db/` modules, never raw SQL in routes
|
||||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
- **Provider requests** flow through `open-sse/handlers/`
|
||||||
| Repository navigation and architecture | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md), [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
- **MCP/A2A pages** are tabs inside `/dashboard/endpoint`, not standalone routes
|
||||||
| 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) |
|
- **No memory leaks** in SSE streams (abort signals, cleanup)
|
||||||
| 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) |
|
- **Rate limit headers** must be parsed correctly
|
||||||
| 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) |
|
- All API inputs validated with **Zod schemas**
|
||||||
| 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) |
|
- **Provider constants** validated at module load via Zod (`src/shared/validation/providerSchema.ts`)
|
||||||
| Releases and quality | [`docs/ops/RELEASE_CHECKLIST.md`](docs/ops/RELEASE_CHECKLIST.md), [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md) |
|
- **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.
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ FROM runner-base AS runner-cli
|
|||||||
# runner-base runs.
|
# runner-base runs.
|
||||||
USER root
|
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).
|
# Install system dependencies required by openclaw (git+ssh references).
|
||||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
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 \
|
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||||
|
|||||||
26
THIRD_PARTY_NOTICES.md
Normal file
26
THIRD_PARTY_NOTICES.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 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.
|
||||||
56
bin/chatgpt-web-codex-mcp.mjs
Normal file
56
bin/chatgpt-web-codex-mcp.mjs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- **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
|
||||||
14
changelog.d/fixes/9006-vertex-claude-catalog-dispatch.md
Normal file
14
changelog.d/fixes/9006-vertex-claude-catalog-dispatch.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
- **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))
|
||||||
1
changelog.d/fixes/9013-model-param-filter-save.md
Normal file
1
changelog.d/fixes/9013-model-param-filter-save.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- **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))
|
||||||
@@ -1 +0,0 @@
|
|||||||
- **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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- **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`.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- **fix(cli-tools):** keep Apply enabled for active OpenAI-compatible and Anthropic-compatible providers without static catalog entries. (thanks @lazysaltyfish)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- **fix(translator):** harden Claude format detection for relative message endpoints and kebab-case version metadata. (thanks @ervareza)
|
|
||||||
2
compression-core/.gitignore
vendored
Normal file
2
compression-core/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
27
compression-core/Cargo.toml
Normal file
27
compression-core/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[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
|
||||||
61
compression-core/README.md
Normal file
61
compression-core/README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 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
|
||||||
17
compression-core/crates/bench/Cargo.toml
Normal file
17
compression-core/crates/bench/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[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
|
||||||
19
compression-core/crates/bench/benches/tokenizer.rs
Normal file
19
compression-core/crates/bench/benches/tokenizer.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! 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);
|
||||||
10
compression-core/crates/core-api/Cargo.toml
Normal file
10
compression-core/crates/core-api/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "core-api"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
76
compression-core/crates/core-api/src/lib.rs
Normal file
76
compression-core/crates/core-api/src/lib.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
//! 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;
|
||||||
|
}
|
||||||
13
compression-core/crates/ffi/Cargo.toml
Normal file
13
compression-core/crates/ffi/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[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.
|
||||||
8
compression-core/crates/ffi/src/lib.rs
Normal file
8
compression-core/crates/ffi/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//! 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;
|
||||||
11
compression-core/crates/tests/Cargo.toml
Normal file
11
compression-core/crates/tests/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[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 }
|
||||||
66
compression-core/crates/tests/src/lib.rs
Normal file
66
compression-core/crates/tests/src/lib.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
//! 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");
|
||||||
|
}
|
||||||
13
compression-core/crates/tokenizer/Cargo.toml
Normal file
13
compression-core/crates/tokenizer/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[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 }
|
||||||
71
compression-core/crates/tokenizer/src/lib.rs
Normal file
71
compression-core/crates/tokenizer/src/lib.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//! 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
compression-core/fixtures/expected/tokenizer-manifest.json
Normal file
80
compression-core/fixtures/expected/tokenizer-manifest.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
6
compression-core/fixtures/tokenizer/sample-000.json
Normal file
6
compression-core/fixtures/tokenizer/sample-000.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "000",
|
||||||
|
"text": "Hello world! This is a test.",
|
||||||
|
"cl100k_tokens": 8,
|
||||||
|
"o200k_tokens": 8
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-001.json
Normal file
6
compression-core/fixtures/tokenizer/sample-001.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "001",
|
||||||
|
"text": "The quick brown fox jumps over the lazy dog.",
|
||||||
|
"cl100k_tokens": 10,
|
||||||
|
"o200k_tokens": 10
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-002.json
Normal file
6
compression-core/fixtures/tokenizer/sample-002.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "002",
|
||||||
|
"text": "🎉🎊 party time! emoji heavy sentence 🚀",
|
||||||
|
"cl100k_tokens": 15,
|
||||||
|
"o200k_tokens": 12
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-003.json
Normal file
6
compression-core/fixtures/tokenizer/sample-003.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "003",
|
||||||
|
"text": "JSON:\n{\"name\":\"test\",\"values\":[1,2,3]}",
|
||||||
|
"cl100k_tokens": 15,
|
||||||
|
"o200k_tokens": 15
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-004.json
Normal file
6
compression-core/fixtures/tokenizer/sample-004.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "004",
|
||||||
|
"text": "Code:\n```rust\nfn main() { println!(\"hi\"); }\n```",
|
||||||
|
"cl100k_tokens": 15,
|
||||||
|
"o200k_tokens": 15
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-005.json
Normal file
6
compression-core/fixtures/tokenizer/sample-005.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "005",
|
||||||
|
"text": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀",
|
||||||
|
"cl100k_tokens": 100,
|
||||||
|
"o200k_tokens": 50
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-006.json
Normal file
6
compression-core/fixtures/tokenizer/sample-006.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "006",
|
||||||
|
"text": "Поддерживается ли русский текст корректно? Проверяем длинное предложение с кириллицей и пунктуацией!",
|
||||||
|
"cl100k_tokens": 41,
|
||||||
|
"o200k_tokens": 23
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-007.json
Normal file
6
compression-core/fixtures/tokenizer/sample-007.json
Normal file
File diff suppressed because one or more lines are too long
6
compression-core/fixtures/tokenizer/sample-008.json
Normal file
6
compression-core/fixtures/tokenizer/sample-008.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "008",
|
||||||
|
"text": "t",
|
||||||
|
"cl100k_tokens": 1,
|
||||||
|
"o200k_tokens": 1
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-009.json
Normal file
6
compression-core/fixtures/tokenizer/sample-009.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "009",
|
||||||
|
"text": "",
|
||||||
|
"cl100k_tokens": 0,
|
||||||
|
"o200k_tokens": 0
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-010.json
Normal file
6
compression-core/fixtures/tokenizer/sample-010.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"id": "010",
|
||||||
|
"text": "Mixed 🎯 unicode 中文 한국어 + english + numbers 12345",
|
||||||
|
"cl100k_tokens": 18,
|
||||||
|
"o200k_tokens": 14
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-011.json
Normal file
6
compression-core/fixtures/tokenizer/sample-011.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
6
compression-core/fixtures/tokenizer/sample-012.json
Normal file
6
compression-core/fixtures/tokenizer/sample-012.json
Normal file
File diff suppressed because one or more lines are too long
77
compression-core/scripts/generate-fixtures.ts
Normal file
77
compression-core/scripts/generate-fixtures.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/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/`);
|
||||||
45
compression-core/scripts/verify-golden.ts
Normal file
45
compression-core/scripts/verify-golden.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/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 ✅");
|
||||||
@@ -127,12 +127,6 @@
|
|||||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ConnectionsListPanel.tsx": {
|
||||||
"TS2322": 2
|
"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": {
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderModalsPanel.tsx": {
|
||||||
"TS2322": 3,
|
"TS2322": 3,
|
||||||
"TS2739": 1,
|
"TS2739": 1,
|
||||||
@@ -147,9 +141,6 @@
|
|||||||
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
|
"src/app/(dashboard)/dashboard/providers/[id]/components/ProviderPlaygroundPanel.tsx": {
|
||||||
"TS2503": 1
|
"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": {
|
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": {
|
||||||
"TS2322": 1
|
"TS2322": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -121,6 +121,8 @@
|
|||||||
"tailwind-merge",
|
"tailwind-merge",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"tls-client-node",
|
"tls-client-node",
|
||||||
|
"turndown",
|
||||||
|
"turndown-plugin-gfm",
|
||||||
"tsup",
|
"tsup",
|
||||||
"tsx",
|
"tsx",
|
||||||
"type-coverage",
|
"type-coverage",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"_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_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_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).",
|
"_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).",
|
||||||
@@ -349,7 +350,7 @@
|
|||||||
"open-sse/executors/deepseek-web.ts": 1148,
|
"open-sse/executors/deepseek-web.ts": 1148,
|
||||||
"open-sse/executors/grok-web.ts": 1044,
|
"open-sse/executors/grok-web.ts": 1044,
|
||||||
"open-sse/executors/muse-spark-web.ts": 1405,
|
"open-sse/executors/muse-spark-web.ts": 1405,
|
||||||
"open-sse/handlers/chatCore.ts": 5020,
|
"open-sse/handlers/chatCore.ts": 5023,
|
||||||
"open-sse/handlers/imageGeneration.ts": 3101,
|
"open-sse/handlers/imageGeneration.ts": 3101,
|
||||||
"open-sse/handlers/responseSanitizer.ts": 1115,
|
"open-sse/handlers/responseSanitizer.ts": 1115,
|
||||||
"open-sse/handlers/search.ts": 1536,
|
"open-sse/handlers/search.ts": 1536,
|
||||||
@@ -385,8 +386,7 @@
|
|||||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
|
||||||
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
|
||||||
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
|
||||||
"_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": 1109,
|
||||||
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1153,
|
|
||||||
"src/app/api/providers/[id]/models/route.ts": 2250,
|
"src/app/api/providers/[id]/models/route.ts": 2250,
|
||||||
"src/app/api/v1/models/catalog.ts": 1549,
|
"src/app/api/v1/models/catalog.ts": 1549,
|
||||||
"src/lib/db/apiKeys.ts": 1529,
|
"src/lib/db/apiKeys.ts": 1529,
|
||||||
@@ -401,8 +401,8 @@
|
|||||||
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
"src/shared/components/RequestLoggerV2.tsx": 1629,
|
||||||
"src/shared/components/analytics/charts.tsx": 1035,
|
"src/shared/components/analytics/charts.tsx": 1035,
|
||||||
"src/shared/services/cliRuntime.ts": 1122,
|
"src/shared/services/cliRuntime.ts": 1122,
|
||||||
"src/sse/handlers/chat.ts": 1845,
|
"src/sse/handlers/chat.ts": 1846,
|
||||||
"src/sse/services/auth.ts": 2508,
|
"src/sse/services/auth.ts": 2512,
|
||||||
"tests/unit/account-fallback-service.test.ts": 1572,
|
"tests/unit/account-fallback-service.test.ts": 1572,
|
||||||
"tests/unit/provider-validation-specialty.test.ts": 2980,
|
"tests/unit/provider-validation-specialty.test.ts": 2980,
|
||||||
"open-sse/executors/hyperagent.ts": 1026
|
"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_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_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_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."
|
"_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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
chatgpt-web-codex-browser:
|
||||||
|
condition: service_started
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: runner-cli
|
target: runner-cli
|
||||||
@@ -67,6 +69,7 @@ services:
|
|||||||
- HOSTNAME=0.0.0.0
|
- HOSTNAME=0.0.0.0
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
- OMNIROUTE_BASE_PATH=${OMNIROUTE_BASE_PATH:-}
|
||||||
|
- CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||||
ports:
|
ports:
|
||||||
- "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
- "${PROD_DASHBOARD_PORT:-20130}:${DASHBOARD_PORT:-${PORT:-20128}}"
|
||||||
- "${PROD_API_PORT:-20131}:${API_PORT:-20129}"
|
- "${PROD_API_PORT:-20131}:${API_PORT:-20129}"
|
||||||
@@ -80,7 +83,19 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 15s
|
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:
|
volumes:
|
||||||
|
chatgpt-web-codex-browser-prod-data:
|
||||||
|
name: omniroute-chatgpt-web-codex-browser-prod-data
|
||||||
omniroute-prod-data:
|
omniroute-prod-data:
|
||||||
name: omniroute-prod-data
|
name: omniroute-prod-data
|
||||||
redis-prod-data:
|
redis-prod-data:
|
||||||
|
|||||||
@@ -98,6 +98,21 @@ services:
|
|||||||
args:
|
args:
|
||||||
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
OMNIROUTE_BASE_PATH: ${OMNIROUTE_BASE_PATH:-}
|
||||||
image: omniroute:web
|
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:
|
ports:
|
||||||
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
||||||
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
||||||
@@ -105,6 +120,20 @@ services:
|
|||||||
profiles:
|
profiles:
|
||||||
- web
|
- 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) ─────────────────
|
# ── Profile: cli (CLIs installed inside container) ─────────────────
|
||||||
omniroute-cli:
|
omniroute-cli:
|
||||||
<<: *common
|
<<: *common
|
||||||
@@ -252,6 +281,8 @@ services:
|
|||||||
- cliproxyapi
|
- cliproxyapi
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
chatgpt-web-codex-browser-data:
|
||||||
|
name: omniroute-chatgpt-web-codex-browser-data
|
||||||
cliproxyapi-data:
|
cliproxyapi-data:
|
||||||
name: cliproxyapi-data
|
name: cliproxyapi-data
|
||||||
redis-data:
|
redis-data:
|
||||||
|
|||||||
10
docker/chatgpt-web-codex-browser/Dockerfile
Normal file
10
docker/chatgpt-web-codex-browser/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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"]
|
||||||
72
docker/chatgpt-web-codex-browser/cdp-proxy.mjs
Normal file
72
docker/chatgpt-web-codex-browser/cdp-proxy.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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");
|
||||||
57
docker/devin-bridge/Dockerfile
Normal file
57
docker/devin-bridge/Dockerfile
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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"]
|
||||||
218
docker/devin-bridge/compose.yml
Normal file
218
docker/devin-bridge/compose.yml
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
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: {}
|
||||||
229
docker/devin-bridge/mock-devin.mjs
Executable file
229
docker/devin-bridge/mock-devin.mjs
Executable file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/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" } });
|
||||||
|
}
|
||||||
|
});
|
||||||
130
docker/devin-bridge/network-guard/policy.mjs
Normal file
130
docker/devin-bridge/network-guard/policy.mjs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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" };
|
||||||
|
}
|
||||||
136
docker/devin-bridge/network-guard/proxy.mjs
Normal file
136
docker/devin-bridge/network-guard/proxy.mjs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
27
docker/devin-bridge/run-claude-e2e.sh
Executable file
27
docker/devin-bridge/run-claude-e2e.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/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
|
||||||
52
docker/devin-bridge/run-claude-live-e2e.sh
Normal file
52
docker/devin-bridge/run-claude-live-e2e.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/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'
|
||||||
135
docker/devin-bridge/run-contract.mjs
Normal file
135
docker/devin-bridge/run-contract.mjs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/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");
|
||||||
181
docs/DEVIN_CLAUDE_BRIDGE.md
Normal file
181
docs/DEVIN_CLAUDE_BRIDGE.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# 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.
|
||||||
115
docs/DEVIN_CLAUDE_BRIDGE_PROGRESS.md
Normal file
115
docs/DEVIN_CLAUDE_BRIDGE_PROGRESS.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 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.
|
||||||
@@ -64,6 +64,8 @@ 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.
|
- [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.
|
- [MONITORING_SECTIONS.md](architecture/MONITORING_SECTIONS.md) — monitoring/costs dashboard navigation.
|
||||||
- [cluster-decisions.md](architecture/cluster-decisions.md) — optional sidecar/cluster profile decisions.
|
- [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/
|
## reference/
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user