feat(npm): add npm package publishing with CLI entry point - #15
Conversation
- Add bin/omniroute.mjs CLI with banner, auto-open browser, graceful shutdown - Add scripts/prepublish.mjs to build Next.js standalone into app/ - Add .github/workflows/npm-publish.yml for automated publish on release - Update package.json: name=omniroute, bin, files, engines, keywords, prepublishOnly - Add output: 'standalone' to next.config.mjs - Add MIT LICENSE - Update .npmignore and .gitignore for app/ build artifact
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Pull request overview
This PR adds npm package publishing capabilities to OmniRoute, transforming it from a development-only project into an installable CLI tool. The implementation uses Next.js standalone mode to create a self-contained build that can be globally installed via npm.
Changes:
- Added CLI entry point (
bin/omniroute.mjs) with server lifecycle management, argument parsing, and auto-browser opening - Created build script (
scripts/prepublish.mjs) to generate Next.js standalone output and copy runtime dependencies toapp/directory - Configured automated npm publishing via GitHub Actions workflow triggered on release creation
- Updated package metadata for public npm distribution with proper bin configuration, files array, and keywords
- Enabled Next.js standalone mode and improved environment variable handling
- Added MIT license and updated ignore files for build artifacts
Reviewed changes
Copilot reviewed 6 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| bin/omniroute.mjs | CLI entry point that spawns Next.js standalone server with banner, port configuration, graceful shutdown, and browser auto-open |
| scripts/prepublish.mjs | Build script that creates standalone Next.js build, copies static assets, MITM utilities, and shared dependencies to publishable app/ directory |
| package.json | Updated package name to "omniroute", added bin entry, files array, engines requirement, keywords, license, and prepublishOnly hook |
| package-lock.json | Reflects package.json changes including name, version, license, bin, engines, and marks fsevents as dev dependency |
| next.config.mjs | Enabled standalone output mode for npm packaging and removed hardcoded NEXT_PUBLIC_CLOUD_URL in favor of .env configuration |
| .github/workflows/npm-publish.yml | Automated npm publishing on GitHub release with version sync, standalone build, and provenance |
| LICENSE | Added MIT license with 2026 copyright |
| .npmignore | Configured to exclude source files and include only bin/ and app/ directories in published package |
| .gitignore | Added /app directory to ignore list since it's a build artifact |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // ── Step 10: Ensure data/ directory exists ────────────────── | ||
| mkdirSync(join(APP_DIR, "data"), { recursive: true }); |
There was a problem hiding this comment.
The build script creates an empty data/ directory in the published package. When users install via npm, this directory will be in their global npm packages directory (e.g., /usr/local/lib/node_modules/omniroute/app/data/), which may not have write permissions for all users. The application should handle the case where this directory is not writable and potentially use a user-specific data directory (e.g., ~/.omniroute/data/ or respect the DATA_DIR environment variable from .env.example). Verify that the application properly handles data directory initialization and permissions at runtime.
| // ── Step 10: Ensure data/ directory exists ────────────────── | |
| mkdirSync(join(APP_DIR, "data"), { recursive: true }); | |
| // ── Note: data/ directory is initialized at runtime ──────── | |
| // Do not create a writable data/ directory inside app/ here, as it | |
| // would reside in a potentially non-writable global npm directory. |
| open-sse/ | ||
| cloud/ | ||
| images/ | ||
| logs/ |
There was a problem hiding this comment.
The package.json excludes scripts/ in the .npmignore but includes scripts/prepublish.mjs that won't be in the published package. This is intentional since the build script is only needed during the publishing process (via prepublishOnly), not after installation. However, consider adding a comment in package.json or the build script explaining that prepublishOnly runs before publishing to npm, not during user installation, to clarify the build workflow.
| logs/ | |
| logs/ | |
| # Note: scripts/ (e.g. scripts/prepublish.mjs) is only used during npm publish via `prepublishOnly`, | |
| # so it is intentionally excluded from the published package. |
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 22 |
There was a problem hiding this comment.
The workflow uses Node.js 22, but the package.json specifies engines.node: ">=18.0.0". While this is not necessarily wrong (testing on a higher version that still meets the minimum requirement), consider whether the package should be tested on Node.js 18 as well to ensure compatibility with the minimum supported version, especially since users might install it on Node.js 18.
| node-version: 22 | |
| node-version: 18 |
| // ── Step 2: Install dependencies ─────────────────────────── | ||
| console.log(" 📦 Installing dependencies..."); | ||
| execSync("npm install", { cwd: ROOT, stdio: "inherit" }); | ||
|
|
||
| // ── Step 3: Build Next.js ────────────────────────────────── | ||
| console.log(" 🏗️ Building Next.js (standalone)..."); | ||
| execSync("npx next build --webpack", { cwd: ROOT, stdio: "inherit" }); |
There was a problem hiding this comment.
The build script uses execSync with stdio: "inherit" but doesn't wrap it in try-catch blocks. If any command fails (npm install, next build), the script will crash with an unhandled exception. While this might be acceptable since the process should exit on build failure anyway, consider adding try-catch blocks with clearer error messages to help diagnose build failures, especially for the critical Next.js build step.
| eslint.config.mjs | ||
| prettier.config.mjs | ||
| postcss.config.mjs |
There was a problem hiding this comment.
The .npmignore excludes all .env* files including .env.example. Users installing the package via npm won't have a reference for required environment variables. Consider either: (1) including .env.example in the published package by adding !.env.example to .npmignore, (2) documenting required environment variables in the README, or (3) having the CLI create a default configuration on first run. This is especially important for variables like JWT_SECRET, INITIAL_PASSWORD, and DATA_DIR from the .env.example file.
| eslint.config.mjs | |
| prettier.config.mjs | |
| postcss.config.mjs | |
| !.env.example | |
| eslint.config.mjs | |
| prettier.config.mjs |
| # Remove 'v' prefix if present (v0.1.0 -> 0.1.0) | ||
| VERSION="${VERSION#v}" | ||
| npm version "$VERSION" --no-git-tag-version --allow-same-version | ||
| echo "Publishing version: $VERSION" |
There was a problem hiding this comment.
The workflow synchronizes the version from the GitHub release tag, which overwrites the version in package.json. This means the version "0.1.0" in package.json is only a placeholder and will be replaced during publishing. Consider adding a comment in package.json near the version field to clarify that the actual version is set from the GitHub release tag during publishing, to avoid confusion for maintainers.
| echo "Publishing version: $VERSION" | |
| echo "Publishing version (from GitHub release tag, overriding package.json placeholder): $VERSION" |
| # Development | ||
| # Source code (pre-built app/ is published instead) | ||
| src/ | ||
| open-sse/ |
There was a problem hiding this comment.
The .npmignore file excludes the open-sse/ workspace directory, which might cause issues if the Next.js standalone build doesn't properly bundle this workspace dependency. Verify that @omniroute/open-sse is correctly included in the standalone output at .next/standalone/node_modules/@omniroute/open-sse or adjust the build script to explicitly copy it. The transpilePackages configuration in next.config.mjs should handle this, but it should be tested to ensure the published package works correctly.
| open-sse/ |
| */ | ||
|
|
||
| import { execSync } from "node:child_process"; | ||
| import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync } from "node:fs"; |
There was a problem hiding this comment.
The writeFileSync import is unused in this script. It's imported on line 13 but never called. Consider removing it to keep the imports clean.
| import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync } from "node:fs"; | |
| import { existsSync, mkdirSync, cpSync, rmSync, readFileSync } from "node:fs"; |
| }, | ||
| "files": [ | ||
| "bin/", | ||
| "app/", |
There was a problem hiding this comment.
The files array in package.json includes app/ which will contain the standalone Next.js build and all dependencies. Verify that this directory doesn't accidentally include large or unnecessary files that could bloat the package size. The standalone build typically includes node_modules, but only production dependencies. Consider checking the final package size before publishing and potentially adding more exclusions to .npmignore if needed.
| "app/", | |
| "app/.next/standalone/", |
…ROUTE_CLI_SALT, tray PNG, preservePatterns defaults, missing docs - management.ts: replace === with timingSafeEqual for CLI token comparison - machineToken.ts: salt upgraded to omniroute-cli-auth-v1; OMNIROUTE_CLI_SALT env var honoured for rotation; full 64-char SHA-256 hex token - tray.ps1: accept .png via GDI+ Bitmap->Icon handle; Windows tray works without .ico - tray.ts: getIconPath() tries icon.ico then icon.png on Windows - compression/types.ts: DEFAULT_CAVEMAN_CONFIG.preservePatterns filled with six defaults (fenced code, inline code, URLs, paths, error lines, stack traces) - CLAUDE.md: Hard Rule #15 — spawn-capable routes must use isLocalOnlyPath() - .env.example + docs/reference/ENVIRONMENT.md: document OMNIROUTE_CLI_SALT - docs/security/CLI_TOKEN.md: new (was referenced in changelog but missing) - docs/security/ROUTE_GUARD_TIERS.md: new (was referenced in changelog but missing) - tests/unit/lib/machineToken.test.ts: updated for 64-char token; added OMNIROUTE_CLI_SALT env-var rotation test
- COMPRESSION_ENGINES.md: add MCP accessibility-tree filter section with config reference, algorithm description, and comparison table - COMPRESSION_LANGUAGE_PACKS.md: document SHARED_BOUNDARIES clause (6 patterns × 6 languages × 3 intensities, preservePatterns defaults) - MCP-SERVER.md: add accessibility-tree filter note in Compression Tools - CONTRIBUTING.md: fix coverage gate (60%→75/70), add Hard Rules #15/#16 to PR checklist, add links to new security/ops docs
…tream-ca routes (F5) 12 REST routes under /api/tools/agent-bridge/ covering all AgentBridge backend surfaces: server lifecycle, per-agent state/DNS/mappings/detect, cert status/download/regenerate, bypass pattern CRUD, upstream CA config. All routes use Zod validation and route errors through sanitizeErrorMessage. feat(authz): mark agent-bridge LOCAL_ONLY + SPAWN_CAPABLE (F5) Adds /api/tools/agent-bridge/ to both LOCAL_ONLY_API_PREFIXES and SPAWN_CAPABLE_PREFIXES in routeGuard.ts — satisfying Hard Rules diegosouzapw#15 + diegosouzapw#17.
… listener leak
- routeGuard: classify POST /api/providers/{id}/login as LOCAL_ONLY via a new
regex matcher (LOCAL_ONLY_API_PATTERNS) so loopback is enforced before auth —
it launches a headful Playwright Chromium (child process). Scoped to the
/login sub-route so ordinary /api/providers/* CRUD stays remotely reachable
(Hard Rules #15 + #17). TDD: route-guard-provider-login-local-only.test.ts
pins the gate AND the non-over-match (fails without the matcher).
- login/route.ts: route both error bodies through sanitizeErrorMessage so raw
err.message never leaks in the HTTP response (Hard Rule #12).
- electron/main.js: register the loginManager 'status' listener once instead of
inside the login:start handler (was attaching a new listener per call); drop
the dead require('./sqlite-inspection').persistSecret.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
…symbols, route-guard, complexity, docs-symbols, db-rules) Deterministic gates, each freezing pre-existing violations in a documented allowlist (ratchet) so they pass now and block only NEW regressions: - check-error-helper (Rule #12): 7 executors/handlers forwarding raw err.message frozen - check-public-creds (Rule #11): 5 literal client_ids (Claude/Codex/Qwen/Kimi/Copilot) frozen - check-migration-numbering: gaps 026/055 + dup 041 frozen (prevents the git-rm-deleted-migration incident) - check-known-symbols: 93 executors conformance + 15 combo strategies + 18 translator pairs - check-route-guard-membership (#15/#17): all 25 spawn-capable routes verified local-only (0 gaps) - check-complexity: cyclomatic>15 / fn-length>80 ratchet (baseline 1739) - check-docs-symbols: 30 stale doc /api refs frozen (docs hallucination) - check-db-rules (#2/#5): 25 unexported db modules + 15 raw-SQL routes frozen Wired into CI (lint / docs-sync-strict / quality-gate jobs). 115 TDD tests, all green. ESLint ratchet held at 3482.
…ment guardrails (Phases 0-6) (#3471) * feat(quality): generic ratchet comparator (multi-metric, regression-only) * chore(ci): Fase 0 quality-gate fixes — reconcile coverage gate (40->60), tier npm audit, wire orphaned contract gates, re-enable cheap husky pre-commit * feat(quality): ratchet engine (collector + frozen baseline + CI job) and provider-consistency gate - collect-metrics.mjs: emits quality-metrics.json (ESLint warnings + coverage when present) - quality-baseline.json: frozen baseline (eslintWarnings=3482, regression-only) - ci.yml: quality-gate job (ratchet + step summary + artifact) and check:provider-consistency in lint job - check-provider-consistency.ts: every REGISTRY id must be a canonical provider (found krutrim half-registered → allowlisted as known pre-existing, blocks any NEW orphan) - TDD: 9 tests (5 ratchet + 4 provider-consistency) * feat(quality): Fase 2 anti-hallucination gates — fetch-targets, openapi-routes, deps allowlist - check-fetch-targets: every dashboard fetch(/api/...) resolves to a real route.ts; found 7 pre-existing dashboard->route mismatches frozen as KNOWN_MISSING for triage - check-openapi-routes: every openapi.yaml path resolves to a real route; found 1 stale spec entry (agent-bridge agents/{id}/state) frozen as KNOWN_STALE_SPEC - check-deps: anti-slopsquatting allowlist (105 deps); new deps need explicit human-reviewed entry - all wired into CI lint/docs jobs; TDD +12 tests (21 total across 5 gates) * docs(quality): add quality-gates report + implementation plan to repo root * feat(quality): Fase 3a — file-size ratchet (freeze 91 files >800 LOC, cap 800 for new) - check-file-size.mjs: frozen files can only shrink; new files must be <= cap (kills the next 12k-line god-component) - file-size-baseline.json: 91 files frozen at current LOC (largest 12883) - wired into CI lint job; TDD 5 tests; --update ratchets the baseline down on shrink * feat(quality): Fase 3b — duplication ratchet (jscpd@4, baseline 5.72%) - check-duplication.mjs: runs jscpd@4 (pinned; v5 is an incompatible Rust rewrite) over src+open-sse, fails if duplication % rises vs frozen baseline (5.72%, measured: 1358 clones / 22967 dup lines). Targets the executor copy-paste (48/50 override execute() wholesale) - wired into the parallel quality-gate CI job (off the lint critical path); TDD 4 tests; --update ratchets down - snapshot now complete: coverage ~82.6%, eslint 3482 (98.5% no-explicit-any), duplication 5.72%, 91 files >800 LOC * feat(quality): Fase 4a — anti test-masking gate - check-test-masking.mjs: for each MODIFIED test file in a PR, flags net assert removal + new assert.ok(true) tautologies (base...HEAD diff). Directly enforces CLAUDE.md 'never weaken asserts to go green' - wired into pr-test-policy CI job (reuses base fetch); no-op outside PR; TDD 5 tests * feat(quality): Fase 4b — coverage ratchet (conservative floors, CI consumes merged coverage) - quality-baseline.json: coverage.{statements,lines,functions,branches} floors (80/80/82/73, real ~82.58/82.58/84.23/75.22 with margin; tighten via --update after a green main run) - check-quality-ratchet.mjs: --allow-missing (local quality:gate skips coverage.* without a coverage run; CI runs strict) - ci.yml quality-gate job: needs test-coverage + downloads merged coverage-report so the ratchet enforces 'coverage cannot drop' - TDD +1 test (6 total) * feat(quality): Fase 6 — 8 new gates (Rule #11/#12, migrations, known-symbols, route-guard, complexity, docs-symbols, db-rules) Deterministic gates, each freezing pre-existing violations in a documented allowlist (ratchet) so they pass now and block only NEW regressions: - check-error-helper (Rule #12): 7 executors/handlers forwarding raw err.message frozen - check-public-creds (Rule #11): 5 literal client_ids (Claude/Codex/Qwen/Kimi/Copilot) frozen - check-migration-numbering: gaps 026/055 + dup 041 frozen (prevents the git-rm-deleted-migration incident) - check-known-symbols: 93 executors conformance + 15 combo strategies + 18 translator pairs - check-route-guard-membership (#15/#17): all 25 spawn-capable routes verified local-only (0 gaps) - check-complexity: cyclomatic>15 / fn-length>80 ratchet (baseline 1739) - check-docs-symbols: 30 stale doc /api refs frozen (docs hallucination) - check-db-rules (#2/#5): 25 unexported db modules + 15 raw-SQL routes frozen Wired into CI (lint / docs-sync-strict / quality-gate jobs). 115 TDD tests, all green. ESLint ratchet held at 3482. * docs(quality): Phase 7 plan (security/dead-code/mutation/community tooling) — GATED to 2026-06-16 Stored, not active. 7 suggested gates + all discussed OSS/Community tools (SonarQube Community + osv-scanner + CodeQL + knip + sonarjs + type-coverage + lockfile-lint + Stryker + size-limit + axe-core + semcheck + agent-lsp + Qlty). Activation gate: do not start before 2026-06-16 (use Phases 0-6 in production for 1 week, validate in practice, then evolve).
…sibling sweep Real production defects fixed (all red on the pure tip, each with its origin): - routeGuard.ts: #8949 accidentally DELETED the /api/providers/[id]/login local-only pattern — the route spawns a browser, so the loopback gate for a process-spawning route was gone (Hard Rules #15/#17); restored (314 guard tests green) - agentSkills generator: #9058's category dispatch gave the config category an empty body, wiping skills/config-codex-cli/SKILL.md at the #10131 sync; fixed + SKILL.md regenerated via the official generator - imageRegistry: #9982 broke same-provider bare aliasing (antigravity preview id sent upstream unresolved); new resolveSameProviderBareAlias() keeps the fal cross-provider fix intact - imageRegistry: #9982's prefix strip handed the bare nano-banana ids to fal-ai, violating the pinned 2026-07-31 operator decision (adobe-firefly owns them); fal entries made prefix-only (dispatch already re-prefixes) - mediaGeneration/fal.ts: the missing-credential 401 guard was lost when #10198 deleted the superseded falHandler — tests were hitting the live network - bottleneckPatch/rateLimitManager: #9041's merge clobbered #9604, resurrecting the Bottleneck v2.19.5 heartbeat bug (reservoir never refills); patched the library defect at the root and re-aligned chat-rate-limit-body-lock to the working reservoir contract - processSupervisor.mjs: #9761 regressed the Node spawn to bare "node" (the #9156 launchd bug) and dropped #9209's ipv4first args; both restored - openai-responses/pureHelpers: #9423's Agent null-sentinel was unreachable on the schemaless JSON-string path; gate extended - i18n en.json: #8222's regen reverted the #9976 unclosed-tag fix and #8559's combo-cooldown copy; #9038 shipped 40 t() calls with no messages (runtime MISSING_MESSAGE); all restored/added + official sync-ui stamps, and vi's zero-marker policy re-established via the sanctioned translation backend Stale sibling tests aligned (movers cited inline): chat-helpers (#9447), executor-antigravity (#9351), video-fal-grok (#9982), visionBridge (#9759), web-session-credentials (#8974), production-build-module-integrity (positive anchor added), agentSkills-generator/skillManifestsLint/skills-injection/ agentSkillTools-mcp/listCapabilities-a2a (#9058), memory-settings (#10010), model-catalog-policy-invalidation (#8906), model-alias-seed (#9485), reactive-context-compaction (#8949), combo-provider-wildcard (broken upsert helper), oauth-google-loopback (43-locale resurrected-key removal) Validation: 501/501 across the 47 touched test files; typecheck:core, lint, file-size, docs-sync all green. Refs #9985
* fix(ci): clear base-reds on release/v3.8.50 (round 3) - CHANGELOG.md: restore the top [Unreleased] section dropped by the #10189 reconcile (docs-sync gate: first section must be Unreleased) - env-doc-sync: document CONDUCTOR_ORCHESTRATOR_TOKEN + CONDUCTOR_SPOKESPERSON_URL in .env.example/ENVIRONMENT.md; allowlist the CI-only GITHUB_STEP_SUMMARY and TS7_BASE_REF (ts7 ratchet signals); drop a stray merge artifact line - providers: restore the audited chatanywhere metadata entry that base-reds round 2 dropped together with its duplicate — the provider was half-wired (registry+endpoint without APIKEY metadata), which is what the wave3 test catches; re-pin providers-constants-split at the measured 228 - docs counts: 338 -> 339 (today's +2 void-ai/helixmind, -1 Puter) via gen:provider-reference + README/AGENTS/llm.txt/package.json/diagrams/i18n mirrors - file-size ratchet: annotated rebaseline for the two pre-existing drifts (ModelSelectModal 1138, gateways 1250) following the 2026-08-11 precedent Refs #9985 * fix(ci): base-reds round 3b — stale sibling tests + mode-pack weight contract - check-docs-counts-sync.test.ts: drop the imports/subtests of the four helpers #10196 removed from the gate script (readMcpFactsFromSource, listLocalizedDocs, makeRequiredCountsValidator, checkFreeTierInventory) — the new-API tests that #10196 added stay; the file now loads again under the node runner - quota-connection-recovery.test.ts: convert from vitest APIs to node:test — the file lives in tests/unit/*.test.ts (node-runner glob) and the vitest runtime crashes when imported outside vitest, killing the whole shard entry - modePacks.ts: re-normalize all six mode packs to sum 1.0 — #8940 added sessionAvailability: 0.05 to every pack without rebalancing (1.05 total); ratios preserved exactly (÷1.05), so post-normalizeScoringWeights behavior is unchanged; restores the declared sum-to-1.0 contract the 4235 test pins Refs #9985 * fix(ci): base-reds round 3c — vitest siblings, weights default, secrets FP, mutation tap - DistributeProxiesButton.test.tsx: wrap renders in NextIntlClientProvider — #9245 localized the component (useTranslations) and left the test without the intl context, failing all 14 cases - scoring.ts: re-normalize DEFAULT_WEIGHTS to sum 1.0 (same #8940 class as the mode packs — sessionAvailability added without rebalancing; ratios preserved) - .gitleaks.toml: generalize the kimi sponsor-banner localStorage-key allowlist to -v\d+ — #10200 bumped v1→v2 and the stale regex regressed the secrets ratchet with a false positive - stryker.conf.json: register 6 covering unit tests in tap.testFiles (4 modules) so their mutant kills count — unblocks check:mutation-test-coverage --strict Refs #9985 * fix(ci): base-reds round 3d — inspector factor gap, stale registry/gap tests, i18n key sync - comboScoringInspector: add cacheAffinity/sessionAvailability/connectionDensity to FACTOR_KEYS + the factor-key type — calculateScore() weighs them but the breakdown omitted them, so the explained contributions never summed to the reported score (inspector bug, red on the pure tip) - combo-scoring-inspector.test: make the explicit-weights override sum-neutral (±0.05 shift) so it stays valid for any DEFAULT_WEIGHTS values — the hardcoded override only summed to 1.0 against the pre-#8940 defaults, which is also why explicit weights silently fell back to 'default' on the tip - unorouter-registry.test: align to the canonical .com host (api.unorouter.ai 301-redirects there, verified live) and to wave4's live model discovery (passthrough, no static seed) — the .ai/auto-model expectations were stale - check-migration-numbering.test: 147 left KNOWN_GAPS when 147_api_keys_model_access_mode.sql landed — assert absent (same as 143) - i18n: sync-ui pass — 35,914 missing UI keys stamped as __MISSING__ placeholders across 42 locales (mechanical; greens the pt-BR key-presence integrity test; coverage pct unchanged by design — translation is a separate workstream) Refs #9985 * fix(ci): base-reds round 3e — 2 real defects + 14 stale sibling tests (waves A-E) Real defects fixed: - src/lib/db/apiKeys.ts: #9313's empty-allowlist early return bypassed the group permission check, silently disabling group deny rules (#8817) for every key without a per-key allowlist; fall-through restored, restricted+[] deny-all kept - open-sse/utils/proxyFetch.ts: #10032 re-appended the raw transport error to the propagated message, reintroducing the proxy user:password leak #9837 closed; new redactProxyDetailsInMessage() keeps the reason, redacts URL/credentials - .github/workflows/quality.yml: #10134 added the TS7 ratchet as a separate blocking step AFTER the aggregated gates — the exact #8542 masking mechanism; folded into the non-fail-fast loop (still blocking, still PR-only)⚠️ CI edit, gate-strengthening — explicit owner sign-off requested on the PR - src/i18n/messages/ko.json: 3 machine-mistranslation regressions caught by the #8244 glossary checker (장애인→비활성화됨, 양말5://→socks5://, 비클로드→Claude가 아닌) Stale sibling tests aligned to deliberately-moved contracts (each cites its mover): request-log-detail-layout + -stream (#9245 intl provider), repro-8542 pin update, quality-rail-gate-membership (#10134 shape), agentSkills-routes 45→46 (#9058), cloudflare-ai-catalog-8717 (#8804 supersedes #8808), executor-xai (#9994), vision-bridge-claude-wire (#9463 minimax→openai), sse-auth forced-pin (#8893), tls-proxy-context (strengthened leak guards), rate-limit-local-error-classification (#9164/#9342), minimax-thinking-signature (#9463), codebuddy-cn (#9723 +1 test), github-copilot-custom-model (#9050), providers-g4f-batch3 (#9584), synced-capability-warmup (#9199, stricter), sidebar-tools-group (#8221), oauth-modal-grok-cli-paste (#9245); agentSkills/catalog.ts comment 45→46; file-size rebaseline for proxyFetch (+19, annotated) Refs #9985 * fix(ci): base-reds round 3f — waves F-J: 9 more real defects + stale sibling sweep Real production defects fixed (all red on the pure tip, each with its origin): - routeGuard.ts: #8949 accidentally DELETED the /api/providers/[id]/login local-only pattern — the route spawns a browser, so the loopback gate for a process-spawning route was gone (Hard Rules #15/#17); restored (314 guard tests green) - agentSkills generator: #9058's category dispatch gave the config category an empty body, wiping skills/config-codex-cli/SKILL.md at the #10131 sync; fixed + SKILL.md regenerated via the official generator - imageRegistry: #9982 broke same-provider bare aliasing (antigravity preview id sent upstream unresolved); new resolveSameProviderBareAlias() keeps the fal cross-provider fix intact - imageRegistry: #9982's prefix strip handed the bare nano-banana ids to fal-ai, violating the pinned 2026-07-31 operator decision (adobe-firefly owns them); fal entries made prefix-only (dispatch already re-prefixes) - mediaGeneration/fal.ts: the missing-credential 401 guard was lost when #10198 deleted the superseded falHandler — tests were hitting the live network - bottleneckPatch/rateLimitManager: #9041's merge clobbered #9604, resurrecting the Bottleneck v2.19.5 heartbeat bug (reservoir never refills); patched the library defect at the root and re-aligned chat-rate-limit-body-lock to the working reservoir contract - processSupervisor.mjs: #9761 regressed the Node spawn to bare "node" (the #9156 launchd bug) and dropped #9209's ipv4first args; both restored - openai-responses/pureHelpers: #9423's Agent null-sentinel was unreachable on the schemaless JSON-string path; gate extended - i18n en.json: #8222's regen reverted the #9976 unclosed-tag fix and #8559's combo-cooldown copy; #9038 shipped 40 t() calls with no messages (runtime MISSING_MESSAGE); all restored/added + official sync-ui stamps, and vi's zero-marker policy re-established via the sanctioned translation backend Stale sibling tests aligned (movers cited inline): chat-helpers (#9447), executor-antigravity (#9351), video-fal-grok (#9982), visionBridge (#9759), web-session-credentials (#8974), production-build-module-integrity (positive anchor added), agentSkills-generator/skillManifestsLint/skills-injection/ agentSkillTools-mcp/listCapabilities-a2a (#9058), memory-settings (#10010), model-catalog-policy-invalidation (#8906), model-alias-seed (#9485), reactive-context-compaction (#8949), combo-provider-wildcard (broken upsert helper), oauth-google-loopback (43-locale resurrected-key removal) Validation: 501/501 across the 47 touched test files; typecheck:core, lint, file-size, docs-sync all green. Refs #9985 * fix(ci): base-reds round 3g — wave K/L: 4 more real defects + stale alignments Real defects: - base/reasoningEffort.ts: the stale duplicate cherry-pick #9612 re-added the codex minimal→low rewrite that #9883 had deliberately removed (OMP minimal passthrough); block removed again - cursorImages.ts: #9840 wired prepareCursorImageForWire (sharp re-encode, fail-closed) into the SHARED resolveCursorImages, breaking zai-web and conol-web image uploads (HTTP 400 'undecodable'); new prepareForWire opt-out, Cursor default path unchanged (8 cursor suites green) - modelCapabilities/snapshot: catalog prepare still issued 323 per-model reads of model_context_overrides + max_input_tokens overrides, violating #9199's bulk-load contract; both now resolve from the snapshot single pass - v1-models-discovery-conformance: re-pinned to the bounded 30s SWR window (#9199/#10198) — the old 'stale-first regardless of age' contract is gone Stale tests aligned (movers cited inline): codex-tools-strict-default (#9828 redundant-oneOf strip), devin-providers (#9245 i18n), db-migrationrunner- constants-split (147→151 renumber #8228), gitlab-duo-oauth-setup (#9245), chatcore-extracted-modules (#9161 outbound-protocol keying) compression-api CI failures were cascade artifacts of codex-tools-strict-default failing in the same force-exit shard process — no own defect (171/171 local). Refs #9985 * fix(test): compression-api — register both describes before the runner starts The DATA_DIR setup + route/db top-level awaits sat BETWEEN the two describes; under --test-force-exit (the CI unit-runner flag) the process exits once the already-registered tests finish, so on slow CI machines the whole second describe died as 'Promise resolution is still pending' — the recurring CI-only shard-2 failure that never reproduced locally without the flag. Moved to the top of the file; 10/10 under --test-force-exit locally. Refs #9985 * fix(quality): freeze modelCapabilities.ts at 1006 (annotated) — snapshot routing growth Refs #9985 * fix(quality): move the modelCapabilities freeze into the frozen map (nested schema) Refs #9985 * fix(i18n): translate all 39,718 pending UI keys across 42 locales (owner-approved) Mass-translated every __MISSING__ placeholder via the official i18n:sync-ui --translate-markers pipeline (operator backend), restoring i18nUiCoverage to the 100 baseline (was 89.9 after the merge-storm UI landings + the 42 keys #9038 never shipped). Post-pass repairs, all caught by the existing gates: - glossary: retired renderings the machine reintroduced normalized again (提供商→提供者 zh-CN/zh-TW, 鏈接→連結, 文檔→文件, 調用→呼叫, 供應商→提供者, 響應→回應, 不活躍→未啟用 zh-TW; 클로드→Claude, 옴니루트→OmniRoute ko); DATA_DIR forbidden rendering avoided via 数据文件夹 rephrase - ICU integrity: 120 values with renamed/dropped {params} repaired (39 positional renames, 81 reset to the en source — functional over fluent) Validation: glossary/pt-BR/vi/deno-relay/settings-keys/value-drift/google- loopback suites 76/76; placeholder diff en×42 locales = 0; worst-locale coverage = 100.0%. Refs #9985 --------- Co-authored-by: backryun <bakryun0718@proton.me>
…ocess-spawning endpoints (#11189) Validated on the combined batch board (gates + typecheck clean) and this branch: security-route-guard-tiers green. Regression coverage for the Hard Rule #15/#17 contract — Tier 1 process-spawning prefixes (/api/services/, /api/mcp/, /api/cli-tools/runtime/) must stay LOCAL_ONLY before any auth check. Conflict with the tip was only stale provider-count docs. Thank you @rqzbeh!
Validated in a combined 10-PR batch worktree off release/v3.8.51 tip. - Focused tests: tests/unit/authz/route-guard-tunnel-processes-local-only.test.ts + tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts — 6+ pass - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK - Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip — unrelated to this diff⚠️ base-red inherited: #11449 Thanks for closing the process-spawning tunnel routes to local-only (Hard Rule #15/#17 territory) while preserving authenticated remote read access to status endpoints.
Two Fast Quality Gates failures on diegosouzapw#12400. `check:test-discovery` reported all three new suites as orphans: the unit runner's subdirectory glob is an explicit allowlist (`tests/unit/{api,auth,authz,...}/**`) and `deploy` is not in it, so nothing under `tests/unit/deploy/` was ever collected. The suites passed locally and in no CI job — the exact failure mode that gate was added for after the 2026-06-09 audit found ~135 orphaned tests, one of them a Hard Rules diegosouzapw#15/diegosouzapw#17 check whose asserts were already failing unnoticed. Moved the three files to `tests/unit/` rather than widening the allowlist: adding a directory there changes the runner for every contributor, which is not this PR's business. Import depths adjusted; the fourth suite was already at the collected level. `check:complexity-ratchets` flagged `buildDeployment` at 83 lines against a limit of 80. The pod template is almost entirely the container object, so that object moved to `buildContainer`. No behaviour change — the generator's output is byte-identical, which the parity test against deploy/kubernetes/base pins. Both gates verified locally: test-discovery OK, complexityNewCode=0.
- chore(changelog): drop four stale fragments already in CHANGELOG (diegosouzapw#9435, diegosouzapw#9550, diegosouzapw#9568, diegosouzapw#9575) — stale-fragment gate exits nonzero on duplicate - fix(translator/claudeHelper): re-filter empty messages after the Pass 1.4 nameless-tool_use drop so non-final assistant messages whose only content was a nameless block do not survive into the request (#5, #6) - fix(ws/responses-ws-proxy): serialize concurrent forwardClientMessage calls via dispatchQueue so a second client message arriving during ensureUpstream does not drop the late frame (diegosouzapw#7) - fix(ws/v1-ws-bridge): close the session with code 1002 after a frame decode failure so an undecodable byte run does not loop forever (diegosouzapw#8) - fix(ws/both): send the WebSocket close frame BEFORE flipping this.closed so the sendFrame guard does not turn the close handshake into a no-op (diegosouzapw#9) - fix(oauth/freebuff): add 'network_error' to freebuffPollResponseSchema so the network-error return path typechecks (diegosouzapw#10) - fix(oauth/freebuff): declare onProgress on FreebuffPollOptions so the poll-loop callback typechecks (diegosouzapw#11) - fix(freebuff/cliEmulator): add readonly ok to FreebuffHttpResponse and populate it in tlsClientFetch, wreqFetch, and globalFetch so successful responses are not misclassified as failures (diegosouzapw#12) - test(translator/openai-to-claude): defer content_block_start until a name arrives and throw on a permanently nameless streamed call so the Muse/GLM diegosouzapw#2077 sequence and the nameless-rejection test both pass (diegosouzapw#16) - test(9568): resolve the three merge-marker regions in favor of HEAD — the test reflects the post-diegosouzapw#10392 translator behavior (diegosouzapw#13, diegosouzapw#14, diegosouzapw#15)
* fix(oauth): Kiro import token endpoint no longer overwrites existing connection when using shared cached OIDC clientId (diegosouzapw#9435) Closes diegosouzapw#9435 (cherry picked from commit 9edefd4) * fix(translator): add case-insensitive fallback for upstream tool call name lookups (diegosouzapw#9575) Closes diegosouzapw#9575 (cherry picked from commit c9a3361) * fix(translator): restore original tool name casing in Gemini response translators (diegosouzapw#9568) Closes diegosouzapw#9568 (cherry picked from commit c9debe9) * fix(model): add aq alias for amazon-q provider so parseModel resolves it instead of falling back to OpenAI (diegosouzapw#9550) Closes diegosouzapw#9550 (cherry picked from commit f338363) * fix(translator): reject nameless streaming tool calls (cherry picked from commit e274819) * fix(devin-desktop): sanitize Claude Code prompt triggers and enforce tool budget (cherry picked from commit 8c1514a) (cherry picked from commit a18771b) * fix(devin-desktop): sanitize interactive Claude Code CLI identity declarations (cherry picked from commit e5553c1) (cherry picked from commit db089c8) * fix(claude-code): format context overflow as 'Prompt is too long' to trigger reactive compact (cherry picked from commit b5fec6b) (cherry picked from commit 5e83dc6) * fix(chat): import getCachedSettings from readCache (cherry picked from commit 999c708) (cherry picked from commit d8329dd) * fix(image): add 'agy' alias for antigravity image generation models (cherry picked from commit 2ac3e0d) (cherry picked from commit 203cdb3) * fix(translator): subtract cachedContentTokenCount from input_tokens in gemini-to-claude translator to avoid double-counting in Claude Code (cherry picked from commit 4c5ec77) (cherry picked from commit d82672d) * fix(translator): add cache_creation_input_tokens, update tests, and set auto-compression defaults (cherry picked from commit 9b68a4b) (cherry picked from commit 6f4e1f4) * fix(translator,compression): sanitize token metrics with Number.isFinite and restore opt-in compression defaults (cherry picked from commit 985c3c2) (cherry picked from commit cf9da8e) * fix(vision-bridge): cross-check agy/antigravity aliases and declare SWE vision capabilities (cherry picked from commit d1f2f5a) (cherry picked from commit a80b6df) * fix(vision-bridge): return skip if mixed combo has vision capable target (cherry picked from commit a049fbb) (cherry picked from commit fd2d093) * fix(ratelimit): set 120s maxWaitMs floor for long-context providers to prevent 504 on 100k+ tokens (cherry picked from commit 98cc4b0) (cherry picked from commit 6195ebf) * fix(ratelimit): clean up redundant zai-web entry, add null safety and long-context override tests (cherry picked from commit 743a118) (cherry picked from commit 4421ceb) * test(ratelimit): add direct assertion for opencode in queue wait floor test (cherry picked from commit dda584d) (cherry picked from commit 5b987d9) * fix(translator): synthesize fallback text block from reasoning when model emits only thinking for compaction (cherry picked from commit 0e13723) (cherry picked from commit bc9e1c5) * fix(devin-desktop): classify content policy trailer errors as deterministic 400 and bypass cooldown (cherry picked from commit 8820f15) (cherry picked from commit f8a4a3b) * feat(pricing): add SWE-1.7 Lightning pricing for devin-desktop and windsurf providers (cherry picked from commit 11ec472) (cherry picked from commit 0956840) * feat(pricing): set swe-1-7 price at half Lightning rate (.25/zsh.5/.25 per 1M) (cherry picked from commit 60def8f) (cherry picked from commit 526c814) * feat(models): add Gemini 3.8 Flash tiers and pricing Introduce Gemini 3.8 Flash across OmniRoute on top of the existing 3.6/3.7 Flash support: - modelSpecs: gemini-3.8-flash{,-high,-medium,-low,-tiered} with 1M context, 64K output, defaultReasoningEffort=medium. - pricing: promo $0.75/$3.75 per 1M for gemini, antigravity/agy, frontier-labs, cheaperinference, orcarouter, and inference-hosts. - provider registries: gemini, gemini-web, github, ghe-copilot, cursor, cheaperinference, kilocode, opencode/zen, orcarouter, raycast, tinycms, lmarena. - AGY/Antigravity catalogs, aliases and free-model catalog. - Fallback services: conol, githubCopilot, notionWeb, promptql. - CLI tool model aliases and modelAliasSeed. - modelFamilyFallback chains for the 3.8 Flash family. - geminiRateLimits.json. Tests: t28, models-catalog-route, model-capabilities-registry, pricing-ag-flash-tiers, agy-gemini-3696-tier-passthrough and model-family-fallback-notation all pass. npm run typecheck:core and npm run lint pass. (cherry picked from commit 7e44a6d) (cherry picked from commit c8391ff) * fix(chat): return resource pressure guard as a normal result and avoid cooldown executeChatWithBreaker previously returned { localResourcePressureResult } when the resource pressure guard tripped, but the caller in chat.ts only destructures { result, tlsFingerprintUsed } and immediately reads result.success. That caused a TypeError ('Cannot read properties of undefined (reading "success")') and made combo fallback logs show a confusing crash. Now the pressure guard is returned as a normal { result } with errorCode/errorType "resource_pressure", and chat.ts surfaces the 503 immediately without marking the connection unavailable. (cherry picked from commit 22b641e) (cherry picked from commit ee2af6f) * ops(docker): raise runtime heap to 4096MB and pin tls-client native version OMNIROUTE_MEMORY_MB 1024MB was too tight for production traffic with large fusion-combo panels, causing constant GC pressure and PSI CPU/memory critical states. Raise the default runtime heap ceiling to 4096MB. Also pin TLS_CLIENT_VERSION=1.15.1 so the tls-client-node postinstall does not fetch an upstream "latest" release that may be missing the Linux .so asset. (cherry picked from commit db021d9) (cherry picked from commit 8583cfe) * fix(providers): harden empty responses and oauth test classification - stream: inject a space instead of empty text for the synthetic Claude empty-response fallback so clients do not trip on zero-length completions (502 noise seen in production call_logs). - providers/test: return statusCode 400 for unsupported provider tests; add custom-oauth-connection-proxy to OAUTH_TEST_CONFIG (checkExpiry only — the proxy has no userinfo endpoint). - resolveRoutingModel: default subagent routing model claude-sonnet-5 -> claude-haiku-4.5 to reduce pressure on the antigravity lane. (cherry picked from commit c219b43) (cherry picked from commit 2892e9d) * feat: port subagent routing + tool-call shim + devin/transport hardening from local production (cherry picked from commit 2cb7b19) * test: fix gemini-to-claude usage test fixture, drop repairedRaw tests (cherry picked from commit 53462ff) * chore: prune unused eslint suppressions (cherry picked from commit 8515409) * fix(docker): relax healthcheck to tolerate event-loop stalls under load The 5s timeout / 3 retries / 15s start-period was too strict for this single-process app: /healthz probes timed out during startup module loading and under sustained heavy /v1/messages traffic (~180k-token requests), flipping the container unhealthy and causing Docker/Dokploy to kill mid-session — the observed restart loop / 502 source. start-period=120s covers cold start; timeout=15s + retries=5 tolerate transient event-loop stalls (~150s) while still catching dead processes. (cherry picked from commit c749903) * fix(devin-desktop): accumulate id-less tool-call argument deltas Devin Desktop streams tool-call arguments as separate proto messages carrying only field 3 (arguments) with no id/name. The decoder dropped them (`if (toolCall.id)`), so clients received tool_use blocks with empty input {} — Claude Code then rejected ~50% of tool calls with InputValidationError, and the empty calls in history taught the model to emit empty arguments itself, causing a self-reinforcing error loop. Keep argument-only deltas and append them to the most recently started tool call so input_json_delta reaches the client. (cherry picked from commit 8a82a13) * fix(translator): purge never-succeedable empty tool_use pairs from history Clients whose history was poisoned by lost tool-call arguments keep replaying tool_use {input:{}} + InputValidationError pairs, teaching the model to emit empty arguments on new calls (self-reinforcing loop). When translating a Claude-format request, drop tool_use blocks whose input is empty AND whose tool schema declares required params (such a call could never have succeeded), along with their matching tool_result blocks and "(empty response)" placeholder texts. Tools with no required params (e.g. chrome_read_page) and tools absent from the tools list are left untouched. (cherry picked from commit 164c61d) * fix(review): adversarial-review hardening + deterministic stream error status Code-review patches: - chatDispatch: detect resource_pressure via typed error fields (dead localResourcePressureResult check evicted session affinity wrongly) - chat: return resource-pressure 503 before safeLogEvents so it is not logged as an upstream error; fix stale "retrying once" log message - stop.mjs: share isTestEnvironment() guard across POSIX and Win32 kill paths - supervisor: mkdir log dir before createWriteStream (ENOENT crash) - resolveRoutingModel: trim SUBAGENT_DEFAULT_MODEL env (whitespace-safe) - claudeHelper: drop string-content "(empty response)" messages; normalize Claude Code file-unchanged sentinel for non-Anthropic Claude-shape targets - toolCallShim: AskUserQuestion option objects emit only schema fields, question arrays join cleanly, options-only calls get a fallback question - devin-desktop: sanitizeDevinPrompt scoped to system/developer roles only; document parallel tool-call arg-delta attribution limitation - Dockerfile: healthcheck start-period 120s -> 60s (faster dead-detection, stall tolerance kept via timeout/retries) Compact-incident fix (session log analysis): - streamReadiness: a stream ending with only a deterministic upstream diagnostic (content policy / safety filter) now surfaces its real 400 status instead of a retryable 502 STREAM_EARLY_EOF — stops the observed ~9x/10min retry loop on payloads that can never succeed - sameAccountTransportRetry: 403 only retryable for the Devin permission_denied-internal-error quirk; content-policy text never retries - test: bound assertion follows SAME_ACCOUNT_TRANSPORT_RETRY_MAX (cherry picked from commit e37ae2b) * fix(translator): error event for upstream-truncated tool-call arguments When the upstream stream ends mid-tool-call (observed: Devin Desktop Connect streams cut during argument deltas), the accumulated arguments buffer is unparseable JSON. Closing the tool_use block normally handed the client a truncated partial_json - Claude Code raised InputValidationError, and the model retried with hallucinated stub args (seen across recent session logs). The openai-to-claude finish path now validates each non-shimmed tool call's argBuffer and emits a terminal error event instead of closing the block, so the turn is retried rather than executed as a truncated command. Shimmed tools are exempt (their shim repairs the buffer at close). (cherry picked from commit 4dca2f3) * feat(translator): schema-driven tool-call argument repair Models that emit wrong-typed fields, extra keys, or missing required args hit client-side InputValidationError retry loops (observed across recent Claude Code session logs). Add a generic sanitizer that repairs parsed tool arguments against the client-declared input_schema: coerce unambiguous type mismatches (stringified numbers/booleans, scalar to array, JSON-stringified objects), drop undeclared keys on closed schemas, fill missing required fields with type-correct empties - nested and depth-capped. openai-to-claude now buffers arguments for any tool with a declared schema (previously only named-shim tools) and emits one corrected input_json_delta at block close. The finish path also validates every accumulated argBuffer and emits a terminal error event when upstream truncated the JSON mid-call, so the turn is retried instead of executing a cut command. chatCore now passes whichever body still carries tools into the stream state - executor paths that consume/re-key the tools array (e.g. Devin Desktop) left the dispatched body without client schemas. (cherry picked from commit 84754f5) * fix(devin-desktop): encode CompletionConfig to stop silent 1024-token output cap The Devin Desktop Connect GetChatMessage wire was missing the CompletionConfiguration sub-message (field diegosouzapw#8). Without it, the upstream server defaulted output to ~1024 tokens for swe-1-7, which truncated Claude Code multi-tool turns (e.g. five Agent calls), produced empty Agent payloads, and led to the mid-response server error. Encode field diegosouzapw#8 with the OpenAI request's max_tokens (default 8192), temperature clamped to >=0.001, top_p, and top_k. Set max_newlines to a safe context-window value so it cannot accidentally become the binding output cap. Log the resolved max_tokens for monitoring. Update the golden fixture and add a focused test for custom values and the temperature=0 clamp. (cherry picked from commit 62effb4) * feat: add Kiro PWA bridge and websocket proxy support Publish the Kiro OAuth/PWA bridge flow together with websocket proxy scripts and route updates so the branch can be shared for iteration before full test validation completes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (cherry picked from commit 056a413) * feat(kiro): implement automatic account deactivation on quota exhaustion (cherry picked from commit 6a98790) * fix(health): add public /api/health route for external liveness probes - Add '/api/health' to PUBLIC_READONLY_API_ROUTE_PREFIXES so the auth pipeline classifies it as public (GET-only). - Create src/app/api/health/route.ts with a lightweight { status, timestamp } response, parallel to the existing /api/health/ping handler. External healthcheck/monitoring services probing /api/health were returning HTTP 401 (AUTH_001) because the route was not in any public prefix list. /api/monitoring/health was public but /api/health was not. (cherry picked from commit 6dac784) * feat: implement Freebuff CLI emulator and provider meta-service for connection and quota management (cherry picked from commit 1ac3383) * feat(freebuff): flip default from opt-in to opt-out Previously the Freebuff provider required FREEBUFF_ENABLED=1 to be exposed under /providers/freebuff. This commit flips the default so the provider is on by default (no env var required) and FREEBUFF_ENABLED=0 disables it explicitly. Changes: - base.ts: isFreebuffEnabled() returns true when env var is unset, empty, or any value other than 0/false. - registry.ts: error messages reference FREEBUFF_ENABLED=0 instead of =1. - en.json / fr.json: notEnabled.description updated to match. - .env.example + docs/providers/freebuff*.md: documentation reflects the new default and the disable-by-setting-FREEBUFF_ENABLED=0 flow. - freebuff-base.test.ts + freebuff-registry.test.ts: tests for the new default behaviour. (cherry picked from commit 050a34c) * fix(security): resolve freebuff public client credential and satisfy db re-export gate (cherry picked from commit a5b141f) * fix(review): address PR #1 review threads - chore(changelog): drop four stale fragments already in CHANGELOG (diegosouzapw#9435, diegosouzapw#9550, diegosouzapw#9568, diegosouzapw#9575) — stale-fragment gate exits nonzero on duplicate - fix(translator/claudeHelper): re-filter empty messages after the Pass 1.4 nameless-tool_use drop so non-final assistant messages whose only content was a nameless block do not survive into the request (#5, #6) - fix(ws/responses-ws-proxy): serialize concurrent forwardClientMessage calls via dispatchQueue so a second client message arriving during ensureUpstream does not drop the late frame (diegosouzapw#7) - fix(ws/v1-ws-bridge): close the session with code 1002 after a frame decode failure so an undecodable byte run does not loop forever (diegosouzapw#8) - fix(ws/both): send the WebSocket close frame BEFORE flipping this.closed so the sendFrame guard does not turn the close handshake into a no-op (diegosouzapw#9) - fix(oauth/freebuff): add 'network_error' to freebuffPollResponseSchema so the network-error return path typechecks (diegosouzapw#10) - fix(oauth/freebuff): declare onProgress on FreebuffPollOptions so the poll-loop callback typechecks (diegosouzapw#11) - fix(freebuff/cliEmulator): add readonly ok to FreebuffHttpResponse and populate it in tlsClientFetch, wreqFetch, and globalFetch so successful responses are not misclassified as failures (diegosouzapw#12) - test(translator/openai-to-claude): defer content_block_start until a name arrives and throw on a permanently nameless streamed call so the Muse/GLM diegosouzapw#2077 sequence and the nameless-rejection test both pass (diegosouzapw#16) - test(9568): resolve the three merge-marker regions in favor of HEAD — the test reflects the post-diegosouzapw#10392 translator behavior (diegosouzapw#13, diegosouzapw#14, diegosouzapw#15) * fix(review): address PR #1 review round 2 - docs(freebuff-api): replace nonexistent provider-specific routes with the generic [provider]/{models,limits,chat,embeddings,images} surface so the doc no longer documents 404-bound URLs (#1) - fix(translator/claudeHelper): scope the '(empty response)' placeholder drop to placeholders whose adjacent message actually had a paired tool_use/tool_result that this pass dropped; previously every historical placeholder was removed whenever any declared tool had required fields, silently deleting unrelated content and reordering turns - fix(translator/openai-to-claude): suppress input_json_delta passthrough while content_block_start is pending; replay the accumulated argBuffer in one shot after the late name arrives so the diegosouzapw#2077 Muse sequence produces a single start event with the full argument history instead of orphan deltas (diegosouzapw#16 follow-up) - fix(guardrails/visionBridge): a mixed combo (some targets lack vision) now returns 'process' instead of 'skip' so images are described before fan-out; matches the contract test at tests/unit/guardrails/visionBridge-combo-reroute.test.ts:147 - refactor(freebuff/httpClient): remove the tls-client-node fallback — it is not a declared dependency and tests/unit/tls-client-wreq-residue.test.ts asserts it stays absent; only wreq-js + global fetch remain as backends - fix(publicApiRoutes): remove '/api/health' from the CORS-relaxed list — the file's own comment says it must stay only in PUBLIC_READONLY_API_ROUTES_EXACT to avoid widening CORS - feat(pwa-register): wire shouldEnablePwaRegistration and isOmniRouteServiceWorkerScript into PwaRegister; loopback hostnames now unregister only matching OmniRoute workers rather than every worker and every cache entry - fix(oauth/kiro): extend CREDITS_EXHAUSTED_SIGNALS with Kiro-specific patterns ('usage limit exceeded', 'ThrottlingException') so the auto-deactivate path detects Kiro quota exhaustion; anchored on Kiro-distinguishing language to avoid broadening generic 429 classification - test(oauth-kiro-poll): drop 'as any' on connections[0] in favor of the inferred Record<string, unknown> from getProviderConnections --------- Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Thinh <thinh0704hcm@users.noreply.github.com> Co-authored-by: Vonic <phanquochoipt@gmail.com> Co-authored-by: Aminetwiti <93622642+Aminetwiti@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Moutia <114811455+Moutia-Ben-Yahia@users.noreply.github.com> Co-authored-by: amine <amine@TweeDev.localdomain> Co-authored-by: Mohamed Benyahia <benyahiamohamd@gmail.com> Co-authored-by: Raudbjorn <Raudbjorn@users.noreply.github.com>
…LOCAL_ONLY (#13745) GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89 — the same defect class as /api/acp/agents (GHSA-hf57): a route whose handler chain spawns a host process was classified Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth when requireLogin=false. Hard Rules #15/#17 require the LOCAL_ONLY gate, which runs on the stamped real peer before any auth check. cli-tools (GHSA-35fw): 14 routes reach getCliRuntimeStatus() -> locateCommand() -> runProcess("sh", ["-c", 'command -v -- "$1"']) -> spawn(), exactly like their six gated siblings (forge/grok-build/jcode/qwen/omp/letta-settings): all-statuses, status, and the claude/cline/codewhale/codex/crush/deepseek-tui/ droid/kilo/openclaw/pi/smelt-settings routes. The advisory counted 13; it missed /api/cli-tools/detect, which is heavier — detectAllTools() runs execFile(binary, ["--version"]) and execFile("which") per tool. skills (GHSA-jx89): POST /api/skills/install stores the request's handlerCode verbatim as the skill handler with no allowlist, so a value equal to a built-in name (execute_command / eval_code) aliases the real sandboxed built-in; POST /api/skills/executions then runs it. The sandbox is a real container, but the spawn is transitive, which is why the 6A.8 source scan never flagged it. Entries are exact paths, not a /api/cli-tools/ blanket prefix: apply, backups, config, guide-settings, hermes-agent-settings, keys, logs, openclaw/auto-order and codex-profiles do not spawn and remote dashboards use them. All 16 are mirrored into SPAWN_CAPABLE_PREFIXES (no manage-scope bypass) and added to the route-guard-membership roots so the gate enforces them from now on. Functional trade-off, same one already accepted for grok/forge/jcode/qwen: a dashboard served through a tunnel no longer shows the CLI Tools status badges. Tests are red-first. Two existing negative controls pointed at routes that turn out to spawn (/api/cli-tools/all-statuses, /api/skills/install); they now point at routes that genuinely do not (/api/cli-tools/config, /api/skills/marketplace, /api/skills/skillssh/install), so the non-over-gating assertions are kept.
…apw#15/diegosouzapw#17) runManagedDbHealthCheck() now forks native diagnostics into a child process via healthCheckRunner.ts. Every process-spawning route must be classified isLocalOnlyPath() so a leaked JWT via tunnel cannot trigger process spawning, matching the /api/db-backups/exportAll precedent. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Merged after a maintainer rework that kept every one of @HouMinXi's commits intact. **What the rework added on top of the contribution:** the new DB health-check behaviour is gated behind a default-off feature flag (`src/shared/constants/featureFlagDefinitions.ts`, `defaultValue: "false"`), documented in `docs/reference/FEATURE_FLAGS.md` with the description key carried into all 66 locales, so the release default is unchanged and the new bounds only apply when an operator opts in. The optional-FTS5 migration set was reconciled by hand with the "180" entry that landed meanwhile (`src/lib/db/migrationRunner/constants.ts`). **Carried from your rebased head:** the `/api/db/health` local-only classification in `src/server/authz/routeGuard.ts` plus its `routeGuard` assertion — `runManagedDbHealthCheck()` forks native diagnostics into a child process, so Hard Rules #15/#17 apply. Re-verified here: 37 pass / 0 fail. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you for the depth of this one — the resource-bounds suite and the sql.js startup/backup coverage are the kind of tests that keep a database layer honest.
Two Fast Quality Gates failures on diegosouzapw#12400. `check:test-discovery` reported all three new suites as orphans: the unit runner's subdirectory glob is an explicit allowlist (`tests/unit/{api,auth,authz,...}/**`) and `deploy` is not in it, so nothing under `tests/unit/deploy/` was ever collected. The suites passed locally and in no CI job — the exact failure mode that gate was added for after the 2026-06-09 audit found ~135 orphaned tests, one of them a Hard Rules diegosouzapw#15/diegosouzapw#17 check whose asserts were already failing unnoticed. Moved the three files to `tests/unit/` rather than widening the allowlist: adding a directory there changes the runner for every contributor, which is not this PR's business. Import depths adjusted; the fourth suite was already at the collected level. `check:complexity-ratchets` flagged `buildDeployment` at 83 lines against a limit of 80. The pod template is almost entirely the container object, so that object moved to `buildContainer`. No behaviour change — the generator's output is byte-identical, which the parity test against deploy/kubernetes/base pins. Both gates verified locally: test-discovery OK, complexityNewCode=0.
…loopback-only) + dashboard UI (diegosouzapw#5939) * feat(discovery): Phase 2 reporter — discoveryResults DB module + service wiring Adds src/lib/db/discoveryResults.ts (CRUD over the discovery_results table from migration 074) and wires the opt-in discovery service to persist and read findings through it: persistDiscoveryResult / getDiscoveryResults / getDiscoveryResultById / markVerified / deleteDiscoveryResult, with (provider, method, endpoint) upsert de-duplication. Re-exported from localDb. The service stays opt-in / default-off. The /api/discovery/* routes and the dashboard UI tab are intentionally deferred to Phase 2b — they need the local-only enforcement model (Hard Rules diegosouzapw#15/diegosouzapw#17 territory) decided first. TDD: tests/unit/db/discovery-results.test.ts (8 cases, DB + service delegation), isolated DATA_DIR with resetDbInstance cleanup. * feat(discovery): Phase 2b — /api/discovery/* routes (strict loopback-only) Adds the discovery HTTP surface on top of the reporter DB module: GET /api/discovery/results list findings (optional ?providerId) GET /api/discovery/results/:id one finding (404 if absent) DELETE /api/discovery/results/:id delete a finding POST /api/discovery/scan scan a provider + persist findings POST /api/discovery/verify/:id mark a finding verified Authorization: strict loopback-only. "/api/discovery/" is added to LOCAL_ONLY_API_PREFIXES so the central authz pipeline (proxy.ts → runAuthzPipeline → managementPolicy) rejects non-loopback callers with a 403 LOCAL_ONLY before any handler runs. It is deliberately NOT in LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES — no remote manage-scope bypass — because POST /scan issues outbound probes to provider endpoints (SSRF-adjacent) and must never be tunnel-reachable. Handlers also call requireManagementAuth (defense in depth) and return sanitized errors via createErrorResponse. Tests: - tests/unit/authz/discovery-routes-local-only.test.ts (8) — security guard: isLocalOnlyPath true + not manage-scope-bypassable for all four paths. - tests/unit/api/discovery-routes.test.ts (6) — handler integration over an isolated DATA_DIR: list/filter, by-id 200/404/400, scan persist + 400 on empty/malformed body, verify 200/404, delete 200/404, no stack-trace leak. * feat(discovery): Phase 2c — dashboard UI tab (Tools → Discovery) Adds the /dashboard/discovery page (DiscoveryPageClient) that consumes the Phase 2b /api/discovery/* routes: scan a provider, list findings, verify or delete them. Registered in the sidebar under the Tools group (icon travel_explore) and given a "discovery" i18n namespace + sidebar keys in en.json (other locales fall back to en via next-intl until synced — the locale files are in a pre-existing coverage deficit unrelated to this change). Registers the UI test path in vitest.config.ts (advisory ui suite). Tests: src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx (3 cases: loads+renders results, empty state, fetches /api/discovery/results on mount; stable useTranslations mock to avoid the fetch-loop). NOTE: the ui vitest suite cannot run in this workspace — @testing-library/dom (a @testing-library/ react peer dep) is absent from node_modules, which fails ALL existing ui tests equally; the test runs in CI. Component verified locally via typecheck + lint. * test(discovery): register discovery-routes-local-only in stryker tap.testFiles The mutation-test-coverage gate (--strict) flags any unit test covering a mutated module that isn't listed in stryker.conf.json tap.testFiles. This PR's tests/unit/authz/discovery-routes-local-only.test.ts covers src/server/authz/ routeGuard.ts (a mutated module, which this PR edits by adding the /api/discovery/ local-only prefix), so it must be registered for its mutant kills to count. No behavior change. * refactor(discovery): split DiscoveryPageClient to satisfy max-lines-per-function The complexity ratchet (max-lines-per-function: 80) flagged the single 184-line DiscoveryPageClient function (+1 over baseline). Extract the data layer into two hooks (useDiscoveryResults for list/loading/feedback, useDiscoveryActions for scan/verify/delete), a shared callApi helper, and two presentational sub-components (DiscoveryScanForm, DiscoveryResultCard). Every function is now under the 80-line ceiling; complexity gate back to baseline 1995. No behavior change — same exported component, same endpoints, same props. * test(sidebar): include discovery in omni-proxy item-order snapshot Adding the Discovery item to the Tools group (this PR's sidebar entry) extends the ordered omni-proxy section list. Update the exact-match deepEqual snapshot in sidebar-visibility.test.ts to include "discovery" in its position (after traffic-inspector). The assertion stays exact — this reflects the intentional new item, it does not weaken the check. * docs(changelog): restore release bullets eaten by merge auto-resolve; re-add discovery bullet additively * chore(quality): bump testFrozen for translator-openai-responses-req.test.ts (1097 -> 1172) Base-red inherited from diegosouzapw#5933, which grew the test file to 1171 lines (Hard Rule diegosouzapw#18 regression tests) without adjusting the frozen cap. The release tip itself fails check:file-size; this unblocks every PR into release/v3.8.44. File untouched by this PR. * chore(quality): restore stryker tap.testFiles entries eaten by merge auto-resolve The merge of origin/release/v3.8.44 silently dropped the 3 entries added on the release side (diegosouzapw#5903, clinepass, diegosouzapw#5923). Took the release version verbatim and re-added only this PR's entry (discovery-routes-local-only) in alphabetical order. check:mutation-test-coverage green locally. * chore(quality): reconcile inherited v3.8.44 merge-burst drift + include discovery in tools-group order test - complexity 1995->2003 and cognitive 856->859: both measure IDENTICAL on the pristine release tip (947a0b0) and this PR's merged HEAD — the PR is complexity-net-zero; drift is from the 2026-07-02 merge burst (notes added to both baselines, same family as prior reconciliations). - sidebar-tools-group.test.ts: append 'discovery' to the expected TOOLS_GROUP order — the intentional new sidebar item this PR adds (same expected-value update already made in sidebar-visibility.test.ts).
…ecar supported) (diegosouzapw#4649) Integrated into release/v3.8.37 — headroom proxy lifecycle (status/start/stop, local-only + spawn-capable per Rules diegosouzapw#15/diegosouzapw#17). Cherry-picked onto release tip; lifecycle 7/7 + route-guard 43/43 + check:cycles green.
…te-guard) (diegosouzapw#5070) The Cursor auto-import route runs execFile("which", ["cursor"]) to verify a local Cursor install before importing credentials — a child-process spawn. The check:route-guard-membership gate (Hard Rules diegosouzapw#15/diegosouzapw#17) flagged it as an unclassified spawn-capable route: reachable past the loopback gate, an RCE-via-tunnel surface (a leaked JWT over a tunnel could trigger the spawn). Classify the specific path in LOCAL_ONLY_API_PREFIXES so loopback enforcement runs unconditionally before any auth check. Scoped to the exact path — the rest of /api/oauth/ (browser redirect/callback flows) stays remote-reachable. TDD: added a failing-then-passing assertion in route-guard-local-prefix.test.ts (classification + an over-broadening guard proving sibling OAuth paths stay remote). check:route-guard-membership now reports 0 new gaps.
…ment guardrails (Phases 0-6) (diegosouzapw#3471) * feat(quality): generic ratchet comparator (multi-metric, regression-only) * chore(ci): Fase 0 quality-gate fixes — reconcile coverage gate (40->60), tier npm audit, wire orphaned contract gates, re-enable cheap husky pre-commit * feat(quality): ratchet engine (collector + frozen baseline + CI job) and provider-consistency gate - collect-metrics.mjs: emits quality-metrics.json (ESLint warnings + coverage when present) - quality-baseline.json: frozen baseline (eslintWarnings=3482, regression-only) - ci.yml: quality-gate job (ratchet + step summary + artifact) and check:provider-consistency in lint job - check-provider-consistency.ts: every REGISTRY id must be a canonical provider (found krutrim half-registered → allowlisted as known pre-existing, blocks any NEW orphan) - TDD: 9 tests (5 ratchet + 4 provider-consistency) * feat(quality): Fase 2 anti-hallucination gates — fetch-targets, openapi-routes, deps allowlist - check-fetch-targets: every dashboard fetch(/api/...) resolves to a real route.ts; found 7 pre-existing dashboard->route mismatches frozen as KNOWN_MISSING for triage - check-openapi-routes: every openapi.yaml path resolves to a real route; found 1 stale spec entry (agent-bridge agents/{id}/state) frozen as KNOWN_STALE_SPEC - check-deps: anti-slopsquatting allowlist (105 deps); new deps need explicit human-reviewed entry - all wired into CI lint/docs jobs; TDD +12 tests (21 total across 5 gates) * docs(quality): add quality-gates report + implementation plan to repo root * feat(quality): Fase 3a — file-size ratchet (freeze 91 files >800 LOC, cap 800 for new) - check-file-size.mjs: frozen files can only shrink; new files must be <= cap (kills the next 12k-line god-component) - file-size-baseline.json: 91 files frozen at current LOC (largest 12883) - wired into CI lint job; TDD 5 tests; --update ratchets the baseline down on shrink * feat(quality): Fase 3b — duplication ratchet (jscpd@4, baseline 5.72%) - check-duplication.mjs: runs jscpd@4 (pinned; v5 is an incompatible Rust rewrite) over src+open-sse, fails if duplication % rises vs frozen baseline (5.72%, measured: 1358 clones / 22967 dup lines). Targets the executor copy-paste (48/50 override execute() wholesale) - wired into the parallel quality-gate CI job (off the lint critical path); TDD 4 tests; --update ratchets down - snapshot now complete: coverage ~82.6%, eslint 3482 (98.5% no-explicit-any), duplication 5.72%, 91 files >800 LOC * feat(quality): Fase 4a — anti test-masking gate - check-test-masking.mjs: for each MODIFIED test file in a PR, flags net assert removal + new assert.ok(true) tautologies (base...HEAD diff). Directly enforces CLAUDE.md 'never weaken asserts to go green' - wired into pr-test-policy CI job (reuses base fetch); no-op outside PR; TDD 5 tests * feat(quality): Fase 4b — coverage ratchet (conservative floors, CI consumes merged coverage) - quality-baseline.json: coverage.{statements,lines,functions,branches} floors (80/80/82/73, real ~82.58/82.58/84.23/75.22 with margin; tighten via --update after a green main run) - check-quality-ratchet.mjs: --allow-missing (local quality:gate skips coverage.* without a coverage run; CI runs strict) - ci.yml quality-gate job: needs test-coverage + downloads merged coverage-report so the ratchet enforces 'coverage cannot drop' - TDD +1 test (6 total) * feat(quality): Fase 6 — 8 new gates (Rule diegosouzapw#11/diegosouzapw#12, migrations, known-symbols, route-guard, complexity, docs-symbols, db-rules) Deterministic gates, each freezing pre-existing violations in a documented allowlist (ratchet) so they pass now and block only NEW regressions: - check-error-helper (Rule diegosouzapw#12): 7 executors/handlers forwarding raw err.message frozen - check-public-creds (Rule diegosouzapw#11): 5 literal client_ids (Claude/Codex/Qwen/Kimi/Copilot) frozen - check-migration-numbering: gaps 026/055 + dup 041 frozen (prevents the git-rm-deleted-migration incident) - check-known-symbols: 93 executors conformance + 15 combo strategies + 18 translator pairs - check-route-guard-membership (diegosouzapw#15/diegosouzapw#17): all 25 spawn-capable routes verified local-only (0 gaps) - check-complexity: cyclomatic>15 / fn-length>80 ratchet (baseline 1739) - check-docs-symbols: 30 stale doc /api refs frozen (docs hallucination) - check-db-rules (diegosouzapw#2/diegosouzapw#5): 25 unexported db modules + 15 raw-SQL routes frozen Wired into CI (lint / docs-sync-strict / quality-gate jobs). 115 TDD tests, all green. ESLint ratchet held at 3482. * docs(quality): Phase 7 plan (security/dead-code/mutation/community tooling) — GATED to 2026-06-16 Stored, not active. 7 suggested gates + all discussed OSS/Community tools (SonarQube Community + osv-scanner + CodeQL + knip + sonarjs + type-coverage + lockfile-lint + Stryker + size-limit + axe-core + semcheck + agent-lsp + Qlty). Activation gate: do not start before 2026-06-16 (use Phases 0-6 in production for 1 week, validate in practice, then evolve).
…ired, 2 production bug fixes, vitest in CI (diegosouzapw#3536) check-test-discovery gate (TDD; 195 orphans found, 135 re-wired into the node runner, 60 frozen+annotated). Triage fixed 2 real production bugs: missing BYPASS_PREFIX_NOT_ALLOWED zod refine (spawn-capable prefixes accepted into the bypass list, Hard Rules diegosouzapw#15/diegosouzapw#17) and resetDbInstance not firing stateReset resetters (stale schema memo → 503 instead of 403; also hit backup-restore). New test-vitest CI job: test:vitest blocking (146/146), test:vitest:ui informational (14 pre-existing fails, triage 2026-06-16).
…-shell install Add /api/cli-tools/whycodes-settings to LOCAL_ONLY_API_PREFIXES, SPAWN_CAPABLE_PREFIXES and the route-guard membership audit (Hard Rules diegosouzapw#15/diegosouzapw#17), replace the curl|bash install step with a link to the vendor docs, and move the whycodes catalog test to a collected path. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Changes
bin/omniroute.mjs— CLI entry point with banner, auto-open browser, graceful shutdownscripts/prepublish.mjs— Build script to generate Next.js standalone inapp/.github/workflows/npm-publish.yml— Auto-publish to npm on GitHub Releasepackage.json— name=omniroute, bin, files, engines, keywords, prepublishOnlynext.config.mjs— Addedoutput: 'standalone'LICENSE— MIT.npmignore/.gitignore— Updated for app/ build artifactUsage after publish
Auto-publish workflow
When creating a GitHub Release (e.g.
v0.1.0), npm publish runs automatically via GitHub Actions.