diff --git a/.env.example b/.env.example index 3941efae2..4d8a72bc4 100644 --- a/.env.example +++ b/.env.example @@ -402,6 +402,12 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # ALL_PROXY=socks5://127.0.0.1:7890 # NO_PROXY=localhost,127.0.0.1 +# Max concurrent sockets per cached HTTP/SOCKS proxy dispatcher. +# Long-lived SSE streams such as Codex /v1/responses need more than one +# connection when multiple requests share the same account-level proxy. +# Set to 1 only for legacy diagnostics. Values above 256 are capped. +# OMNIROUTE_PROXY_DISPATCHER_CONNECTIONS=32 + # Proxy fail-open mode (default: false = fail-closed). # When false, a request whose assigned proxy fails to resolve is REFUSED rather than # falling back to a direct connection β€” prevents real-IP leaks in egress-controlled diff --git a/.github/workflows/build-fork.yml b/.github/workflows/build-fork.yml index 185d29524..5520cf746 100644 --- a/.github/workflows/build-fork.yml +++ b/.github/workflows/build-fork.yml @@ -25,7 +25,7 @@ jobs: packages: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a318a0d..a5144f941 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: API_KEY_SECRET: ci-lint-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -79,7 +79,7 @@ jobs: contents: read security-events: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -161,7 +161,7 @@ jobs: # fetch-depth: 0 β€” the OpenAPI breaking-change gate (oasdiff) reads the base # spec via `git show :docs/reference/openapi.yaml`; a shallow clone # would lack the base ref and the gate would self-skip (base-unresolved). - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 @@ -265,7 +265,7 @@ jobs: name: Docs Sync (Strict) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -297,7 +297,7 @@ jobs: # existing doc corpus is brought up to style. Promote to blocking once it converges. continue-on-error: true steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -320,7 +320,7 @@ jobs: name: i18n UI Coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -336,7 +336,7 @@ jobs: outputs: langs: ${{ steps.langs.outputs.langs }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - id: langs @@ -355,7 +355,7 @@ jobs: lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }} needs: i18n-matrix steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-python@v6 @@ -384,7 +384,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 @@ -414,7 +414,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -447,7 +447,7 @@ jobs: env: JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -472,7 +472,7 @@ jobs: JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation CSC_IDENTITY_AUTO_DISCOVERY: "false" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -507,7 +507,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -516,7 +516,7 @@ jobs: cache: npm - run: npm ci - run: npm run check:node-runtime - - run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" + - run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" test-vitest: name: Vitest (MCP / autoCombo / UI components) @@ -528,7 +528,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -548,7 +548,7 @@ jobs: node-24-compat: name: Node 24 Compatibility (${{ matrix.shard }}/2) runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 needs: build strategy: fail-fast: false @@ -559,7 +559,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -569,12 +569,12 @@ jobs: - run: npm ci - run: npm run check:node-runtime - run: npm run build - - run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" + - run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" node-26-compat: name: Node 26 Compatibility (${{ matrix.shard }}/2) runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 needs: build strategy: fail-fast: false @@ -585,7 +585,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -595,7 +595,7 @@ jobs: - run: npm ci - run: npm run check:node-runtime - run: npm run build - - run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" + - run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" test-coverage-shard: name: Coverage Shard (${{ matrix.shard }}/8) @@ -611,7 +611,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -636,7 +636,7 @@ jobs: --exclude=tests/** \ --exclude=**/*.test.* \ node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \ - --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" + --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts" - name: Upload raw shard coverage if: always() uses: actions/upload-artifact@v7 @@ -655,7 +655,7 @@ jobs: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -733,7 +733,7 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 @@ -853,7 +853,7 @@ jobs: DISABLE_SQLITE_AUTO_BACKUP: "true" OMNIROUTE_PLAYWRIGHT_SKIP_BUILD: "1" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -896,7 +896,7 @@ jobs: DATA_DIR: /tmp/omniroute-ci-${{ matrix.shard }} DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -916,7 +916,7 @@ jobs: API_KEY_SECRET: ci-test-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 01380d824..903eedd61 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -30,7 +30,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 72616d5f9..89fa78523 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,7 +19,7 @@ jobs: actions: read contents: read steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/dast-smoke.yml b/.github/workflows/dast-smoke.yml index 4c1548aad..e06968cf6 100644 --- a/.github/workflows/dast-smoke.yml +++ b/.github/workflows/dast-smoke.yml @@ -15,7 +15,7 @@ jobs: JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index e5e1fc757..ec91e0c22 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -42,7 +42,7 @@ jobs: IMAGE_NAME: diegosouzapw/omniroute steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} @@ -143,7 +143,7 @@ jobs: GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} @@ -243,7 +243,7 @@ jobs: PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }} diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 42344dfa4..2b4839153 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -27,7 +27,7 @@ jobs: version: ${{ steps.validate.outputs.version }} steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 @@ -84,7 +84,7 @@ jobs: deb_ext: .deb steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - name: Setup Node @@ -216,7 +216,7 @@ jobs: contents: write # softprops/action-gh-release creates the GitHub Release steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/mutation-redundancy.yml b/.github/workflows/mutation-redundancy.yml new file mode 100644 index 000000000..e584bea48 --- /dev/null +++ b/.github/workflows/mutation-redundancy.yml @@ -0,0 +1,63 @@ +name: Mutation Redundancy (disableBail, on-demand) +# One-off measurement to UNBLOCK R1 (test-redundancy prune). The nightly mutation run +# (nightly-mutation.yml) bails on the first kill, so `killedBy` lists only the FIRST +# killer β€” 🟠 redundant is understated and 🟒 unique overstated (see the caveat in +# scripts/quality/mutation-radiography.mjs). This workflow re-runs the SAME combo + +# chatCore leaf batches with stryker.disablebail.json (disableBail:true, incremental:false) +# so `killedBy` lists EVERY killer. Feed the uploaded reports to +# `node scripts/quality/mutation-radiography.mjs --candidates mutation-nobail-*/mutation.json` +# to get the accurate R1 prune-candidate list (πŸ”΄ empty βˆͺ 🟠 redundant) for human review. +# +# Batches mirror the nightly's leaf decomposition (d/e/f/g/h/i) rather than 2 mega-batches: +# disableBail is MORE expensive than bail (it never stops early), and Stryker only writes +# mutation.json on a SUCCESSFUL finish β€” a batch cancelled at the cap produces NO data β€” so +# smaller batches each fit the 300min headroom and run in parallel. auth/accountFallback and +# the security quartet are out of scope: R1 targets the combo/chatCore leaves. +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + stryker-nobail: + name: Stryker disableBail (batch ${{ matrix.batch.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + batch: + - name: d + mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts" + - name: e + mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts" + - name: f + mutate: "open-sse/services/combo/quotaScoring.ts,open-sse/services/combo/quotaStrategies.ts" + - name: g + mutate: "open-sse/handlers/chatCore/comboContextCache.ts,open-sse/handlers/chatCore/idempotency.ts,open-sse/handlers/chatCore/passthroughHelpers.ts,open-sse/handlers/chatCore/responseHeaders.ts,open-sse/handlers/chatCore/sanitization.ts,open-sse/handlers/chatCore/upstreamTimeouts.ts" + - name: h + mutate: "open-sse/handlers/chatCore/headers.ts,open-sse/handlers/chatCore/logTruncation.ts,open-sse/handlers/chatCore/memoryExtraction.ts,open-sse/handlers/chatCore/nonStreamingSse.ts,open-sse/handlers/chatCore/passthroughToolNames.ts,open-sse/handlers/chatCore/executorHelpers.ts" + - name: i + mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts" + timeout-minutes: 300 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - name: Run Stryker (disableBail) + env: + BATCH_MUTATE: ${{ matrix.batch.mutate }} + run: npx stryker run --config-file stryker.disablebail.json --mutate "$BATCH_MUTATE" + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@v7 + with: + name: mutation-nobail-${{ matrix.batch.name }} + path: reports/mutation/ + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/nightly-llm-security.yml b/.github/workflows/nightly-llm-security.yml index 60e354133..f03969023 100644 --- a/.github/workflows/nightly-llm-security.yml +++ b/.github/workflows/nightly-llm-security.yml @@ -12,7 +12,7 @@ jobs: name: promptfoo β€” injection guard (block mode, no secret) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -61,7 +61,7 @@ jobs: echo "run=false" >> "$GITHUB_OUTPUT" echo "::notice::PROMPTFOO_PROVIDER_KEY not set β€” skipping garak probes (advisory)." fi - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false if: steps.gate.outputs.run == 'true' diff --git a/.github/workflows/nightly-mutation.yml b/.github/workflows/nightly-mutation.yml index c57fb5150..656210666 100644 --- a/.github/workflows/nightly-mutation.yml +++ b/.github/workflows/nightly-mutation.yml @@ -36,11 +36,15 @@ jobs: # a successful finish); a job cancelled at the cap writes nothing, so the next run is cold # again β€” an infinite never-seeds loop. actions/cache is also branch-scoped, so each branch # (incl. release) must seed its OWN cache via a run with enough headroom. Measured cold runs - # (run 27801802713): a/b never finished even isolated; c=180min CANCELLED (4 modules), - # d=180min CANCELLED (3 combo modules), g=142min, h=132min, e=66, f=45, i=33. So a/b/c/d get - # 350min (job max 360) and g/h a 240min variance buffer; once seeded, later nightlies only - # re-test changed mutants and fit well under the default. NOTE: batch c's old "2 modules ~2h" - # assumption went stale when c grew to 4 modules β€” keep this budget in sync with the matrix. + # Measured cold totals (run 27801802713, extrapolated from the % at the 180/350 cancel point): + # auth ~375min (2301 mutants β€” EXCEEDS the 360min job max even isolated), accountFallback + # ~358min (1441), the c security quartet ~348min (1163), d ~197min (1316), g=142, h=132, e=66, + # f=45, i=33. The widely-covered modules blow the budget because the tap-runner re-runs every + # covering test file per mutant (a perTest fixed cost over ~138 test files, times thousands of + # mutants). A flat timeout bump cannot rescue auth (>360min max) β€” so the three over-budget + # batches are SPLIT so each half fits: auth->a1/a2 and accountFallback->b1/b2 by mutation range + # (`file:startLine-endLine`), the c quartet->c1/c2 by module pair. Splitting also seeds each + # sub-batch's own incremental cache, after which nightlies re-test only changed mutants. # Full coverage every night in parallel; wall-clock = the slowest batch's cold run until seeded. # Runs at stryker concurrency=4 with per-process DATA_DIR isolation # (tests/_setup/isolateDataDir.ts) β€” see _concurrency_comment in stryker.conf.json. @@ -48,23 +52,38 @@ jobs: fail-fast: false matrix: batch: - # Per-batch `timeout` (minutes) tiers the cold-seeding budget by measured cost β€” see the - # cold-run measurements in the comment above. Batches without the key default to 180. - # a/b (large isolated modules) + c/d (4 and 3 modules, observed to exceed 180min cold) get - # 350 (GitHub-hosted job max is 360); g/h (142/132min cold β€” within runner-variance distance - # of the 180 cap) get a 240 buffer. e/f/i (33-66min) stay at the 180 default. - - name: a - mutate: "src/sse/services/auth.ts" - timeout: 350 - - name: b - mutate: "open-sse/services/accountFallback.ts" - timeout: 350 - - name: c - mutate: "src/server/authz/routeGuard.ts,src/shared/utils/circuitBreaker.ts,open-sse/utils/error.ts,open-sse/utils/publicCreds.ts" - timeout: 350 + # Per-batch `timeout` (minutes) tiers the cold-seeding budget by measured cost; batches + # without the key default to 180. Cold-run profiling (run 27801802713) showed the + # widely-covered modules need FAR more than the 180 cap because the tap-runner re-runs every + # covering test file per mutant: auth ~375min (2301 mutants, EXCEEDS the 360min GitHub job + # max even isolated), accountFallback ~358min, the c security quartet ~348min β€” none fit a + # single job. So auth/accountFallback are split by MUTATION RANGE (`file:startLine-endLine`, + # ~half the mutants each) into a1/a2, b1/b2; the c quartet is split by MODULE pair into + # c1/c2. d (3 combo modules, ~197min) stays whole. Split-heavy batches get 300min headroom + # for their cold seeding run; once each batch completes once and writes + # stryker-incremental.json, later runs re-test only changed mutants and finish far faster. + # g/h (142/132min cold) keep a 240 buffer; e/f/i (33-66min) keep the 180 default. + - name: a1 + mutate: "src/sse/services/auth.ts:1-1109" + timeout: 300 + - name: a2 + mutate: "src/sse/services/auth.ts:1110-2218" + timeout: 300 + - name: b1 + mutate: "open-sse/services/accountFallback.ts:1-863" + timeout: 300 + - name: b2 + mutate: "open-sse/services/accountFallback.ts:864-1726" + timeout: 300 + - name: c1 + mutate: "src/server/authz/routeGuard.ts,src/shared/utils/circuitBreaker.ts" + timeout: 300 + - name: c2 + mutate: "open-sse/utils/error.ts,open-sse/utils/publicCreds.ts" + timeout: 300 - name: d mutate: "open-sse/services/combo/comboStructure.ts,open-sse/services/combo/autoStrategy.ts,open-sse/services/combo/validateQuality.ts" - timeout: 350 + timeout: 300 - name: e mutate: "open-sse/services/combo/shadowRouting.ts,open-sse/services/combo/targetSorters.ts,open-sse/services/combo/comboPredicates.ts,open-sse/services/combo/rrState.ts,open-sse/services/combo/comboData.ts" - name: f @@ -77,11 +96,15 @@ jobs: timeout: 240 - name: i mutate: "open-sse/handlers/chatCore/telemetryHelpers.ts,open-sse/handlers/chatCore/memorySkillsInjection.ts,open-sse/handlers/chatCore/semanticCache.ts" - # Per-batch budget: a/b/c/d override to 350min, g/h to 240min; the rest default to 180min. - # `matrix.batch.timeout` is null for batches without the key -> `|| 180`. + # Per-batch budget: split-heavy batches (a1/a2/b1/b2/c1/c2/d) override to 300min, g/h to 240min; + # the rest default to 180min. `matrix.batch.timeout` is null for batches without the key -> `|| 180`. + # NOTE: a1+a2 both mutate auth.ts (disjoint line ranges) and b1+b2 both mutate accountFallback.ts; + # when merging the per-batch mutation.json for radiography/scores, same-file mutants from sibling + # ranges must be UNIONED (scripts/check/check-mutation-ratchet.mjs::measureMutationScores and + # scripts/quality/mutation-radiography.mjs both merge per file). timeout-minutes: ${{ matrix.batch.timeout || 180 }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -109,3 +132,32 @@ jobs: path: reports/mutation/ if-no-files-found: warn retention-days: 14 + + # Aggregation gate (T3): each split batch emits a PARTIAL view of a mutated file + # (auth.ts lives in a1+a2, accountFallback in b1+b2), so a PER-BATCH ratchet would + # only ever see half a file vs the whole-file baseline. This job runs AFTER every + # batch, downloads all reports, and ratchets the MERGED per-module scores + # (check-mutation-ratchet UNIONS same-file mutants across reports) against the + # dedicatedGate `mutationScore.*` floors in quality-baseline.json (seeded ~2pt below + # the first full measurement). Blocking: a module dropping below its floor fails the + # run. Missing reports (e.g. an artifact-upload flake) are skipped, never failed. + mutation-ratchet: + name: Mutation score ratchet (blocking) + needs: stryker + if: always() + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Download all mutation reports + uses: actions/download-artifact@v8 + with: + pattern: mutation-report-* + path: reports/all + - name: Ratchet merged per-module mutation scores + run: node scripts/check/check-mutation-ratchet.mjs reports/all/*/mutation.json --ratchet diff --git a/.github/workflows/nightly-property.yml b/.github/workflows/nightly-property.yml index 70492fb6d..776426573 100644 --- a/.github/workflows/nightly-property.yml +++ b/.github/workflows/nightly-property.yml @@ -10,7 +10,7 @@ jobs: property-random-seed: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 diff --git a/.github/workflows/nightly-resilience.yml b/.github/workflows/nightly-resilience.yml index 7a9d2d6f8..ad7a92ee4 100644 --- a/.github/workflows/nightly-resilience.yml +++ b/.github/workflows/nightly-resilience.yml @@ -12,7 +12,7 @@ jobs: name: Heap-growth gate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -26,7 +26,7 @@ jobs: name: Resilience chaos (fault injection) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -40,7 +40,7 @@ jobs: name: k6 load/soak runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -74,3 +74,37 @@ jobs: - name: Stop server if: always() run: kill "$(cat server.pid)" || true + + a11y: + name: A11y axe (nightly, freeze-and-alert) + runs-on: ubuntu-latest + # The Playwright webServer (`start` mode) builds Next via build-next-isolated.mjs and + # boots the standalone server itself (waits on /api/monitoring/health, 15min webServer + # timeout). Unlike the per-PR test-e2e job, this nightly job has no pre-built artifact, + # so it self-builds β€” hence the generous job timeout. REQUIRE_AXE=1 makes the suite run + # the real axe analysis (the 4 page tests are gated to nightly so per-PR e2e stays fast) + # and makes the meta-test fail loudly if @axe-core/playwright ever goes missing. + timeout-minutes: 30 + env: + JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-test-api-key-secret-long + DISABLE_SQLITE_AUTO_BACKUP: "true" + REQUIRE_AXE: "1" + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@v5.0.5 + with: + path: ~/.cache/ms-playwright + key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: playwright-chromium-${{ runner.os }}- + - run: npx playwright install --with-deps chromium + - name: Run axe a11y suite (self-building webServer) + run: npx playwright test tests/e2e/a11y.spec.ts diff --git a/.github/workflows/nightly-schemathesis.yml b/.github/workflows/nightly-schemathesis.yml index c249c158a..0ee4149c8 100644 --- a/.github/workflows/nightly-schemathesis.yml +++ b/.github/workflows/nightly-schemathesis.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d2659584f..354381d4f 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -55,7 +55,7 @@ jobs: packages: write # publish to npm.pkg.github.com steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false # Need full tag history to compare against highest semver when @@ -202,7 +202,7 @@ jobs: id-token: write # npm provenance steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/opencode-plugin-ci.yml b/.github/workflows/opencode-plugin-ci.yml index 97fd14381..0925d4c7f 100644 --- a/.github/workflows/opencode-plugin-ci.yml +++ b/.github/workflows/opencode-plugin-ci.yml @@ -32,7 +32,7 @@ jobs: matrix: node: ["22", "24"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 diff --git a/.github/workflows/opencode-provider-ci.yml b/.github/workflows/opencode-provider-ci.yml index 0d7f93d18..52a206dd3 100644 --- a/.github/workflows/opencode-provider-ci.yml +++ b/.github/workflows/opencode-provider-ci.yml @@ -32,7 +32,7 @@ jobs: matrix: node: ["20", "22", "24"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest needs: test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v6 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 61015b27f..b6cfcb2e8 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -27,7 +27,7 @@ jobs: API_KEY_SECRET: ci-lint-api-key-secret-long DISABLE_SQLITE_AUTO_BACKUP: "true" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 17dbc0723..bb3a48272 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -21,7 +21,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index ab3fc6e7e..c26ee334f 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -12,7 +12,7 @@ jobs: container: image: semgrep/semgrep steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Run semgrep (advisory) diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index 4de2789a3..c3b095f75 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup Node uses: actions/setup-node@v6 diff --git a/.husky/pre-commit b/.husky/pre-commit index c302cfb65..ec14ffcd2 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -10,3 +10,4 @@ fi npx lint-staged node scripts/check/check-docs-sync.mjs npm run check:any-budget:t11 +node scripts/check/check-tracked-artifacts.mjs diff --git a/.vscode/settings.json b/.vscode/settings.json index c5699ad0f..261dc258a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,15 @@ }, "git.ignoreLimitWarning": true, + // ─── Git: nΓ£o adicionar os ~44 repos aninhados (9 worktrees + _references/* + + // _mono_repo/* + _ideia/_tasks/.agents) ao Source Control. Era a causa do + // "validando muito": um git status + watcher por repo. SΓ³ o repo raiz fica. ─── + "git.autoRepositoryDetection": false, + "git.repositoryScanMaxDepth": 0, + "git.detectSubmodules": false, + "git.autofetch": false, + "git.autorefresh": true, + // ─── Performance: nΓ£o seguir o symlink de node_modules criado pelas worktrees ─── // As worktrees em .worktrees/ apontam node_modules para o checkout principal; // sem isto a busca atravessa o symlink mesmo com node_modules excluΓ­do. diff --git a/CHANGELOG.md b/CHANGELOG.md index 442f58190..76af7585c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,97 @@ --- +## [3.8.30] β€” 2026-06-20 + +### ✨ New Features + +- **feat(dashboard): category (media serviceKind) filter on the providers page** β€” `/dashboard/providers` gains a media-category filter row (Image / Video / Music / Textβ†’Speech / Speechβ†’Text / Embedding) that composes with the existing search, free-only and "show configured only" filters. Membership is derived from the backend media registries (a provider that serves a kind is surfaced even if it never declared `serviceKinds`), keeping the UI in lockstep with the backend. ([#4240](https://github.com/diegosouzapw/OmniRoute/issues/4240)) +- **feat(combo): per-step account allowlist β€” scope a round-robin/weighted step to a subset of a provider's connections** β€” a combo model step can now carry a first-class account allowlist so a round-robin (or weighted) strategy is scoped to a chosen subset of a provider's connections (e.g. only `foo1`+`foo2` out of `foo1..foo4`) without hand-pinning one step per account. Empty = the whole active pool (unchanged). When a step both has an allowlist and is tag-routed, the two intersect (most-restrictive wins); a single pinned account still takes precedence. The combo builder's Precision step editor gains an optional "Restrict to accounts" picker. ([#3266](https://github.com/diegosouzapw/OmniRoute/issues/3266)) +- **feat(providers): add OpenAdapter, dit.ai and TokenRouter as OpenAI-compatible providers** β€” three community-requested OpenAI-compatible aggregators now register as standard named OpenAI-style providers with live `/v1/models` discovery (the zenmux pattern), falling back to a seeded catalog when the upstream list is unavailable: **OpenAdapter** (`https://api.openadapter.in/v1`, free tier, 70+ open-source models β€” [#4239](https://github.com/diegosouzapw/OmniRoute/issues/4239)), **dit.ai** (`https://api.dit.ai/v1`, dynamic-pricing router/gateway β€” [#4155](https://github.com/diegosouzapw/OmniRoute/issues/4155)), and **TokenRouter** (`https://api.tokenrouter.com/v1`, free MiniMax model β€” [#3841](https://github.com/diegosouzapw/OmniRoute/issues/3841), thanks @FerLuisxd). No custom executor/translator β€” default OpenAI passthrough. +- **feat(api): `x-omniroute-no-memory` request header β€” per-request opt-out of memory/skills injection** β€” clients that manage their own context (e.g. their own RAG/memory) can send `x-omniroute-no-memory: true` (mirrors the existing `x-omniroute-no-cache` convention) to skip the gateway injecting up to `memorySettings.maxTokens` (~2k) tokens of memory **and** skills context into that chat request β€” avoiding the token/cost inflation it otherwise adds on every call. Absent the header, behavior is unchanged. (PRD-2026-06-19-no-memory-header) +- **feat(dashboard): MITM tool card lists the exact hosts-file entries to add manually** β€” the CLI-tools MITM card's "How it works" section now lists the full set of `127.0.0.1 ` lines for the selected tool (sourced from the canonical MITM target registry) instead of a single example domain. Users on locked-down machines β€” where the automatic, sudo-gated hosts-file edit isn't available β€” can now copy every required entry by hand. (thanks @mrcyclo) +- **feat(cli): `omniroute launch-codex` + `setup-codex` β€” run/configure the Codex CLI against OmniRoute** β€” a launcher and setup command that point the Codex CLI at an OmniRoute endpoint (remote-mode aware). ([#4270](https://github.com/diegosouzapw/OmniRoute/pull/4270)) +- **feat(cli): Claude Code launcher + setup β€” remote mode + profiles** β€” `omniroute launch`/`setup` for Claude Code with remote-mode support and named connection profiles. ([#4274](https://github.com/diegosouzapw/OmniRoute/pull/4274)) +- **feat(cli): OpenCode setup β€” OpenAI-compatible provider + remote-aware plugin** β€” `setup-opencode` registers OmniRoute as an OpenAI-compatible provider for OpenCode and installs a remote-aware plugin. ([#4277](https://github.com/diegosouzapw/OmniRoute/pull/4277)) +- **feat(cli): one-command setup for popular AI coding tools** β€” new `setup-*` commands that configure each tool to talk to OmniRoute: **Cline** ([#4280](https://github.com/diegosouzapw/OmniRoute/pull/4280)), **Kilo Code** ([#4284](https://github.com/diegosouzapw/OmniRoute/pull/4284)), **Continue** ([#4289](https://github.com/diegosouzapw/OmniRoute/pull/4289)), **Cursor** ([#4291](https://github.com/diegosouzapw/OmniRoute/pull/4291)), **Roo Code** ([#4292](https://github.com/diegosouzapw/OmniRoute/pull/4292)), **Crush** ([#4298](https://github.com/diegosouzapw/OmniRoute/pull/4298)), **Goose** ([#4300](https://github.com/diegosouzapw/OmniRoute/pull/4300)), **Qwen Code** ([#4301](https://github.com/diegosouzapw/OmniRoute/pull/4301)), **Aider** ([#4302](https://github.com/diegosouzapw/OmniRoute/pull/4302)) and the **Gemini CLI** (native `/v1beta`) ([#4303](https://github.com/diegosouzapw/OmniRoute/pull/4303)). +- **feat(providers): provider model sweep β€” live discovery, refreshed catalogs, dead-provider cleanup** β€” a broad sweep that enables live `/v1/models` discovery for more OpenAI-style providers (the zenmux pattern), refreshes the seeded catalogs with current models, and marks dead providers `deprecated`. ([#4324](https://github.com/diegosouzapw/OmniRoute/pull/4324)) +- **feat(mitm): translate Antigravity cloudcode end-to-end (Gap B)** β€” the MITM decrypt path now translates Antigravity `cloudcode` traffic end-to-end. ([#4299](https://github.com/diegosouzapw/OmniRoute/pull/4299)) +- **feat(keys): per-key USD usage quota controls** β€” an API key can now carry a USD spend quota that caps its usage once the threshold is reached. ([#4327](https://github.com/diegosouzapw/OmniRoute/pull/4327) β€” thanks @Witroch4) + +### πŸ”§ Changed + +- **change(memory): memory is now OFF by default** β€” `DEFAULT_MEMORY_SETTINGS.enabled` now defaults to `false`. Enabling memory injects up to ~2,000 tokens of retrieved context into **every** chat request (and that context is billed), which was a surprising default for new installs and for clients with their own context. Memory is now an explicit opt-in: installs that already enabled it keep it on; installs that never configured it default to off. The Settings β†’ Memory panel now shows a token-cost warning when memory is enabled. (PRD-2026-06-19-no-memory-header) + +### πŸ› Fixed + +- **fix(compliance): startup cleanup honors the dashboard data-retention setting instead of always trimming to 7 days** β€” on every restart, `cleanupExpiredLogs()` (run at startup) read retention only from the `CALL_LOG_RETENTION_DAYS` / `APP_LOG_RETENTION_DAYS` env vars, which default to **7 days** when unset, and trimmed `usage_history` (the Usage Analysis data) before the dashboard-based `runAutoCleanup()` β€” which respects the configured retention β€” ever ran. So a dashboard "Data Retention" of 90 days was silently overridden and the Usage Analysis page only ever showed the last 7 days after a restart. Retention now follows the precedence **explicit env var β†’ dashboard DB setting β†’ 7-day default**, per table (`usage_history`β†’`usageHistory`, `call_logs`/`proxy_logs`/`request_detail_logs`β†’`callLogs`, `mcp_tool_audit`β†’`mcpAudit`); an operator who sets the env var still wins, and non-DB deployments still fall back to it. ([#4354](https://github.com/diegosouzapw/OmniRoute/issues/4354) β€” thanks @akbardwi) +- **fix(providers): bailian-coding-plan static fallback catalog matches the registry (10 models)** β€” the provider-model sweep (#4324) added four current Model Studio coding-plan models (`qwen3.7-plus`, `qwen3-coder-plus`, `qwen3-coder-next`, `glm-4.7`) to the `bailian-coding-plan` registry entry but missed the static fallback mirror in `staticModels.ts`, which still listed only the older six. The static catalog (served when live discovery is unavailable) therefore diverged from the registry, and the existing static↔registry parity test went red on the release branch (only surfacing when test-impact analysis happened to select it). The static mirror now carries all ten models in registry order, restoring parity. ([#4324](https://github.com/diegosouzapw/OmniRoute/pull/4324)) +- **fix(executors): ArenaLLM accepts LMArena's split Supabase SSR auth cookie** β€” LMArena migrated to `@supabase/ssr` chunked auth cookies: the single `arena-auth-prod-v1` cookie is now empty and the real session is split across `arena-auth-prod-v1.0`, `arena-auth-prod-v1.1`, … (ascending). A user who pasted the (now-empty) single cookie therefore sent an empty session and upstream rejected it as "invalid cookie". The LMArena executor now reconstructs the single cookie from its chunks β€” reading `.0`, `.1`, … in ascending numeric order until one is missing and concatenating their raw values (`@supabase/ssr`'s `combineChunks` rule: plain `join("")`, no base64-decode, no JSON-parse, the `base64-` prefix kept verbatim) β€” while preserving the rest of the pasted jar. A non-empty single cookie is still forwarded unchanged (back-compat). The credential UX now instructs pasting the **full Cookie header** and tracks the `.0`/`.1` storage keys. ([#4271](https://github.com/diegosouzapw/OmniRoute/issues/4271) β€” thanks @caussao) +- **fix(compression): preserve the cacheable prefix for automatic-cache providers** β€” OpenAI / Codex (and Azure-OpenAI) use _automatic_ prefix caching: the upstream caches the longest matching prefix of a request (system prompt + earliest messages) **without** any explicit `cache_control` markers in the body. The cache-aware compression guard only protected that prefix when the request carried explicit `cache_control`, so for automatic-cache providers the guard was skipped β€” and with compression enabled and `preserveSystemPrompt: false` (or a prefix-compressing mode like `aggressive`/`ultra`) it rewrote the system prompt / earliest messages, guaranteeing a cache miss and **higher** token spend through OmniRoute than going direct. The guard now treats a caching provider as sufficient on its own (`isCachingProvider` alone, independent of `cache_control`) to skip the system prompt and downgrade prefix-compressing modes, and OpenAI/Codex/Azure are now recognized as caching providers. Compression is still off by default β€” this only affects operators who enabled it with prefix preservation turned off. ([#3955](https://github.com/diegosouzapw/OmniRoute/issues/3955)) +- **fix(executors): DuckDuckGo AI Chat uses duckduckgo.com (fixes 400)** β€” the DuckDuckGo AI Chat executor fetched status/chat and set `Origin`/`Referer` against `https://duck.ai` while still sending `Sec-Fetch-Site: same-origin`, so the request's same-origin triplet (host + Origin + Referer) was inconsistent and the backend rejected it with HTTP 400. All current DDG reverse-engineering references β€” and the provider registry's own `baseUrl` β€” use `https://duckduckgo.com`; the executor now uses it consistently for the status URL, chat URL, `Origin`, and `Referer` (the same-origin header is now coherent). The `x-fe-version` scrape regex also required a 40-hex tail but the real served token has a 20-hex tail (e.g. `serp_20250401_100419_ET-19d438eb199b2bf7c300`), so it silently fell back to a hardcoded default; the pattern is relaxed to a bounded `{20,40}` tail (still ReDoS-safe). This addresses the DuckDuckGo half of the report; the separate Chipotle/`chipotle` upstream breakage is tracked independently. ([#4037](https://github.com/diegosouzapw/OmniRoute/issues/4037) β€” thanks @daniij) +- **fix(security): bound the prompt-injection scan to the first 16 KB (hot-path perf)** β€” the prompt-injection guard joined every message/system string into one buffer and ran several regexes over the **whole** thing on every chat request, with no size cap β€” so a 300 KB body (pasted code, RAG context) meant O(body) CPU scanning on the hot path, a self-inflicted latency/GC source under concurrency. Both detection call sites (`detectInjection` in `inputSanitizer.ts` and the custom-pattern scan in `promptInjection.ts`) now slice the joined text to the first **16 KB** (`MAX_INJECTION_SCAN_BYTES`) before the regex loop. Injection directives sit near the top of a prompt, so the generous cap preserves real detection while scanning only a bounded prefix; the existing 10 MB body-size cap (which protects ingestion) is unchanged. ([#3932](https://github.com/diegosouzapw/OmniRoute/issues/3932) β€” thanks @KooshaPari) +- **fix(sse): retry direct-connection socket failures on a fresh socket (fewer `502` bursts)** β€” the default direct-connection undici dispatcher pools keep-alive sockets for up to 4 s, but some edges (e.g. `nvidia`, `opencode-zen`) silently close idle keep-alive sockets within that window, so the next request reusing a pooled socket fails with `UND_ERR_SOCKET` ("other side closed") β€” in bursts. `proxyFetch` already retried once on such transient errors, but the retry reused the **same** pooled dispatcher and could grab another stale socket, then fell through to native fetch (which also pools) β†’ the job sat in the rate-limit queue until the 30 s timeout β†’ `502` + circuit-breaker open. The retry now uses a dedicated **no-keep-alive / no-pipelining** dispatcher so it opens a brand-new socket that can't be a dead pooled one; the first attempt still uses the pooled dispatcher (healthy keep-alive reuse is preserved). Complements the v3.8.29 diagnostics (`describeFetchCause`, #4281). ([#4252](https://github.com/diegosouzapw/OmniRoute/issues/4252) β€” thanks @klimadev) +- **fix(sse): combo now stops at the first body-specific 400 instead of trying every target** β€” the `#2101` guard that detects a body-specific 400 (context overflow / malformed / model-access-denied, e.g. "model is not supported when using Codex with a ChatGPT account") logged "stopping combo" but executed a bare `break`, which only exited the inner retry loop; `executeTarget` then returned `null` and the outer target loop treated that as "this target produced nothing" and advanced to the next model. A combo of N targets that all reject the same request body therefore marched through all N (the report shows a 143-model Codex combo iterating every target), wasting upstream calls and per-attempt work. The guard now surfaces the 400 via the `{ ok, response }` contract (mirroring the 499 client-disconnect path) so the combo resolves and stops immediately. ([#4279](https://github.com/diegosouzapw/OmniRoute/issues/4279)) +- **fix(sse): non-streaming combo over a Responses-API target no longer returns empty content** β€” a Responses-API target (codex/`cx`) streams from upstream even on `stream:false`, and its terminal `response.completed` snapshot can carry a non-empty `output` that lacks the assistant message item (e.g. only a `reasoning` item) while the streamed `output_text` deltas had reconstructed the full message. The SSEβ†’JSON aggregator preferred the terminal `output` wholesale, dropping the reconstructed text β†’ HTTP 200 with empty content (hit notably via n8n, which defaults to `stream:false`). The aggregator now falls back to the reconstructed delta output when the terminal output has no message item but the reconstruction does; the terminal snapshot still wins whenever it already carries the message. ([#3948](https://github.com/diegosouzapw/OmniRoute/issues/3948)) +- **fix(executors): preserve tool-name casing on native Claude OAuth (`read` no longer leaks back as `Read`)** β€” native Claude OAuth traffic runs through an anti-fingerprint tool-name cloak that renames a tool literally named `read` to `Read` on the wire and records the reverse alias on a non-enumerable `_toolNameMap`, which the response side uses to restore the client's original casing. Since v3.8.27 the executor returned a JSON-round-tripped copy of the body as `transformedBody`, and that round-trip dropped the non-enumerable map β€” so the restore saw an empty map and the cloaked `Read` streamed verbatim to the client, corrupting the tool name. The executor now re-attaches the cloak map onto the serialized body (mirroring the Antigravity executor), so tool-name casing round-trips correctly. ([#4307](https://github.com/diegosouzapw/OmniRoute/issues/4307) β€” thanks @dev-cj) +- **fix(api): cache-HIT `X-OmniRoute-Response-Cost` now reports the incremental cost (β‰ˆ0), not the original** β€” on a semantic-cache HIT the gateway serves the stored response **without** an upstream call, but `X-OmniRoute-Response-Cost` was reporting the original call's full cost (recomputed from the cached `usage`). A consumer summing `response-cost` for billing was therefore charging for responses that cost β‰ˆ$0 to serve (and stale entries could inflate it). Cache hits now bill `X-OmniRoute-Response-Cost: 0.0000000000` (the real incremental cost), and the avoided cost is surfaced in a new **`X-OmniRoute-Cost-Saved`** header for cache analytics β€” mirroring the existing `tokens_saved` concept. The MISS path is unchanged. (PRD-2026-06-19-cache-hit-cost-reporting) +- **fix(models): imported vision-capable models keep their vision capability** β€” after importing a provider key, vision-capable models (e.g. OpenRouter models whose `architecture` declares image input, and other synced providers) were listed as text-only in `/v1/models` and the dashboard β€” even though image requests actually worked. Synced model records never captured the vision flag, and the catalog's OpenRouter live-enrichment (which derives vision from `architecture.input_modalities`) is skipped once a provider has synced models. Discovery now captures `supportsVision` at sync time (from `architecture.input_modalities`, the string `architecture.modality`, or a top-level `input_modalities`), mirroring the existing `supportsThinking` capture, and the catalog surfaces `capabilities.vision` for synced models. ([#4264](https://github.com/diegosouzapw/OmniRoute/issues/4264) β€” thanks @FerLuisxd) +- **fix(providers): Cloudflare Workers AI model discovery shows model names, not UUIDs** β€” importing a Cloudflare Workers AI key listed models with internal UUID identifiers (e.g. `429b9e8b-d99e-…`) instead of their usable slugs (`@cf/meta/llama-3.1-8b-instruct`). Cloudflare's `/ai/models/search` returns `{ id: "", name: "@cf/…" }`, and discovery was passing the raw objects through β€” so the UUID `id` became the callable model id. The `cloudflare-ai` discovery now maps each result's `name` β†’ id, surfacing the real `@cf/…` model ids. ([#4259](https://github.com/diegosouzapw/OmniRoute/issues/4259) β€” thanks @FerLuisxd) +- **fix(translator): clamp Responses API `call_id` to 64 characters** β€” the OpenAI Responses API rejects `call_id` values longer than 64 characters with a 400. Long upstream tool-call ids (some clients emit ids well over the limit) are now clamped deterministically on both the `function_call` item and its matching `function_call_output`, so the pair stays matched through the orphaned-output filter and the request is accepted. (thanks @anuragg-saxenaa, @ngapngap) +- **fix(oauth): GitHub Copilot token refresh now sends the public client_id** β€” the `github` provider config never carried a `clientId`, so GitHub OAuth `refresh_token` exchanges either omitted `client_id` or sent the literal string `undefined` (and a bogus `client_secret=undefined`), which GitHub rejects β€” leaving a Copilot connection stuck once its short-lived token expired and the long-lived refresh path was needed. The provider now resolves its public device-flow `client_id` from the embedded public credential and omits `client_secret` entirely (GitHub's Copilot app is a public client with no secret). (thanks @baslr) +- **fix(translator): a tool property named `pattern` survives Gemini/Antigravity schema sanitization** β€” the Gemini schema sanitizer strips JSON-Schema constraint keywords Gemini rejects (`pattern`, `minLength`, …) at every nesting level, but it also deleted any tool **property** literally *named* one of those keywords. glob/grep tools declare a property called `pattern`, so on `ag/*` (Antigravity) backends that argument (and its `required` entry) was silently dropped, breaking the tools. Keyword stripping is now position-aware: it only removes constraint keywords at the schema-node level and never against the user-defined names inside a `properties` map. A genuine string-level `pattern` *constraint* is still stripped. (thanks @youthanh) +- **fix(translator): MCP `namespace` tools flatten to individual functions on the Responsesβ†’Chat path** β€” when a Codex CLI client routes a Responses-API request to a non-Codex backend (e.g. `kr/claude-opus-4.7`), each MCP server is declared as a `namespace` tool (`{ type:"namespace", name, tools:[…] }`). The Responsesβ†’Chat translator had no `namespace` branch, so the whole group collapsed into a single empty-schema function named `mcp____` and every MCP call returned `unsupported call: mcp____`, breaking all MCP-based workflows (context7, codegraph, custom MCPs) for that combination. The translator now expands a namespace into one Chat function per sub-tool (preserving each sub-tool's name and parameters); an empty namespace yields no tools instead of a broken placeholder. The native Codex passthrough path was already correct. (thanks @V13t4nh) +- **fix(cli): the active remote-context credential wins over an ambient `OMNIROUTE_API_KEY`** β€” when a remote context is selected, its scoped access token now takes precedence over an `OMNIROUTE_API_KEY` present in the environment, so the connected remote is targeted as expected. ([#4364](https://github.com/diegosouzapw/OmniRoute/pull/4364)) +- **fix(cli): wire the `contexts` command into the CLI program** β€” the `omniroute contexts` command (list/switch saved remote contexts) was implemented but never registered, so it was unreachable; it is now wired into the CLI program. ([#4369](https://github.com/diegosouzapw/OmniRoute/pull/4369)) +- **fix(mitm): mask bare `Bearer ` header values in the Traffic Inspector** β€” the inspector now redacts bare `Authorization: Bearer …` values so tokens don't leak into captured traffic. ([#4358](https://github.com/diegosouzapw/OmniRoute/pull/4358)) +- **fix(pricing): price the `gpt-5.x-pro` OpenAI models + align the opencode-go discovery test** β€” adds pricing for the gpt-5.x-pro models so cost telemetry reports a real cost instead of zero. ([#4355](https://github.com/diegosouzapw/OmniRoute/pull/4355)) +- **fix(sse): release the reader and cancel the stream on abort/error (no more Undici pool socket leak)** β€” on abort or a mid-stream error the response reader is released and the stream cancelled, preventing leaked pooled sockets that degraded later requests. ([#4309](https://github.com/diegosouzapw/OmniRoute/pull/4309) β€” thanks @Ardem2025) +- **fix(kiro): emit an early role-only start chunk to release the stream-readiness gate** β€” Kiro streams now send an initial role-only chunk so the stream-readiness gate releases promptly instead of stalling. ([#4311](https://github.com/diegosouzapw/OmniRoute/pull/4311) β€” thanks @artickc) +- **fix(dashboard): the proxy modal stops pre-filling new scopes with an unrelated proxy** β€” adding a new scope assignment no longer inherits a previously-selected proxy's configuration. ([#4312](https://github.com/diegosouzapw/OmniRoute/pull/4312)) +- **fix(open-sse): inner-ai stops silently rerouting unmatched models to `models[0]`** β€” an unmatched model id is no longer silently served by the first available model; the lookup now returns null and the request is handled explicitly. ([#4310](https://github.com/diegosouzapw/OmniRoute/pull/4310)) +- **fix(pollinations): handle auth-required premium models (claude, gemini, midjourney)** β€” premium Pollinations models that require authentication are now handled correctly instead of failing. ([#4266](https://github.com/diegosouzapw/OmniRoute/pull/4266) β€” thanks @oyi77) +- **fix(codex): isolate the Spark quota scope** β€” Codex Spark usage is tracked under its own quota scope so it no longer bleeds into other Codex quotas. ([#4293](https://github.com/diegosouzapw/OmniRoute/pull/4293) β€” thanks @xz-dev) +- **fix(dashboard): improve the API "try it" functionality** β€” fixes the request path used by the dashboard's API "try it" panel. ([#4296](https://github.com/diegosouzapw/OmniRoute/pull/4296) β€” thanks @edrickrenan) +- **fix: polyfill `crypto.randomUUID` for non-secure contexts** β€” restores UUID generation when the dashboard is served over a non-secure (plain-HTTP) origin where `crypto.randomUUID` is unavailable. ([#4287](https://github.com/diegosouzapw/OmniRoute/pull/4287) β€” thanks @pizzav-xyz) +- **fix(proxy): allow concurrent proxy dispatcher streams** β€” the proxy dispatcher no longer serializes streams, so concurrent requests through a proxied connection run in parallel. ([#4288](https://github.com/diegosouzapw/OmniRoute/pull/4288) β€” thanks @wilsonicdev) +- **fix(build): co-locate llmlingua SLM optionals into `dist/node_modules` (postinstall)** β€” the optional llmlingua SLM packages are co-located into the standalone build so the compression worker can actually spawn in production. ([#4286](https://github.com/diegosouzapw/OmniRoute/pull/4286)) +- **fix(mitm): surface AgentBridge traffic in the Traffic Inspector (D4 ingest)** β€” AgentBridge requests now appear in the Traffic Inspector. ([#4285](https://github.com/diegosouzapw/OmniRoute/pull/4285)) +- **fix(sse): surface undici `err.cause` on dispatcher failure** β€” dispatcher failures now flatten the cause chain (and `AggregateError`s) into the error detail for diagnosability. ([#4281](https://github.com/diegosouzapw/OmniRoute/pull/4281)) +- **fix(cli): harden `launch`/`launch-codex` with free-claude-code patterns** β€” the launchers adopt the hardened launch patterns ported from free-claude-code. ([#4278](https://github.com/diegosouzapw/OmniRoute/pull/4278)) +- **fix(compression): end-to-end audit β€” fixes across the whole compression flow** β€” a sweep of the compression pipeline fixing ultra/aggressive/lossless edge cases, accessibility-anchor handling, language detection, and mode decoupling. ([#4323](https://github.com/diegosouzapw/OmniRoute/pull/4323)) + +### πŸ§ͺ Tests + +- **test: align two tests left red by merged PRs** β€” re-aligns the db-rules classification count (#4335) and the LMArena split-cookie metadata test (#4271) after concurrent merges. ([#4346](https://github.com/diegosouzapw/OmniRoute/pull/4346)) +- **test(ci): reconcile the release/v3.8.30 baseline + test drift** β€” reconciles quality baselines and drifted tests accumulated on the release branch. ([#4276](https://github.com/diegosouzapw/OmniRoute/pull/4276)) + +### πŸ“ Maintenance + +- **refactor(combo): `ComboContext` + extract `phaseComboSetup` (god-file split, phase 1)** β€” begins decomposing the combo god-file by extracting combo setup into a context object, without touching dispatch/semaphore logic. ([#4326](https://github.com/diegosouzapw/OmniRoute/pull/4326)) +- **feat(quality): cap test-file size β€” anti-reinflation Layer 1** β€” freezes the existing god-tests and caps new test files at 800 lines to stop re-inflation. ([#4273](https://github.com/diegosouzapw/OmniRoute/pull/4273)) +- **feat(quality): seed per-module mutationScore floors + a blocking aggregation ratchet (T3)** β€” adds per-module mutation-score floors with a blocking aggregate gate. ([#4305](https://github.com/diegosouzapw/OmniRoute/pull/4305)) +- **feat(quality): make the a11y gate real (`@axe-core/playwright` in nightly)** β€” wires the previously-phantom accessibility gate into the nightly run with real baselines. ([#4321](https://github.com/diegosouzapw/OmniRoute/pull/4321)) +- **feat(quality): unblock R1 β€” test-redundancy measurement via `disableBail`** β€” enables the test-redundancy measurement that was previously blocked by fail-fast. ([#4322](https://github.com/diegosouzapw/OmniRoute/pull/4322)) +- **fix(quality): the complexity gate now covers `bin/` + `electron/`, and tracked-artifacts runs in pre-commit** β€” extends the complexity gate's scope and moves the tracked-artifacts check into the pre-commit hook. ([#4318](https://github.com/diegosouzapw/OmniRoute/pull/4318)) +- **fix(quality): restore release/v3.8.30 green β€” 3 latent reds from concurrent merges** β€” fixes three latent test reds surfaced by concurrent merges into the release branch. ([#4335](https://github.com/diegosouzapw/OmniRoute/pull/4335)) +- **fix(combo): keep `phaseComboSetup` under the complexity ceiling** β€” extracts a helper so the new combo setup phase stays under the complexity gate. ([#4338](https://github.com/diegosouzapw/OmniRoute/pull/4338)) +- **ci(mutation): split over-budget batches by range/pair so every batch fits the job cap** β€” re-splits the mutation batches so each fits the CI job budget. ([#4272](https://github.com/diegosouzapw/OmniRoute/pull/4272)) +- **chore(ci): align the electron audit gate to the root advisory policy** β€” the electron-workspace audit gate now follows the same advisory policy as the root. ([#4275](https://github.com/diegosouzapw/OmniRoute/pull/4275)) +- **chore(quality): reconcile the complexity/quality baselines across concurrent-merge drift** β€” rolls up the cycle's baseline reconciliations driven by concurrent merges into the release branch. ([#4330](https://github.com/diegosouzapw/OmniRoute/pull/4330), [#4336](https://github.com/diegosouzapw/OmniRoute/pull/4336), [#4370](https://github.com/diegosouzapw/OmniRoute/pull/4370)) +- **docs: ban AI-generation footers in commits/PRs/CHANGELOG (Hard Rule #16)** β€” codifies the prohibition on AI-generation footers and bot co-author trailers. ([#4328](https://github.com/diegosouzapw/OmniRoute/pull/4328)) +- **docs(design): add the OmniRoute design system and visual identity specification** β€” adds the design-system / visual-identity specification document. (thanks @diegosouzapw) + +### πŸ”’ Security + +- **fix(sse): harden the DuckDuckGo lite scraper sanitization (CodeQL)** β€” closes four HIGH CodeQL alerts in the no-key web-search scraper: `decodeEntities` now resolves `&` **last** so an already-escaped entity (e.g. `&lt;`) survives as literal text instead of being double-unescaped (`js/double-escaping`); `stripTags` decodes entities first, then strips tags in a loop to a fixpoint and drops any trailing unclosed `<…`, so entity-encoded markup like `<script>` can never reach the LLM/client as a live tag (`js/incomplete-multi-character-sanitization`); and the host checks in the search tests use `new URL().hostname` equality instead of substring `.includes` (`js/incomplete-url-substring-sanitization`). ([#4356](https://github.com/diegosouzapw/OmniRoute/pull/4356)) + +### πŸ”§ Dependencies + +- **fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security)** β€” addresses the undici SOCKS5-TLS / cache advisories and the dompurify advisory. ([#4306](https://github.com/diegosouzapw/OmniRoute/pull/4306)) +- **chore(deps): bump actions/checkout from 4 to 7** β€” CI checkout-action update. ([#4297](https://github.com/diegosouzapw/OmniRoute/pull/4297)) + +--- + ## [3.8.29] β€” 2026-06-19 ### ✨ New Features diff --git a/CLAUDE.md b/CLAUDE.md index 2050d1d15..3e308c536 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -501,7 +501,7 @@ the stale-enforcement added in Fase 6A.3. 13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` β€” pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`. 14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) β€” dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`. 15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check β€” leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`. -16. Never include `Co-Authored-By` trailers that credit an AI assistant, LLM, or automation account (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses). Such trailers route attribution to the bot account on GitHub, hiding the real author (`diegosouzapw`) in PR history. Human collaborators β€” including upstream PR authors and issue reporters being ported into OmniRoute β€” MAY and SHOULD be credited with standard `Co-authored-by: Name ` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this. +16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent β€” they route attribution to a bot account (or advertise AI authorship) and hide the real author (`diegosouzapw`): **(a)** `Co-Authored-By` trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses); **(b)** AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG β€” e.g. `πŸ€– Generated with [Claude Code]`, "Generated with Claude Code", "Made with ", or any `Co-authored-by: Claude/GPT/Copilot` line. This **overrides any harness, template, or tool default that auto-appends such a footer** (e.g. the Claude Code PR-body/commit default) β€” strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators β€” including upstream PR authors and issue reporters being ported into OmniRoute β€” MAY and SHOULD be credited with standard `Co-authored-by: Name ` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this. 17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check β€” a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`. 18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing β†’ "Bug fix / issue triage protocol" for the full decision tree. 19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch β€” never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow β†’ "Worktree isolation". diff --git a/DESING.md b/DESING.md new file mode 100644 index 000000000..e71f88c77 --- /dev/null +++ b/DESING.md @@ -0,0 +1,254 @@ +# OmniRoute β€” Design System & Visual Identity + +> **Status:** analysis + standardization plan (no code applied yet β€” this doc is the spec to approve before implementation). +> **Date:** 2026-06-16 Β· **Scope:** unify the OmniRoute dashboard (`src/`) with the marketing site (`_mono_repo/omnirouteSite/`) into **one visual identity** β€” same graph-paper grid background, same color tokens, standardized components. + +--- + +## 1. Purpose + +The marketing site (`viral.omniroute.online`, `why.omniroute.online`, `omniroute.online`) and the product dashboard should look like **one product**. The site already borrowed its palette from the dashboard β€” its `css/tokens.css` even says _"Palette mirrors the OmniRoute dashboard (src/app/globals.css)"_. So the two are already ~80% aligned at the color level. What's missing on the dashboard: + +1. The **graph-paper grid wallpaper** the site uses on every page. +2. A handful of **shared design tokens** the site has but the dashboard lacks (radius scale, brand gradient, `surface-2`, mono font). +3. **Component-level consistency** β€” a number of dashboard components bypass the theme tokens with hardcoded hex/rgba. + +This document is the analysis and the plan. **Nothing is changed until approved.** + +--- + +## 2. Principles + +- **Single source of truth = `src/app/globals.css`.** The site mirrors the dashboard, never the other way around. New tokens land in `globals.css` first. +- **Tokens, never literals.** Components consume semantic tokens (`bg-surface`, `text-primary`, `border-border`), never raw `#hex`. +- **Subtle, not loud.** The grid is a faint wallpaper that sits behind content β€” it must never reduce text contrast or fight the UI. +- **Theme-aware.** Everything works in both `.dark` (default-ish, the product's signature look) and light. +- **Surgical rollout.** Ship the grid + tokens first (low risk, high visibility), then component cleanups in waves. + +--- + +## 3. Current state β€” what's already aligned vs. what's not + +### 3.1 Colors β€” already unified βœ… + +Every brand color and surface already matches the site **by value** (only the names differ β€” dashboard prefixes with `--color-`). Verified in `src/app/globals.css:30-128`: + +| Concept | Site token (`tokens.css`) | Dashboard token (`globals.css`) | Match | +| -------------------------- | ------------------------------------------- | ------------------------------- | ------------ | +| primary | `--primary #e54d5e` | `--color-primary #e54d5e` | βœ… | +| primary-hover | `--primary-hover #c93d4e` | `--color-primary-hover #c93d4e` | βœ… | +| accent | `--accent #6366f1` | `--color-accent #6366f1` | βœ… | +| accent-2 | `--accent-2 #8b5cf6` | `--color-accent-hover #8b5cf6` | βœ… (renamed) | +| accent-3 | `--accent-3 #a855f7` | `--color-accent-light #a855f7` | βœ… (renamed) | +| success / warning / error | `#22c55e / #f59e0b / #ef4444` | identical | βœ… | +| traffic lights | `#ff5f56 / #ffbd2e / #27c93f` | identical | βœ… | +| dark bg / surface / border | `#0b0e14 / #161b22 / rgba(255,255,255,.08)` | identical | βœ… | +| light bg / surface / text | `#f9f9fb / #fff / #1a1a2e` | identical | βœ… | + +**Conclusion:** there is no color migration to do. The identity is already shared; we are _finishing_ it, not rebuilding it. + +### 3.2 Gaps β€” what the dashboard is missing + +| Gap | Site has | Dashboard | Action | +| ----------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------- | +| **Grid wallpaper** | `body::before` graph-paper, `--grid-line`, `--grid-size 46px`, `--section-alt` | none (flat `--color-bg`) | **Part A** | +| **Radius scale** | `--radius 14px`, `--radius-sm 9px` | none β€” primitives use ad-hoc `rounded-md/lg/xl` (6/8/12px) | **Part B** | +| **Brand gradient** | `--grad-brand 135deg primaryβ†’accent-3` | none β€” only a one-off `.bg-hero-gradient` | **Part B** | +| **Nested surface** | `--surface-2 #1c2230` | none | **Part B** | +| **Mono font** | `--font-mono` (ui-monospace stack) | none (code/terminal areas have no token) | **Part B** | +| **`text-muted` (dark)** | `#8b8b9e` | `#a1a1aa` (zinc-400) | reconcile β€” **Part B** | + +### 3.3 Theming mechanics (so we don't break anything) + +- **Tailwind v4, CSS-first** (no `tailwind.config.*`). Tokens are defined in `:root`/`.dark` and exposed to utilities via `@theme inline` (`globals.css:130-179`). +- **Dark via `.dark` class** on `` (`@custom-variant dark` at `globals.css:22`), toggled by a custom Zustand store (`src/store/themeStore.ts`), default theme = `system` (`src/shared/constants/appConfig.ts:11`). The site uses `html[data-theme="light"]` instead β€” **the mechanisms differ but never meet** (separate origins), so no conflict. We keep the dashboard's `.dark` mechanism. +- **Runtime primary override** exists (`themeStore.ts:85-97`, presets in `COLOR_THEMES`) β€” users can swap `--color-primary`. Any new token (gradient, etc.) that references `--color-primary` will inherit those overrides for free. βœ… + +--- + +## 4. Part A β€” The graph-paper grid background (headline ask) + +### 4.1 What it is + +The exact recipe from the site (`_mono_repo/omnirouteSite/css/base.css`): a **fixed, full-viewport pseudo-element** painting two 1px line gradients, sitting at `z-index:-1` behind all content. + +```css +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background-image: + linear-gradient(to right, var(--grid-line) 1px, transparent 1px), + linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px); + background-size: var(--grid-size) var(--grid-size); +} +``` + +**Why this works even though `body` has an opaque `background-color`:** a `::before` with `z-index:-1` paints _above_ the element's own background but _below_ its in-flow content. So `--color-bg` is the base fill, the grid is layered on top of it, and the app renders above the grid. + +### 4.2 Precedent already in the codebase + +`src/app/landing/page.tsx:16-26` **already implements this same grid per-page** β€” but with **red** lines (`#E54D5E`, opacity `0.06`) at **50px**, plus animated orbs. So the pattern is proven in the product; we are promoting it to a **global, theme-aware** wallpaper and (optionally) retiring the duplicate. + +### 4.3 Tokens to add (in `globals.css`) + +```css +:root { + /* light */ + --grid-line: rgba(0, 0, 0, 0.045); + --grid-size: 46px; + --section-alt: rgba(0, 0, 0, 0.022); +} +.dark { + /* dark */ + --grid-line: rgba(255, 255, 255, 0.035); + --section-alt: rgba(255, 255, 255, 0.018); +} +``` + +### 4.4 The single blocker + +The grid is global by construction (it covers the panel, `auth`/`login`, error pages β€” every route β€” at once). Exactly **one** element hides it inside the panel: + +- `src/shared/components/layouts/DashboardLayout.tsx:62` β€” the outer wrapper paints an opaque `bg-bg`: + + ```jsx +
+ ``` + + Everything below it is already transparent β€” `
` (`:93`), the scroll container (`:102`), the `max-w-7xl` inner (`:103`). So **removing `bg-bg` from this one line** lets the body grid show through the entire content area (the body's `--color-bg` remains the base fill underneath the grid). + + ```diff + -
+ +
+ ``` + +### 4.5 Chrome interaction (sidebar / header) + +- `Header` (`src/shared/components/Header.tsx:207`, `bg-bg`) and `Sidebar` (`src/shared/components/Sidebar.tsx:430`, `bg-sidebar`) stay **opaque** β†’ the grid shows in the **content area only**, with solid chrome framing it. This is the recommended, calm default and matches how the site separates chrome from canvas. +- _Optional vibrancy variant:_ make the header translucent (`bg-bg/80 backdrop-blur`) so the grid runs behind it. A `.bg-vibrancy` helper already exists (`globals.css:370`). **Decision D3 below.** + +### 4.6 Login / auth / error pages + +These render directly under `` (no panel chrome) and their page wrappers are mostly transparent β€” the global grid appears behind them automatically. One exception: `src/app/login/page.tsx:124,139` uses opaque `bg-bg` wrappers; soften the same way if we want the grid there too (minor, **D4**). + +### 4.7 Landing page + +`landing/page.tsx` keeps its richer animated background (orbs + vignette). Options: (a) leave it as-is (its own splash identity), or (b) align its grid to the global tokens (46px, neutral lines) for consistency. **Recommend (a)** β€” it's a marketing splash, not a panel screen. **Decision D5.** + +--- + +## 5. Part B β€” Token unification + +Add to `globals.css` (`:root` + `@theme inline`) so the dashboard gains the site's missing tokens. None of these change existing colors; they add the _missing_ primitives. + +```css +:root { + --surface-2: #f5f5fa; /* light: nested panels */ + --radius: 14px; + --radius-sm: 9px; + --grad-brand: linear-gradient(135deg, var(--color-primary), var(--color-accent-light)); + --font-mono: ui-monospace, "JetBrains Mono", "Fira Code", "SF Mono", monospace; +} +.dark { + --surface-2: #1c2230; +} + +@theme inline { + --color-surface-2: var(--surface-2); /* enables bg-surface-2 */ + --radius-lg: var(--radius); /* enables rounded-lg = 14px */ + --radius-md: var(--radius-sm); /* enables rounded-md = 9px */ + --font-mono: var(--font-mono); /* enables font-mono */ +} +``` + +| Token | Why | Consumers | +| -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | +| `--radius` / `--radius-sm` | One radius scale (14/9) instead of 6/8/12 ad-hoc | Button, Card, Modal, Input, Select | +| `--grad-brand` | Brand gradient for primary CTAs (redβ†’violet), matching the site | Button `primary`, hero/CTA surfaces | +| `--surface-2` | Nested panels / table headers / inset rows | Card.Section, DataTable header, inputs | +| `--font-mono` | Code blocks, terminal, IDs, endpoints | ConsoleLogViewer, code snippets, `localhost:20128/v1` chips | +| `--text-muted` reconcile | Pick one value site↔panel | global | + +**Decision D2 (text-muted):** site `#8b8b9e` vs dashboard `#a1a1aa`. Recommend keeping the **dashboard's `#a1a1aa`** (it's the live product, slightly higher contrast) and updating the _site_ to match. Low priority, cosmetic. + +--- + +## 6. Part C β€” Component standardization + +The component layer is **custom** (no shadcn/Radix), Tailwind v4, semantic tokens **mostly** adopted (`bg-surface`, `border-white/10`, `ring-primary`) β€” good adoption (195 files import the shared barrel). The work is removing the **bypasses**. Home: `src/shared/components/`. + +Ranked by impact Γ— reach: + +| # | Item | File(s) | Problem β†’ Target | +| --- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| C1 | **Radius alignment** | `Button.tsx:14-18`, `Card.tsx:39`, `Modal.tsx`, `Input.tsx`, `Select.tsx` | mixed 6/8/12px β†’ repoint to `--radius`/`--radius-sm` (14/9) | +| C2 | **Button gradient + `accent` variant** | `Button.tsx:5-12` | primary is flat redβ†’red (`from-primary to-primary-hover`); align to `--grad-brand` (redβ†’violet) and add the missing `accent` variant (indigo `#6366f1` is unused by buttons) β€” **highest visibility, ~195 importers**. **Decision D1.** | +| C3 | **Tables** | `DataTable.tsx:122-176`, `logTableStyles.ts`, `globals.css:405-414` (Ant remnants) | `DataTable` is 100% inline hardcoded rgba + references non-existent vars (`--text-secondary`, `--bg-table-header`); migrate to tokens, retire the 2 divergent table styles. Tables are everywhere (providers/connections/logs) β€” worst offender. | +| C4 | **Centralize status colors** | `flow/edgeStyles.ts:7-12`, `TokenHealthBadge.tsx:14-19`, `DegradationBadge.tsx`, `ProviderCascadeNode.tsx`, `Badge.tsx`, +5 `statusColor` helpers | 6+ copies of the same `#22c55e/#f59e0b/#ef4444` hex; create one `statusColors` module driven off `--color-success/warning/error`. Critical for circuit-breaker / cooldown / lockout badges to read consistently. | +| C5 | **Card border** | `Card.tsx:39` | uses `border-white/5`; brand border is `/8` β†’ align | +| C6 | **Focus ring reconcile** | `globals.css:183` vs component `ring-primary/30` | global `:focus-visible` is indigo (`--color-accent`), components are red (`ring-primary`) β€” pick one (recommend **accent/indigo** globally, it reads as the "interactive" color) | +| C7 | **Add `Checkbox` + `Textarea` primitives** | currently raw ``/`