From 0a800d87e17fa7560a8ddcaef7b0159b6b59274b Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 13 May 2026 10:31:20 -0300 Subject: [PATCH 1/5] chore(env): remove orphan vars from .env.example Removes 11 environment variable entries from `.env.example` that no longer correspond to any reference in source code: - Provider API keys with no runtime hook today: CEREBRAS_API_KEY, NEBIUS_API_KEY, PERPLEXITY_API_KEY, MISTRAL_API_KEY, COHERE_API_KEY, TOGETHER_API_KEY, GROQ_API_KEY, XAI_API_KEY, FIREWORKS_API_KEY. Provider credentials are managed via Dashboard/Providers or the encrypted DB; the leftover entries were stubs only. - CURSOR_PROTOBUF_DEBUG (executor uses CURSOR_DEBUG/CURSOR_STREAM_DEBUG). - CLI_COMPAT_KIRO (Kiro is in CLI_COMPAT_OMITTED_PROVIDER_IDS). Kept four entries from the original audit list because they are still exercised at runtime (verified via grep on docker-compose and dynamic `${PROVIDER}_USER_AGENT` lookup in BaseExecutor): - ANTIGRAVITY_USER_AGENT (dynamic in BaseExecutor.buildHeaders) - KIRO_USER_AGENT (same dynamic pattern) - PROD_API_PORT, PROD_DASHBOARD_PORT (docker-compose.prod.yml) Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 1ad4b13a3c..8c49d88cc8 100644 --- a/.env.example +++ b/.env.example @@ -491,7 +491,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # CLI_COMPAT_CLAUDE=1 # CLI_COMPAT_GITHUB=1 # CLI_COMPAT_ANTIGRAVITY=1 -# CLI_COMPAT_KIRO=1 # CLI_COMPAT_CURSOR=1 # CLI_COMPAT_KIMI_CODING=1 # CLI_COMPAT_KILOCODE=1 @@ -509,20 +508,16 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # Preferred setup: Dashboard → Providers → Add API Key. # Setting here is an alternative for Docker/headless deployments. +# Static API keys for direct-authentication providers wired through the runtime. +# OmniRoute loads provider credentials from the encrypted database or +# data/provider-credentials.json. The variables below are documented escape +# hatches that are referenced in code today. # DEEPSEEK_API_KEY= -# GROQ_API_KEY= -# XAI_API_KEY= -# MISTRAL_API_KEY= -# PERPLEXITY_API_KEY= -# TOGETHER_API_KEY= -# FIREWORKS_API_KEY= -# CEREBRAS_API_KEY= -# COHERE_API_KEY= # NVIDIA_API_KEY= # Embedding Providers (optional — used by /v1/embeddings) -# NEBIUS_API_KEY= -# Provider keys above (OpenAI, Mistral, Together, Fireworks, NVIDIA) also work for embeddings. +# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers +# also work for embeddings. # ═══════════════════════════════════════════════════════════════════════════════ @@ -783,9 +778,6 @@ APP_LOG_TO_FILE=true # ═══════════════════════════════════════════════════════════════════════════════ # These variables enable verbose debugging output. NEVER enable in production. -# Dump Cursor protobuf decode/encode details to console. -# CURSOR_PROTOBUF_DEBUG=1 - # Dump raw Cursor SSE stream data to console. # CURSOR_STREAM_DEBUG=1 From a7a42140a0f45b1a446e00705b91de76ef98fd43 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 13 May 2026 10:44:34 -0300 Subject: [PATCH 2/5] chore(env): add missing OMNIROUTE_*, provider, and CLI vars to .env.example Documents 63+ environment variables that are referenced in source today but were absent from the .env.example contract. Variables grouped by area: - Section 3 (network): HOST, HOSTNAME bind overrides. - Section 7 (URLs/cloud): KIE_CALLBACK_URL, OMNIROUTE_KIE_CALLBACK_URL, OMNIROUTE_PUBLIC_URL. - Section 10 (MCP & background): OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS, OMNIROUTE_MCP_DESCRIPTION_COMPRESSION, OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS, OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS, OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS, OMNIROUTE_SPEND_FLUSH_INTERVAL_MS, OMNIROUTE_SPEND_MAX_BUFFER_SIZE, OMNIROUTE_CONFIG_HOT_RELOAD_MS, OMNIROUTE_MIGRATIONS_DIR, OMNIROUTE_RTK_TRUST_PROJECT_FILTERS, OMNIROUTE_FORCE_DB_HEALTHCHECK, OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS, OMNIROUTE_DISABLE_REDIS_AUTH_CACHE, ANTIGRAVITY_CREDITS. - Section 11 (OAuth): GITLAB_DUO_BASE_URL, GITLAB_BASE_URL, GITLAB_OAUTH_CLIENT_ID, GITLAB_OAUTH_CLIENT_SECRET. - Section 13 (CLI fingerprint): KIMI_CLI_VERSION, KIMI_CODING_DEVICE_ID. - Section 14 (API keys): WINDSURF_API_KEY. - Section 21 (proxy health): RATE_LIMIT_AUTO_ENABLE, HEALTHCHECK_STAGGER_MS. - Section 22 (debugging): CURSOR_DEBUG, CURSOR_DUMP_FILE, CURSOR_STREAM_TIMEOUT_MS, CURSOR_STATE_DB_PATH, CURSOR_TOKEN, OMNIROUTE_LOG_REQUEST_SHAPE. - Section 23 (GitHub): GITHUB_TOKEN. - New section 24 (provider quotas / tunnels / sandbox): ALIBABA_CODING_PLAN_HOST, ALIBABA_CODING_PLAN_QUOTA_URL, CONTEXT_RESERVE_TOKENS, MODEL_ALIAS_COMPAT_ENABLED, CLI_DEVIN_BIN, COMMAND_CODE_CALLBACK_PORT, MITM_LOCAL_PORT, MITM_DISABLE_TLS_VERIFY, ONEPROXY_ENABLED, ONEPROXY_API_URL, ONEPROXY_MAX_PROXIES, ONEPROXY_MIN_QUALITY_THRESHOLD, TAILSCALE_BIN, TAILSCALED_BIN, NGROK_AUTHTOKEN, DB_BACKUP_MAX_FILES, DB_BACKUP_RETENTION_DAYS, OMNIROUTE_TLS_PROXY_URL, SKILLS_MAX_FILE_BYTES, SKILLS_MAX_HTTP_RESPONSE_BYTES, SKILLS_MAX_SANDBOX_OUTPUT_CHARS, SKILLS_SANDBOX_TIMEOUT_MS, SKILLS_SANDBOX_NETWORK_ENABLED, SKILLS_ALLOWED_SANDBOX_IMAGES. - New section 25 (test & E2E): OMNIROUTE_E2E_BOOTSTRAP_MODE, OMNIROUTE_E2E_PASSWORD, OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK, OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK, OMNIROUTE_HIDE_HEALTHCHECK_LOGS, OMNIROUTE_PLAYWRIGHT_SKIP_BUILD, OMNIROUTE_SKIP_UNINSTALL_HOOK, ECOSYSTEM_SERVER_WAIT_MS, ELECTRON_SMOKE_URL, ELECTRON_SMOKE_TIMEOUT_MS, ELECTRON_SMOKE_SETTLE_MS, ELECTRON_SMOKE_APP_EXECUTABLE, ELECTRON_SMOKE_DATA_DIR, ELECTRON_SMOKE_KEEP_DATA, ELECTRON_SMOKE_STREAM_LOGS. All entries are commented out (`#KEY=default`) so behavior is unchanged until an operator explicitly enables them. Defaults reflect the actual values used by the source (e.g. 6h DB healthcheck interval, 10m budget job, 30m reasoning cleanup, 60s spend flush). Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 8c49d88cc8..ab1dec0d05 100644 --- a/.env.example +++ b/.env.example @@ -91,6 +91,12 @@ OMNIROUTE_USE_TURBOPACK=1 # Used by: src/lib/runtime/ports.ts — preserves canonical port in Electron. # OMNIROUTE_PORT=20128 +# Hostname/bind address for the Next.js server. +# Used by: scripts/run-next.mjs (HOST), Playwright runner (HOSTNAME). +# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests). +#HOST=0.0.0.0 +#HOSTNAME=127.0.0.1 + # Environment mode — affects Next.js behavior, logging verbosity, and caching. # Values: production | development | Default: production NODE_ENV=production @@ -250,6 +256,13 @@ NEXT_PUBLIC_CLOUD_URL= # Legacy alias — fallback for NEXT_PUBLIC_BASE_URL in sync schedulers. # NEXT_PUBLIC_APP_URL=http://localhost:20128 +# Public callback URL for asynchronous image/audio jobs (kie.ai, etc.). +# Used by: open-sse/utils/kieTask.ts — overrides callbackUrlFromBaseUrl(). +# Honor order: KIE_CALLBACK_URL → OMNIROUTE_KIE_CALLBACK_URL → OMNIROUTE_PUBLIC_URL. +#KIE_CALLBACK_URL= +#OMNIROUTE_KIE_CALLBACK_URL= +#OMNIROUTE_PUBLIC_URL= + # ═══════════════════════════════════════════════════════════════════════════════ # 8. OUTBOUND PROXY (Upstream Provider Calls) @@ -332,6 +345,17 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # Full list: admin, combos, health, models, routing, budget, metrics, pricing, memory, skills # OMNIROUTE_MCP_SCOPES=admin,combos,health +# Compress MCP tool descriptions before serializing the manifest. +# Used by: open-sse/mcp-server/descriptionCompressor.ts — reduces token spend +# for clients that read the full tool catalog. +# Accepted disabling values: 0, false, off. Default: enabled. +# OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS=1 + +# Algorithm/profile used when description compression is enabled. +# Used by: open-sse/mcp-server/descriptionCompressor.ts +# Set to 0/false/off to skip compression entirely. Default: rtk +# OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk + # Model catalog sync interval in hours. # Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh. # Default: 24 @@ -347,6 +371,48 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70 # Useful for: CI builds, test environments, or resource-constrained containers. # OMNIROUTE_DISABLE_BACKGROUND_SERVICES=false +# Force runtime background tasks (healthchecks/sync) even under automated test +# detection. Used by: src/lib/config/runtimeSettings.ts — overrides the test +# heuristic in instrumentation-node.ts. Default: unset (tests skip background). +#OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS=1 + +# Background job interval for budget reset checks (ms). Default: 600000 (10m). +# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000. +#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000 + +# Reasoning cache cleanup cadence (ms). Default: 1800000 (30m). Floor: 60000. +# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts. +#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000 + +# Spend write batcher cadence (ms) and buffer size before forced flush. +# Used by: src/lib/spend/batchWriter.ts. Defaults: 60000 ms / 1000 entries. +#OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000 +#OMNIROUTE_SPEND_MAX_BUFFER_SIZE=1000 + +# Config hot-reload polling interval (ms). Default: 5000. +# Used by: src/lib/config/hotReload.ts. Lower than 1000ms is rejected. +#OMNIROUTE_CONFIG_HOT_RELOAD_MS=5000 + +# Override the migrations directory used by src/lib/db/migrationRunner.ts. +# Default: /src/lib/db/migrations. +#OMNIROUTE_MIGRATIONS_DIR= + +# Trust user-managed RTK project filter rules without strict signature checks. +# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0. +#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0 + +# Force a DB healthcheck regardless of cadence. Default: 0. +# Used by: src/lib/db/core.ts::shouldRunDbHealthCheck(). +#OMNIROUTE_FORCE_DB_HEALTHCHECK=0 + +# DB healthcheck cadence override (ms). Default: 21600000 (6h). +# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs(). +#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000 + +# Skip the Redis-backed auth cache used by API key lookups (forces DB reads). +# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled. +#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0 + # Flag set by bootstrap script after initial setup is complete. # Used by: src/app/(dashboard)/dashboard/page.tsx — shows setup wizard vs. dashboard. # OMNIROUTE_BOOTSTRAPPED=false @@ -355,6 +421,11 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70 # Used by: open-sse/executors/antigravity.ts — escape hatch for multi-project setups. # OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=0 +# Adjust how Antigravity advertises remaining credits. Used by: +# open-sse/services/antigravityCredits.ts — accepts forced override strings. +# Default: empty (use upstream-reported credits). +#ANTIGRAVITY_CREDITS= + # ═══════════════════════════════════════════════════════════════════════════════ # 11. OAUTH PROVIDER CREDENTIALS @@ -406,6 +477,14 @@ WINDSURF_FIREBASE_API_KEY=AIzaSyBpLTEGSt59AUPKxBb7lIWjSE2ZXQH7mgU # Required scopes: api, read_user, openid, profile, email # GITLAB_DUO_OAUTH_CLIENT_ID=*** # GITLAB_DUO_OAUTH_CLIENT_SECRET=*** # optional — PKCE flow does not require a secret +# +# Self-managed GitLab Duo instance overrides. +# Used by: src/lib/oauth/gitlab.ts and src/lib/oauth/constants/oauth.ts — +# fall back to these when the _DUO_ variants above are unset. +#GITLAB_DUO_BASE_URL=https://gitlab.com +#GITLAB_BASE_URL=https://gitlab.com +#GITLAB_OAUTH_CLIENT_ID= +#GITLAB_OAUTH_CLIENT_SECRET= # ── Qoder ── QODER_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW @@ -500,6 +579,12 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # Or enable for all providers at once: # CLI_COMPAT_ALL=1 +# ── Kimi Coding CLI identity overrides ── +# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers. +# Leave unset to use the captured defaults baked into the OmniRoute build. +#KIMI_CLI_VERSION=1.36.0 +#KIMI_CODING_DEVICE_ID= + # ═══════════════════════════════════════════════════════════════════════════════ # 14. API KEY PROVIDERS @@ -515,6 +600,10 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # DEEPSEEK_API_KEY= # NVIDIA_API_KEY= +# Windsurf / Devin CLI direct API key. +# Used by: open-sse/executors/devin-cli.ts — bypasses OAuth when set. +# WINDSURF_API_KEY= + # Embedding Providers (optional — used by /v1/embeddings) # OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers # also work for embeddings. @@ -772,18 +861,49 @@ APP_LOG_TO_FILE=true # Used by: open-sse/services/rateLimitManager.ts # RATE_LIMIT_MAX_WAIT_MS=120000 +# Force the auto-enable rate limit safety net on/off regardless of the persisted +# Dashboard setting. Used by: open-sse/services/rateLimitManager.ts. +# Accepted values: true|1|on (force on), false|0|off (force off), unset (use Dashboard). +# RATE_LIMIT_AUTO_ENABLE= + +# Stagger interval (ms) between provider token healthchecks at startup. +# Used by: src/lib/tokenHealthCheck.ts. Default: 3000. +# HEALTHCHECK_STAGGER_MS=3000 + # ═══════════════════════════════════════════════════════════════════════════════ # 22. DEBUGGING # ═══════════════════════════════════════════════════════════════════════════════ # These variables enable verbose debugging output. NEVER enable in production. -# Dump raw Cursor SSE stream data to console. +# Cursor executor verbose debug (decoded SSE chunks, etc.). +# CURSOR_STREAM_DEBUG is kept as a backward-compatible alias. +# Used by: open-sse/executors/cursor.ts +# CURSOR_DEBUG=1 # CURSOR_STREAM_DEBUG=1 +# When CURSOR_DEBUG=1, also append raw decoded chunks to this file path. +# CURSOR_DUMP_FILE=/tmp/cursor-stream.log + +# Cursor stream idle timeout (ms). Default: 300000 (5 min). +# Used by: open-sse/executors/cursor.ts. +# CURSOR_STREAM_TIMEOUT_MS=300000 + +# Cursor state DB path override (for cursor version detection). +# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically. +# CURSOR_STATE_DB_PATH= + +# Direct Cursor bearer token used by scripts/cursor-tap.cjs (developer tooling). +# CURSOR_TOKEN= + # Log Responses API SSE-to-JSON translation details. # DEBUG_RESPONSES_SSE_TO_JSON=true +# Log request shape (content-type + content-length) for large chat payloads. +# Used by: src/app/api/v1/chat/completions/route.ts. Set to "0" to silence. +# Default: enabled. +# OMNIROUTE_LOG_REQUEST_SHAPE=1 + # Write raw (untruncated) request/response JSON in call log artifacts. # When enabled, serializeArtifactForStorage skips size-based truncation. # Also enabled automatically when APP_LOG_LEVEL=debug. @@ -805,3 +925,128 @@ APP_LOG_TO_FILE=true # GitHub Personal Access Token with issues:write scope. # GITHUB_ISSUES_TOKEN=ghp_xxxx + +# Generic GitHub access token consumed by issue triage / agent helpers. +# Used by: src/app/api/v1/issues/* and src/lib/cloudAgent/* — falls back to +# GITHUB_ISSUES_TOKEN when unset. +# GITHUB_TOKEN= + + +# ═══════════════════════════════════════════════════════════════════════════════ +# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS +# ═══════════════════════════════════════════════════════════════════════════════ +# Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug +# proxy), 1Proxy egress pool, skills sandbox runtime, and miscellaneous CLI +# binaries referenced by the executor layer or the dashboard runtime. + +# ── Alibaba (Bailian) coding plan quota ── +# Host/full URL override used by: open-sse/services/bailianQuotaFetcher.ts. +# When unset the fetcher uses the production Alibaba endpoints. +# ALIBABA_CODING_PLAN_HOST= +# ALIBABA_CODING_PLAN_QUOTA_URL= + +# ── Context window tuning ── +# Tokens reserved for completion output when computing prompt budgets. +# Used by: open-sse/services/contextManager.ts. Default: 1024. +# CONTEXT_RESERVE_TOKENS=1024 + +# ── Model alias rewriting (legacy compatibility) ── +# Toggle the legacy model-alias compatibility layer used by older clients. +# Used by: open-sse/services/model.ts. Default: enabled. +# MODEL_ALIAS_COMPAT_ENABLED=true + +# ── Devin CLI binary path ── +# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH. +# CLI_DEVIN_BIN=devin + +# ── Command Code (custom CLI) callback ── +# Local port used for OAuth-style callbacks from the Command Code CLI helper. +# Used by: src/app/api/providers/command-code/auth/shared.ts. +# COMMAND_CODE_CALLBACK_PORT= + +# ── MITM debug proxy (development only) ── +# Used by: src/mitm/server.cjs — captures upstream traffic for inspection. +# MITM_LOCAL_PORT=443 +# MITM_DISABLE_TLS_VERIFY=0 + +# ── 1Proxy egress pool ── +# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute +# CrofAI 1Proxy service. Disable, override URL, or tune the import quality. +# ONEPROXY_ENABLED=true +# ONEPROXY_API_URL=https://1proxy-api.aitradepulse.com +# ONEPROXY_MAX_PROXIES=500 +# ONEPROXY_MIN_QUALITY_THRESHOLD=50 + +# ── Tailscale tunnel binaries ── +# Optional explicit paths to tailscale/tailscaled binaries used by the +# dashboard's tunnel manager. Used by: src/lib/tailscaleTunnel.ts. +# TAILSCALE_BIN=/usr/local/bin/tailscale +# TAILSCALED_BIN=/usr/local/bin/tailscaled + +# ── Ngrok tunnel ── +# Used by: src/lib/ngrokTunnel.ts — authenticates outbound tunnels. +# NGROK_AUTHTOKEN= + +# ── Database backups ── +# Used by: src/lib/db/backup.ts. +# DB_BACKUP_MAX_FILES=20 +# DB_BACKUP_RETENTION_DAYS=0 + +# ── TLS sidecar override ── +# Used by: open-sse/services/chatgptTlsClient.ts tests. Production deployments +# should leave this unset; the sidecar is auto-managed. +# OMNIROUTE_TLS_PROXY_URL= + +# ── Skills sandbox (experimental) ── +# Used by: src/lib/skills/builtins.ts. All values support comma lists where +# noted in the source. +# SKILLS_MAX_FILE_BYTES=1048576 +# SKILLS_MAX_HTTP_RESPONSE_BYTES=256000 +# SKILLS_MAX_SANDBOX_OUTPUT_CHARS=100000 +# SKILLS_SANDBOX_TIMEOUT_MS=10000 +# SKILLS_SANDBOX_NETWORK_ENABLED=0 +# SKILLS_ALLOWED_SANDBOX_IMAGES= + + +# ═══════════════════════════════════════════════════════════════════════════════ +# 25. TEST & E2E +# ═══════════════════════════════════════════════════════════════════════════════ +# Used by scripts/run-next-playwright.mjs, scripts/smoke-electron-packaged.mjs, +# scripts/run-ecosystem-tests.mjs and scripts/uninstall.mjs. +# Production deployments should leave every value below unset. + +# E2E bootstrap mode for the Playwright runner. Accepted: auth | fresh | reuse. +# Default (when unset): auth. +# OMNIROUTE_E2E_BOOTSTRAP_MODE=auth + +# Admin password injected into the Playwright test environment. +# Falls back to INITIAL_PASSWORD when unset. +# OMNIROUTE_E2E_PASSWORD= + +# Disable the local healthcheck poll during Playwright runs (default: true). +# OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK=true + +# Disable the OAuth token healthcheck loop during tests (default: true). +# OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true + +# Silence healthcheck noise in Playwright stdout (default: true). +# OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true + +# Skip the Next.js production build before Playwright starts (CI optimization). +# OMNIROUTE_PLAYWRIGHT_SKIP_BUILD=0 + +# Skip the OmniRoute uninstall hook (used by CI to keep node_modules intact). +# OMNIROUTE_SKIP_UNINSTALL_HOOK=0 + +# Ecosystem/protocol test orchestrators wait this long (ms) for the server to +# become healthy. Default: 180000. +# ECOSYSTEM_SERVER_WAIT_MS=180000 + +# Electron smoke harness (used by scripts/smoke-electron-packaged.mjs). +# ELECTRON_SMOKE_URL=http://127.0.0.1:20128/login +# ELECTRON_SMOKE_TIMEOUT_MS=45000 +# ELECTRON_SMOKE_SETTLE_MS=2000 +# ELECTRON_SMOKE_APP_EXECUTABLE= +# ELECTRON_SMOKE_DATA_DIR= +# ELECTRON_SMOKE_KEEP_DATA=0 +# ELECTRON_SMOKE_STREAM_LOGS=0 From 46932961d46647d210421edd77d3606261531866 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 13 May 2026 10:55:33 -0300 Subject: [PATCH 3/5] refactor(env): promote hardcoded URLs and circuit-breaker timeouts to environment Centralizes a handful of hardcoded URLs, fetch timeouts, and circuit-breaker constants behind opt-in environment variables. Behavior is unchanged when the variables are unset because every call site keeps its current default. - open-sse/services/usage.ts: extracts CROF_USAGE_URL, GEMINI_CLI_USAGE_URL, CODEWHISPERER_BASE_URL constants backed by OMNIROUTE_CROF_USAGE_URL, OMNIROUTE_GEMINI_CLI_USAGE_URL, and OMNIROUTE_CODEWHISPERER_BASE_URL. Lets operators redirect quota probes through corporate mirrors or a test fixture. - open-sse/config/constants.ts: PROVIDER_PROFILES circuit-breaker thresholds and reset timeouts now honor OMNIROUTE_CIRCUIT_BREAKER_* env vars (oauth/api-key/local) with the same defaults as before. - src/shared/utils/fetchTimeout.ts: DEFAULT_TIMEOUT_MS reads OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS (fallback 120000) so deployments can raise the global fallback without changing FETCH_TIMEOUT_MS semantics. - open-sse/services/chatgptTlsClient.ts: DEFAULT_TIMEOUT_MS and HARD_TIMEOUT_GRACE_MS now honor OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS and OMNIROUTE_CHATGPT_TLS_GRACE_MS (defaults 60000 / 10000). - .env.example: documents the 11 new variables in the URLs and TIMEOUT sections. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 30 +++++++++++++++++++++++++++ open-sse/config/constants.ts | 22 ++++++++++++++------ open-sse/services/chatgptTlsClient.ts | 6 ++++-- open-sse/services/usage.ts | 14 ++++++++++--- src/shared/utils/fetchTimeout.ts | 3 ++- 5 files changed, 63 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index ab1dec0d05..d4c04f22df 100644 --- a/.env.example +++ b/.env.example @@ -263,6 +263,13 @@ NEXT_PUBLIC_CLOUD_URL= #OMNIROUTE_KIE_CALLBACK_URL= #OMNIROUTE_PUBLIC_URL= +# Upstream quota endpoints used by the Usage page. Override only for +# debugging or when routing through a corporate mirror. Used by: +# open-sse/services/usage.ts. +#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/ +#OMNIROUTE_GEMINI_CLI_USAGE_URL=https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist +#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com + # ═══════════════════════════════════════════════════════════════════════════════ # 8. OUTBOUND PROXY (Upstream Provider Calls) @@ -630,6 +637,29 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s) # FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s) +# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the +# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min). +# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000 + +# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ── +# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for +# the bogdanfinn/tls-client koffi binding and the JS-side grace window +# layered on top of it when the native library is wedged. +# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000 +# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000 + +# ── Circuit breaker thresholds and reset windows ── +# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts. +# Defaults match historical PROVIDER_PROFILES values (post-scaling for +# 500+ connections). Lower the threshold to react faster, raise it to +# tolerate more transient failures before short-circuiting. +# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD=8 +# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS=60000 +# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD=12 +# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS=30000 +# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD=2 +# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS=15000 + # ── Stream idle detection ── # STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000) # # Extended-thinking models rarely pause >90s. diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index b55085526d..21c1adfc80 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -142,13 +142,23 @@ export const RateLimitReason = { // ─── Provider Resilience Profiles ─────────────────────────────────────────── // Separate behavior for OAuth (low-limit, session-based) vs API Key (high-limit, metered) +// Circuit-breaker thresholds and reset windows are overridable via +// OMNIROUTE_CIRCUIT_BREAKER_* env vars so operators can dampen or harden +// behavior without recompiling. +function envInt(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw === null || raw === "") return fallback; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; +} + export const PROVIDER_PROFILES = { oauth: { transientCooldown: 5000, // 5s (session tokens — short recovery) rateLimitCooldown: 60000, // 60s default when no retry-after header maxBackoffLevel: 8, // Higher ceiling (sessions may stay bad longer) - circuitBreakerThreshold: 8, // Scaled for 500+ connections (was 3) - circuitBreakerReset: 60000, // 1min reset + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD", 8), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS", 60000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 10, // Scaled for 500+ connections (was 3) providerFailureWindowMs: 900000, // 15min window (was 10min) @@ -158,8 +168,8 @@ export const PROVIDER_PROFILES = { transientCooldown: 3000, // 3s (API providers recover faster) rateLimitCooldown: 0, // 0 = respect retry-after header from provider maxBackoffLevel: 5, // Lower ceiling (API quotas reset at known intervals) - circuitBreakerThreshold: 12, // Scaled for 500+ connections (was 5) - circuitBreakerReset: 30000, // 30s reset + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD", 12), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS", 30000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 15, // Scaled for 500+ connections (was 5) providerFailureWindowMs: 1800000, // 30min window (was 20min) @@ -172,8 +182,8 @@ export const PROVIDER_PROFILES = { transientCooldown: 2000, // 2s (local — very fast recovery) rateLimitCooldown: 5000, // 5s (local — no real rate limits) maxBackoffLevel: 3, // Low ceiling (local either works or doesn't) - circuitBreakerThreshold: 2, // Opens fast (if local is down, it's down) - circuitBreakerReset: 15000, // 15s reset (check again quickly) + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD", 2), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS", 15000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 2, // 2 failures trigger provider cooldown providerFailureWindowMs: 300000, // 5min window for counting failures diff --git a/open-sse/services/chatgptTlsClient.ts b/open-sse/services/chatgptTlsClient.ts index 4e21f18da4..b5c3fdbf66 100644 --- a/open-sse/services/chatgptTlsClient.ts +++ b/open-sse/services/chatgptTlsClient.ts @@ -21,14 +21,16 @@ let clientPromise: Promise | null = null; let exitHookInstalled = false; const CHATGPT_PROFILE = "firefox_148"; // matches the Firefox 148 UA we send -const DEFAULT_TIMEOUT_MS = 60_000; +const DEFAULT_TIMEOUT_MS = + Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS || "", 10) || 60_000; // Grace period added to the binding's wire-level timeout before our JS-level // hard timeout fires. Under healthy operation `tls-client-node` honors // `timeoutMilliseconds` and rejects on its own; the JS-level race only wins // when the koffi-loaded native library is wedged (which the binding's own // timer can't escape). Keep the grace small so users don't wait noticeably // longer than the configured timeout when the binding is dead. -const HARD_TIMEOUT_GRACE_MS = 10_000; +const HARD_TIMEOUT_GRACE_MS = + Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_GRACE_MS || "", 10) || 10_000; function installExitHook(): void { if (exitHookInstalled) return; diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 007f77f341..21d85889f6 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -3,6 +3,14 @@ */ import { PROVIDERS } from "../config/constants.ts"; + +// Quota / usage upstream URLs (overridable for testing or relays). +const CROF_USAGE_URL = process.env.OMNIROUTE_CROF_USAGE_URL ?? "https://crof.ai/usage_api/"; +const GEMINI_CLI_USAGE_URL = + process.env.OMNIROUTE_GEMINI_CLI_USAGE_URL ?? + "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"; +const CODEWHISPERER_BASE_URL = + process.env.OMNIROUTE_CODEWHISPERER_BASE_URL ?? "https://codewhisperer.us-east-1.amazonaws.com"; import { getAntigravityFetchAvailableModelsUrls, ANTIGRAVITY_BASE_URLS, @@ -482,7 +490,7 @@ async function getCrofUsage(apiKey: string) { let response: Response; try { - response = await fetch("https://crof.ai/usage_api/", { + response = await fetch(CROF_USAGE_URL, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, @@ -1400,7 +1408,7 @@ async function getGeminiCliSubscriptionInfoCached(accessToken: string): Promise< */ async function getGeminiCliSubscriptionInfo(accessToken: string): Promise { try { - const response = await fetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", { + const response = await fetch(GEMINI_CLI_USAGE_URL, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, @@ -2212,7 +2220,7 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec resourceType: "AGENTIC_REQUEST", }; - const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com", { + const response = await fetch(CODEWHISPERER_BASE_URL, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, diff --git a/src/shared/utils/fetchTimeout.ts b/src/shared/utils/fetchTimeout.ts index cfc36205ff..55efe599ac 100644 --- a/src/shared/utils/fetchTimeout.ts +++ b/src/shared/utils/fetchTimeout.ts @@ -7,7 +7,8 @@ * @module shared/utils/fetchTimeout */ -const DEFAULT_TIMEOUT_MS = 120000; // 2 minutes +const DEFAULT_TIMEOUT_MS = + parseInt(process.env.OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS || "", 10) || 120000; // 2 minutes const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "", 10) || DEFAULT_TIMEOUT_MS; interface FetchTimeoutOptions extends RequestInit { From 25f794affab9bc94f7fec32072632815588e343f Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 13 May 2026 11:21:10 -0300 Subject: [PATCH 4/5] docs(env): align ENVIRONMENT.md with .env.example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings docs/ENVIRONMENT.md in line with the cleaned-up .env.example. Every variable present in .env.example now has a row in ENVIRONMENT.md, and the formatting convention used by the doc tables matches the regex used by scripts/check-env-doc-sync.mjs. Notable additions, per section: - §3 Network & Ports — HOST, HOSTNAME bind overrides. - §6 Tool & Routing Policies — OMNIROUTE_PAYLOAD_RULES_PATH, OMNIROUTE_PAYLOAD_RULES_RELOAD_MS. - §7 URLs & Cloud Sync — KIE_CALLBACK_URL, OMNIROUTE_KIE_CALLBACK_URL, OMNIROUTE_PUBLIC_URL, plus the three new quota endpoint overrides (OMNIROUTE_CROF_USAGE_URL, OMNIROUTE_GEMINI_CLI_USAGE_URL, OMNIROUTE_CODEWHISPERER_BASE_URL). - §9 CLI Tool Integration — CLI_QWEN_BIN. - §10 Internal Agent & MCP — MCP description compression toggles, background-task and healthcheck overrides, RTK trust flag, Redis auth cache toggle, ANTIGRAVITY_CREDITS. - §11 OAuth — GitLab legacy fallback variables (`GITLAB_BASE_URL`, `GITLAB_OAUTH_CLIENT_ID`, `GITLAB_OAUTH_CLIENT_SECRET`). - §13 CLI Fingerprint — Kimi identity overrides + CLI_COMPAT_* table reshaped so the variable names appear in their own backticks (matches the env-doc sync regex). Removed the orphaned CLI_COMPAT_KIRO row. - §14 API Key Providers — pruned the stub rows for providers whose static *_API_KEY is no longer consumed at runtime. Added an audit note pointing to the bottom of the doc. - §15 Timeout Settings — OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS, OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS/GRACE_MS, and the OMNIROUTE_CIRCUIT_BREAKER_* table. - §16 Logging — APP_LOG_ROTATION_CHECK_INTERVAL_MS and the CHAT_LOG_* / CHAT_DEBUG_FILE knobs. - §21 Proxy Health — RATE_LIMIT_AUTO_ENABLE force-on/off documentation, HEALTHCHECK_STAGGER_MS. - §22 Debugging — Cursor executor toggles (CURSOR_DEBUG / CURSOR_STREAM_DEBUG / CURSOR_DUMP_FILE / CURSOR_STREAM_TIMEOUT_MS / CURSOR_STATE_DB_PATH / CURSOR_TOKEN), OMNIROUTE_LOG_REQUEST_SHAPE. Removed CURSOR_PROTOBUF_DEBUG (orphan). - §23 GitHub — generic GITHUB_TOKEN fallback. - New §25 — Provider quotas, tunnels, backups & misc runtime, covering Alibaba Bailian overrides, model alias compatibility, context reserve, MITM debug proxy, the 1Proxy egress pool, Tailscale binaries, ngrok, DB backups, and OMNIROUTE_TLS_PROXY_URL. Also documents REDIS_URL, which previously lived only in .env.example. - New §26 — Test & E2E harness: OMNIROUTE_E2E_BOOTSTRAP_MODE, OMNIROUTE_E2E_PASSWORD, healthcheck disablers, Playwright skip-build, uninstall-hook skip, ecosystem wait timeout, all ELECTRON_SMOKE_* variables, CLI_DEVIN_BIN. The Audit section is updated with the v3.8.0 removals (provider API key stubs, CURSOR_PROTOBUF_DEBUG, CLI_COMPAT_KIRO, QIANFAN_API_KEY) and a prominent note at the top of the doc explains the sync contract. .env.example: documents OUTBOUND_SSRF_GUARD_ENABLED (legacy SSRF guard flag actually consumed by src/shared/network/outboundUrlGuard.ts) and CODEX_CLIENT_VERSION override. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 8 ++ docs/ENVIRONMENT.md | 333 +++++++++++++++++++++++++++++--------------- 2 files changed, 232 insertions(+), 109 deletions(-) diff --git a/.env.example b/.env.example index d4c04f22df..cc6d0e05fa 100644 --- a/.env.example +++ b/.env.example @@ -153,6 +153,10 @@ ALLOW_API_KEY_REVEAL=false # Default: false (blocked) | Set true to enable local providers. # OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true +# Legacy alias toggling the SSRF guard. Used by: src/shared/network/outboundUrlGuard.ts +# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable. +# OUTBOUND_SSRF_GUARD_ENABLED=true + # ═══════════════════════════════════════════════════════════════════════════════ # 5. INPUT SANITIZATION & PII PROTECTION (FASE-01) @@ -563,6 +567,10 @@ QWEN_USER_AGENT="QwenCode/0.15.9 (linux; x64)" CURSOR_USER_AGENT="Cursor/3.3" GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" +# Override Codex client version sent in headers independently of the +# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts. +# CODEX_CLIENT_VERSION=0.130.0 + # ═══════════════════════════════════════════════════════════════════════════════ # 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 0087bb2ee0..11bd9f88b9 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -3,6 +3,12 @@ > Complete reference for every environment variable recognized by OmniRoute. > For a quick-start template, see [`.env.example`](../.env.example). +> [!IMPORTANT] +> Every variable documented here must also appear in `.env.example`, and +> every variable in `.env.example` must appear here. `npm run check:env-doc-sync` +> enforces this on commit and in CI. To omit a variable on purpose, add it to +> the allowlist inside `scripts/check-env-doc-sync.mjs`. + --- ## Table of Contents @@ -93,17 +99,19 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari ## 3. Network & Ports -| Variable | Default | Source File | Description | -| ------------------------- | ------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | -| `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. | -| `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. | -| `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. | -| `PROD_DASHBOARD_PORT` | `20130` | `docker-compose.prod.yml` | Host-side published port for the Dashboard in Docker production mode. | -| `PROD_API_PORT` | `20131` | `docker-compose.prod.yml` | Host-side published port for the API in Docker production mode. | -| `OMNIROUTE_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | Takes precedence over `PORT` when running inside Electron or other wrappers. | -| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. | -| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. | +| Variable | Default | Source File | Description | +| ------------------------- | ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | +| `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. | +| `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. | +| `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. | +| `PROD_DASHBOARD_PORT` | `20130` | `docker-compose.prod.yml` | Host-side published port for the Dashboard in Docker production mode. | +| `PROD_API_PORT` | `20131` | `docker-compose.prod.yml` | Host-side published port for the API in Docker production mode. | +| `OMNIROUTE_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | Takes precedence over `PORT` when running inside Electron or other wrappers. | +| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. | +| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. | +| `HOST` | `0.0.0.0` | `scripts/run-next.mjs` | Bind address for the Next.js dev/start server. Overrides the default `0.0.0.0` when set. | +| `HOSTNAME` | `127.0.0.1` | `scripts/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. | ### Port Modes @@ -190,25 +198,33 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp ## 6. Tool & Routing Policies -| Variable | Default | Source File | Description | -| ------------------ | ---------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| `TOOL_POLICY_MODE` | `disabled` | `src/lib/toolPolicy.ts` | Controls LLM tool/function-calling access. `allowlist` = only listed tools, `denylist` = all except listed, `disabled` = no restrictions. | +| Variable | Default | Source File | Description | +| ----------------------------------- | ---------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `TOOL_POLICY_MODE` | `disabled` | `src/lib/toolPolicy.ts` | Controls LLM tool/function-calling access. `allowlist` = only listed tools, `denylist` = all except listed, `disabled` = no restrictions. | +| `OMNIROUTE_PAYLOAD_RULES_PATH` | `./config/payloadRules.json` | `open-sse/services/payloadRules.ts` | Path to payload manipulation rules JSON file (per-model/protocol upstream tweaks). | +| `OMNIROUTE_PAYLOAD_RULES_RELOAD_MS` | `5000` | `open-sse/services/payloadRules.ts` | Reload interval (ms) for hot-reloading the payload rules file. Minimum `1000`. | --- ## 7. URLs & Cloud Sync -| Variable | Default | Source File | Description | -| --------------------------------------- | ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | Server-side URL for internal sync jobs to call `/api/sync/cloud`. | -| `CLOUD_URL` | _(empty)_ | `src/lib/cloudSync.ts` | Cloud relay endpoint URL (premium feature). | -| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | HTTP timeout for cloud sync requests. | -| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links. **Must match your public URL behind reverse proxy.** | -| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. | -| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. | -| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | `open-sse/executors/chatgpt-web.ts` | Browser-facing OmniRoute origin used for image URLs in API responses (e.g., `/v1/chatgpt-web/image/`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. | -| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. | -| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. | +| Variable | Default | Source File | Description | +| --------------------------------------- | --------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | Server-side URL for internal sync jobs to call `/api/sync/cloud`. | +| `CLOUD_URL` | _(empty)_ | `src/lib/cloudSync.ts` | Cloud relay endpoint URL (premium feature). | +| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | HTTP timeout for cloud sync requests. | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links. **Must match your public URL behind reverse proxy.** | +| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. | +| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. | +| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | `open-sse/executors/chatgpt-web.ts` | Browser-facing OmniRoute origin used for image URLs in API responses (e.g., `/v1/chatgpt-web/image/`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. | +| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. | +| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. | +| `KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public callback URL for asynchronous kie.ai jobs. Highest-priority override before `OMNIROUTE_KIE_CALLBACK_URL` and `OMNIROUTE_PUBLIC_URL`. | +| `OMNIROUTE_KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Alternate spelling of `KIE_CALLBACK_URL`. Falls back when the primary variable is unset. | +| `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. | +| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | CrofAI quota lookup endpoint used by the Usage page. Override for relays / test fixtures. | +| `OMNIROUTE_GEMINI_CLI_USAGE_URL` | `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | `open-sse/services/usage.ts` | Gemini CLI quota lookup endpoint. Override for relays / test fixtures. | +| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) usage limits endpoint. Override for relays / test fixtures. | > [!IMPORTANT] > When deploying behind a reverse proxy (nginx, Caddy), `NEXT_PUBLIC_BASE_URL` **must** be set to your public URL (e.g., `https://omniroute.example.com`). Without this, OAuth callbacks will fail because the redirect_uri won't match. @@ -257,6 +273,7 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `CLI_CLINE_BIN` | `cline` | `src/shared/services/cliRuntime.ts` | Custom path to Cline CLI binary. | | `CLI_CONTINUE_BIN` | `cn` | `src/shared/services/cliRuntime.ts` | Custom path to Continue CLI binary. | | `CLI_QODER_BIN` | `qoder` | `src/shared/services/cliRuntime.ts` | Custom path to Qoder CLI binary. | +| `CLI_QWEN_BIN` | `qwen` | `src/shared/services/cliRuntime.ts` | Custom path to the Qwen Code CLI binary. | | `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. | ### Docker Example @@ -273,19 +290,28 @@ CLI_CLAUDE_BIN=/host-cli/bin/claude ## 10. Internal Agent & MCP Integrations -| Variable | Default | Source File | Description | -| --------------------------------------- | ----------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `OMNIROUTE_BASE_URL` | auto-detect | `open-sse/mcp-server/server.ts` | Explicit URL for MCP/A2A tools to reach OmniRoute. Overrides localhost auto-detection. | -| `OMNIROUTE_API_KEY` | _(unset)_ | MCP/A2A modules | API key for internal MCP tool and A2A skill calls. | -| `OMNIROUTE_API_KEY_ID` | _(unset)_ | `open-sse/mcp-server/audit.ts` | Key ID for MCP audit log attribution. | -| `ROUTER_API_KEY` | _(unset)_ | Legacy | Legacy alias for `OMNIROUTE_API_KEY`. | -| `OMNIROUTE_MCP_ENFORCE_SCOPES` | `false` | `open-sse/mcp-server/server.ts` | Enforce scope-based access control on MCP tool calls. | -| `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. | -| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. | -| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/server-init.ts` | Provider rate-limit and quota polling interval. | -| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | `false` | `src/instrumentation-node.ts` | Disable all background services (sync, pricing, model refresh). Useful for CI/test. | -| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. | -| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. | +| Variable | Default | Source File | Description | +| ----------------------------------------------- | ----------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `OMNIROUTE_BASE_URL` | auto-detect | `open-sse/mcp-server/server.ts` | Explicit URL for MCP/A2A tools to reach OmniRoute. Overrides localhost auto-detection. | +| `OMNIROUTE_API_KEY` | _(unset)_ | MCP/A2A modules | API key for internal MCP tool and A2A skill calls. | +| `OMNIROUTE_API_KEY_ID` | _(unset)_ | `open-sse/mcp-server/audit.ts` | Key ID for MCP audit log attribution. | +| `ROUTER_API_KEY` | _(unset)_ | Legacy | Legacy alias for `OMNIROUTE_API_KEY`. | +| `OMNIROUTE_MCP_ENFORCE_SCOPES` | `false` | `open-sse/mcp-server/server.ts` | Enforce scope-based access control on MCP tool calls. | +| `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. | +| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | enabled | `open-sse/mcp-server/descriptionCompressor.ts` | Compress MCP tool descriptions before serializing the manifest. Disable values: `0`, `false`, `off`. | +| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. | +| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. | +| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/server-init.ts` | Provider rate-limit and quota polling interval. | +| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | `false` | `src/instrumentation-node.ts` | Disable all background services (sync, pricing, model refresh). Useful for CI/test. | +| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | _(unset)_ | `src/lib/config/runtimeSettings.ts` | Force background tasks on under automated test detection. Set `1` to override the test heuristic. | +| `OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS` | `600000` | `src/lib/jobs/budgetResetJob.ts` | Budget reset check cadence (ms). Floor `10000`. | +| `OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS` | `1800000` | `src/lib/jobs/reasoningCacheCleanupJob.ts` | Reasoning cache cleanup cadence (ms). Floor `60000`. | +| `OMNIROUTE_CONFIG_HOT_RELOAD_MS` | `5000` | `src/lib/config/hotReload.ts` | Polling interval (ms) for config hot-reload. Lower than `1000` is rejected. | +| `OMNIROUTE_DISABLE_REDIS_AUTH_CACHE` | _(enabled)_ | `src/lib/db/apiKeys.ts` | Set `1` to bypass the Redis-backed API-key auth cache (forces DB reads). | +| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | `0` | `open-sse/services/compression/engines/rtk/filterLoader.ts` | Trust user-managed RTK project filter rules without strict signature checks. | +| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. | +| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. | +| `ANTIGRAVITY_CREDITS` | _(unset)_ | `open-sse/services/antigravityCredits.ts` | Override Antigravity's advertised remaining credits (testing / forced values). | ### OAuth CLI Bridge (Internal) @@ -324,6 +350,9 @@ Built-in credentials for **localhost development**. For remote deployments, regi | `GITLAB_DUO_OAUTH_CLIENT_ID` | GitLab Duo (v3.8) | OAuth client ID for GitLab Duo. Register an app at `https://gitlab.com/-/profile/applications` with redirect URI `/callback` and scopes `api, read_user, openid, profile, email`. Falls back to `GITLAB_OAUTH_CLIENT_ID`. | | `GITLAB_DUO_OAUTH_CLIENT_SECRET` | GitLab Duo (v3.8) | OAuth client secret for GitLab Duo. Optional — PKCE flow does not require a secret. Falls back to `GITLAB_OAUTH_CLIENT_SECRET`. | | `GITLAB_DUO_BASE_URL` | GitLab Duo (v3.8) | Override GitLab base URL (self-hosted GitLab). Defaults to `https://gitlab.com`. Falls back to `GITLAB_BASE_URL`. | +| `GITLAB_BASE_URL` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_BASE_URL`. Used when the `_DUO_` variant is unset. | +| `GITLAB_OAUTH_CLIENT_ID` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_OAUTH_CLIENT_ID` consumed by `src/lib/oauth/constants/oauth.ts`. | +| `GITLAB_OAUTH_CLIENT_SECRET` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_OAUTH_CLIENT_SECRET` consumed by `src/lib/oauth/constants/oauth.ts`. | | `QODER_OAUTH_CLIENT_SECRET` | Qoder | — | | `QODER_OAUTH_AUTHORIZE_URL` | Qoder | Set to enable Qoder OAuth. | | `QODER_OAUTH_TOKEN_URL` | Qoder | — | @@ -379,24 +408,30 @@ When enabled, OmniRoute reorders HTTP headers and JSON body fields to match the ### Per-Provider -| Variable | Effect | -| -------------------------- | --------------------------------------- | -| `CLI_COMPAT_CODEX=1` | Mimics Codex CLI request signature | -| `CLI_COMPAT_CLAUDE=1` | Mimics Claude Code request signature | -| `CLI_COMPAT_GITHUB=1` | Mimics GitHub Copilot request signature | -| `CLI_COMPAT_ANTIGRAVITY=1` | Mimics Antigravity request signature | -| `CLI_COMPAT_KIRO=1` | Mimics Kiro IDE request signature | -| `CLI_COMPAT_CURSOR=1` | Mimics Cursor request signature | -| `CLI_COMPAT_KIMI_CODING=1` | Mimics Kimi Coding request signature | -| `CLI_COMPAT_KILOCODE=1` | Mimics Kilo Code request signature | -| `CLI_COMPAT_CLINE=1` | Mimics Cline request signature | -| `CLI_COMPAT_QWEN=1` | Mimics Qwen Code request signature | +| Variable | Activation | Effect | +| ------------------------ | ---------- | --------------------------------------- | +| `CLI_COMPAT_CODEX` | `=1` | Mimics Codex CLI request signature | +| `CLI_COMPAT_CLAUDE` | `=1` | Mimics Claude Code request signature | +| `CLI_COMPAT_GITHUB` | `=1` | Mimics GitHub Copilot request signature | +| `CLI_COMPAT_ANTIGRAVITY` | `=1` | Mimics Antigravity request signature | +| `CLI_COMPAT_CURSOR` | `=1` | Mimics Cursor request signature | +| `CLI_COMPAT_KIMI_CODING` | `=1` | Mimics Kimi Coding request signature | +| `CLI_COMPAT_KILOCODE` | `=1` | Mimics Kilo Code request signature | +| `CLI_COMPAT_CLINE` | `=1` | Mimics Cline request signature | +| `CLI_COMPAT_QWEN` | `=1` | Mimics Qwen Code request signature | ### Global -| Variable | Effect | -| ------------------ | --------------------------------------------------------------- | -| `CLI_COMPAT_ALL=1` | Enable fingerprint compatibility for **all** providers at once. | +| Variable | Activation | Effect | +| ---------------- | ---------- | --------------------------------------------------------------- | +| `CLI_COMPAT_ALL` | `=1` | Enable fingerprint compatibility for **all** providers at once. | + +### Kimi Coding CLI identity overrides + +| Variable | Default | Source File | Description | +| ----------------------- | -------------------- | ---------------------------------------- | ------------------------------------------------------------ | +| `KIMI_CLI_VERSION` | `1.36.0` | `src/lib/oauth/providers/kimi-coding.ts` | Override the Kimi CLI version sent during OAuth/API calls. | +| `KIMI_CODING_DEVICE_ID` | _(captured default)_ | `src/lib/oauth/providers/kimi-coding.ts` | Override the captured Kimi device ID used in client headers. | > [!NOTE] > This feature works alongside the User-Agent overrides (§12). The fingerprint system handles header ordering and body field ordering, while User-Agent overrides handle the specific UA string. Both can be enabled independently. @@ -411,20 +446,13 @@ Setting via environment variables is an alternative for Docker or headless deplo Recognized pattern: `{PROVIDER_ID}_API_KEY` -| Variable | Provider | -| -------------------- | ------------------- | -| `DEEPSEEK_API_KEY` | DeepSeek | -| `GROQ_API_KEY` | Groq | -| `XAI_API_KEY` | xAI (Grok) | -| `MISTRAL_API_KEY` | Mistral AI | -| `PERPLEXITY_API_KEY` | Perplexity | -| `TOGETHER_API_KEY` | Together AI | -| `FIREWORKS_API_KEY` | Fireworks AI | -| `CEREBRAS_API_KEY` | Cerebras | -| `COHERE_API_KEY` | Cohere | -| `NVIDIA_API_KEY` | NVIDIA NIM | -| `NEBIUS_API_KEY` | Nebius (embeddings) | -| `QIANFAN_API_KEY` | Baidu Qianfan | +| Variable | Provider | +| ------------------ | ---------- | +| `DEEPSEEK_API_KEY` | DeepSeek | +| `NVIDIA_API_KEY` | NVIDIA NIM | + +> [!NOTE] +> Static `${PROVIDER}_API_KEY` entries for Groq, xAI, Mistral, Perplexity, Together AI, Fireworks, Cerebras, Cohere, Nebius, and Qianfan were removed in v3.8.0 because the runtime no longer reads them — those providers rely exclusively on Dashboard / `data/provider-credentials.json` / the encrypted DB. See the _Audit: Removed / Dead Variables_ section at the bottom of this document for the migration path. > [!TIP] > Keys set via the Dashboard are stored encrypted in SQLite and take precedence over environment variables. @@ -469,6 +497,22 @@ REQUEST_TIMEOUT_MS (global override) | `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Bridge keep-alive idle timeout. | | `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | Raw socket timeout (0 = disabled). | | `SHUTDOWN_TIMEOUT_MS` | `30000` | Grace period on SIGTERM/SIGINT before force-exit. | +| `OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS` | `120000` | Fallback used by `src/shared/utils/fetchTimeout.ts` when `FETCH_TIMEOUT_MS` is unset. | +| `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`chatgptTlsClient.ts`). | +| `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | + +### Circuit Breaker Thresholds + +Provider-level circuit breaker tuning. Defaults reflect the scaled values used since v3.6 for 500+ connections. + +| Variable | Default | Source File | Description | +| --------------------------------------------- | ------- | ------------------------------ | --------------------------------------------------------------------------- | +| `OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD` | `8` | `open-sse/config/constants.ts` | Consecutive failure threshold for OAuth providers before the breaker trips. | +| `OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS` | `60000` | `open-sse/config/constants.ts` | Reset window (ms) for OAuth provider breaker. | +| `OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD` | `12` | `open-sse/config/constants.ts` | Consecutive failure threshold for API-key providers. | +| `OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS` | `30000` | `open-sse/config/constants.ts` | Reset window (ms) for API-key provider breaker. | +| `OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD` | `2` | `open-sse/config/constants.ts` | Consecutive failure threshold for local providers (Ollama, LM Studio, ...). | +| `OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS` | `15000` | `open-sse/config/constants.ts` | Reset window (ms) for local provider breaker. | ### Scenarios @@ -484,21 +528,27 @@ REQUEST_TIMEOUT_MS (global override) The logging system writes to both stdout and rotated log files. All configuration is read by `src/lib/logEnv.ts`. -| Variable | Default | Description | -| ----------------------------------------- | -------------------------- | -------------------------------------------------------------------------------- | -| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | -| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | -| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | -| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | -| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | -| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | -| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | -| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | -| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | -| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | -| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. | -| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. | -| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | +| Variable | Default | Description | +| ----------------------------------------- | -------------------------- | --------------------------------------------------------------------------------- | +| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | +| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | +| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | +| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | +| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | +| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | +| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | +| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | +| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | +| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | +| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. | +| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. | +| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | +| `APP_LOG_ROTATION_CHECK_INTERVAL_MS` | `60000` (1 min) | How often `src/lib/logRotation.ts` re-checks the active log file size. | +| `CHAT_LOG_TEXT_LIMIT` | `65536` | Max string length retained in chat log artifacts (default 64 KB). | +| `CHAT_LOG_ARRAY_TAIL_ITEMS` | `24` | Number of array items retained from the tail when truncating chat log payloads. | +| `CHAT_LOG_MAX_DEPTH` | `6` | Max nesting depth before chat log payloads are truncated. | +| `CHAT_LOG_MAX_OBJECT_KEYS` | `80` | Max object keys retained in chat log payloads (0 = unlimited). | +| `CHAT_DEBUG_FILE` | `false` | When true, `serializeArtifactForStorage` skips size-based truncation. Debug only. | --- @@ -582,14 +632,15 @@ Anthropic-compatible provider instead. ## 21. Proxy Health -| Variable | Default | Source File | Description | -| ---------------------------- | ---------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PROXY_FAST_FAIL_TIMEOUT_MS` | `2000` | `src/lib/proxyHealth.ts` | Fast-fail health check timeout. | -| `PROXY_HEALTH_CACHE_TTL_MS` | `30000` | `src/lib/proxyHealth.ts` | Health check result cache TTL. | -| `RATE_LIMIT_MAX_WAIT_MS` | `120000` (2 min) | `open-sse/services/rateLimitManager.ts` | Max time to wait on a 429 before failing the request. | -| `RATE_LIMIT_AUTO_ENABLE` | `false` | `open-sse/services/rateLimitManager.ts` | When `true`/`1`, automatically engages the rate-limit manager on first observed upstream 429 (without requiring manual toggle). Accepts `true`/`1`/`yes`. | -| `REQUEST_RETRY` | `2` | `src/sse/services/cooldownAwareRetry.ts` | Number of automatic retries on model-scoped cooldown responses before returning error to client. | -| `MAX_RETRY_INTERVAL_SEC` | `30` | `src/sse/services/cooldownAwareRetry.ts` | Max backoff interval (seconds) between cooldown retries. Capped by this value regardless of upstream `Retry-After`. | +| Variable | Default | Source File | Description | +| ---------------------------- | ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROXY_FAST_FAIL_TIMEOUT_MS` | `2000` | `src/lib/proxyHealth.ts` | Fast-fail health check timeout. | +| `PROXY_HEALTH_CACHE_TTL_MS` | `30000` | `src/lib/proxyHealth.ts` | Health check result cache TTL. | +| `RATE_LIMIT_MAX_WAIT_MS` | `120000` (2 min) | `open-sse/services/rateLimitManager.ts` | Max time to wait on a 429 before failing the request. | +| `RATE_LIMIT_AUTO_ENABLE` | _(unset)_ | `open-sse/services/rateLimitManager.ts` | Force the auto-enable rate limit safety net on/off regardless of the persisted Dashboard setting. Accepts `true`/`1`/`on` to force on, `false`/`0`/`off` to force off. | +| `HEALTHCHECK_STAGGER_MS` | `3000` | `src/lib/tokenHealthCheck.ts` | Stagger interval (ms) between provider token healthchecks at startup. | +| `REQUEST_RETRY` | `2` | `src/sse/services/cooldownAwareRetry.ts` | Number of automatic retries on model-scoped cooldown responses before returning error to client. | +| `MAX_RETRY_INTERVAL_SEC` | `30` | `src/sse/services/cooldownAwareRetry.ts` | Max backoff interval (seconds) between cooldown retries. Capped by this value regardless of upstream `Retry-After`. | --- @@ -598,12 +649,17 @@ Anthropic-compatible provider instead. > [!CAUTION] > These variables produce **verbose output** and may leak sensitive data. **Never enable in production.** -| Variable | Default | Source File | Description | -| -------------------------------- | --------- | ----------------------------------------- | -------------------------------------------------------------- | -| `CURSOR_PROTOBUF_DEBUG` | _(unset)_ | `open-sse/utils/cursorProtobuf.ts` | Set `1` to dump Cursor protobuf decode/encode details. | -| `CURSOR_STREAM_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Set `1` to dump raw Cursor SSE stream data. | -| `DEBUG_RESPONSES_SSE_TO_JSON` | _(unset)_ | `open-sse/handlers/responseTranslator.ts` | Set `true` to log Responses API SSE→JSON translation details. | -| `NEXT_PUBLIC_OMNIROUTE_E2E_MODE` | _(unset)_ | E2E test harness | Set `true` to enable E2E test mode (relaxed auth, test hooks). | +| Variable | Default | Source File | Description | +| -------------------------------- | ------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | +| `CURSOR_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Set `1` to enable verbose Cursor executor logs (decoded SSE chunks, etc.). | +| `CURSOR_STREAM_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Backward-compatible alias of `CURSOR_DEBUG`. | +| `CURSOR_DUMP_FILE` | _(unset)_ | `open-sse/executors/cursor.ts` | Optional file path that receives raw decoded Cursor chunks when `CURSOR_DEBUG=1`. | +| `CURSOR_STREAM_TIMEOUT_MS` | `300000` | `open-sse/executors/cursor.ts` | Stream idle timeout (ms) for the Cursor executor. | +| `CURSOR_STATE_DB_PATH` | _(probed)_ | `open-sse/utils/cursorVersionDetector.ts` | Override the Cursor state DB lookup used for version detection. | +| `CURSOR_TOKEN` | _(unset)_ | `scripts/cursor-tap.cjs` | Direct Cursor bearer token used by developer tooling. | +| `OMNIROUTE_LOG_REQUEST_SHAPE` | enabled (`!== "0"`) | `src/app/api/v1/chat/completions/route.ts` | Log content-type/length markers for large chat payloads. Set `"0"` to silence. | +| `DEBUG_RESPONSES_SSE_TO_JSON` | _(unset)_ | `open-sse/handlers/responseTranslator.ts` | Set `true` to log Responses API SSE→JSON translation details. | +| `NEXT_PUBLIC_OMNIROUTE_E2E_MODE` | _(unset)_ | E2E test harness | Set `true` to enable E2E test mode (relaxed auth, test hooks). | --- @@ -611,10 +667,11 @@ Anthropic-compatible provider instead. Allow users to report issues directly from the Dashboard. -| Variable | Default | Source File | Description | -| --------------------- | --------- | --------------------------------------- | ------------------------------------------------------- | -| `GITHUB_ISSUES_REPO` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | Repository in `owner/repo` format. | -| `GITHUB_ISSUES_TOKEN` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | GitHub Personal Access Token with `issues:write` scope. | +| Variable | Default | Source File | Description | +| --------------------- | --------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `GITHUB_ISSUES_REPO` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | Repository in `owner/repo` format. | +| `GITHUB_ISSUES_TOKEN` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | GitHub Personal Access Token with `issues:write` scope. | +| `GITHUB_TOKEN` | _(unset)_ | issue triage / cloud agent helpers | Generic GitHub access token used as fallback for `GITHUB_ISSUES_TOKEN` and consumed by cloud agent helpers in `src/lib/cloudAgent/*`. | --- @@ -698,19 +755,77 @@ Limits and safety knobs applied when the Skills framework (`src/lib/skills/`) ex --- +## 25. Provider Quotas, Tunnels, Backups & Misc Runtime + +Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), the 1Proxy egress pool, database backups and small per-feature overrides referenced by the executor layer or scripts. + +| Variable | Default | Source File | Description | +| -------------------------------- | ------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | +| `REDIS_URL` | `redis://localhost:6379` | `src/shared/utils/rateLimiter.ts` | Redis connection string for the rate limiter backend. | +| `ALIBABA_CODING_PLAN_HOST` | _(production host)_ | `open-sse/services/bailianQuotaFetcher.ts` | Override the host used to fetch Alibaba Bailian coding-plan quotas. | +| `ALIBABA_CODING_PLAN_QUOTA_URL` | derived from host | `open-sse/services/bailianQuotaFetcher.ts` | Full quota URL override for Alibaba Bailian. | +| `CONTEXT_RESERVE_TOKENS` | `1024` | `open-sse/services/contextManager.ts` | Tokens reserved for completion output when computing prompt budgets. | +| `MODEL_ALIAS_COMPAT_ENABLED` | enabled | `open-sse/services/model.ts` | Toggle the legacy model-alias compatibility layer used by older clients. | +| `COMMAND_CODE_CALLBACK_PORT` | _(unset)_ | `src/app/api/providers/command-code/auth/shared.ts` | Local port used for OAuth-style callbacks from the Command Code CLI helper. | +| `MITM_LOCAL_PORT` | `443` | `src/mitm/server.cjs` | Local bind port for the MITM debug proxy. | +| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). | +| `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | Enable the 1Proxy egress pool sync. | +| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. | +| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. | +| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | `src/lib/oneproxySync.ts` | Minimum quality score for imported proxies. | +| `TAILSCALE_BIN` | _(auto-detect)_ | `src/lib/tailscaleTunnel.ts` | Explicit path to the `tailscale` binary. | +| `TAILSCALED_BIN` | _(auto-detect)_ | `src/lib/tailscaleTunnel.ts` | Explicit path to the `tailscaled` daemon binary. | +| `NGROK_AUTHTOKEN` | _(unset)_ | `src/lib/ngrokTunnel.ts` | Authenticates outbound ngrok tunnels. | +| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | Maximum SQLite backup files retained on disk. | +| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. | +| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. | + +--- + +## 26. Test & E2E Harness + +Used by `scripts/run-next-playwright.mjs`, `scripts/smoke-electron-packaged.mjs`, +`scripts/run-ecosystem-tests.mjs`, and `scripts/uninstall.mjs`. Leave every +value below unset in production deployments. + +| Variable | Default | Source File | Description | +| ------------------------------------- | -------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- | +| `OMNIROUTE_E2E_BOOTSTRAP_MODE` | `auth` | `scripts/run-next-playwright.mjs` | E2E bootstrap mode (`auth`, `fresh`, `reuse`) for the Playwright runner. | +| `OMNIROUTE_E2E_PASSWORD` | falls back to `INITIAL_PASSWORD` | `scripts/run-next-playwright.mjs` | Admin password injected into the Playwright environment. | +| `OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK` | `true` | `scripts/run-next-playwright.mjs` | Disable the local healthcheck poll during Playwright runs. | +| `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | `true` | `scripts/run-next-playwright.mjs` | Disable the OAuth token healthcheck loop during tests. | +| `OMNIROUTE_HIDE_HEALTHCHECK_LOGS` | `true` | `scripts/run-next-playwright.mjs` | Silence healthcheck noise in Playwright stdout. | +| `OMNIROUTE_PLAYWRIGHT_SKIP_BUILD` | `0` | `scripts/run-next-playwright.mjs` | Skip the Next.js production build before Playwright starts (CI optimization). | +| `OMNIROUTE_SKIP_UNINSTALL_HOOK` | `0` | `scripts/uninstall.mjs` | Skip the OmniRoute uninstall hook (used by CI to keep `node_modules` intact). | +| `ECOSYSTEM_SERVER_WAIT_MS` | `180000` | `scripts/run-ecosystem-tests.mjs` | Wait time (ms) for the server to become healthy before running ecosystem/protocol tests. | +| `ELECTRON_SMOKE_URL` | `http://127.0.0.1:20128/login` | `scripts/smoke-electron-packaged.mjs` | URL the Electron smoke harness expects the packaged app to serve. | +| `ELECTRON_SMOKE_TIMEOUT_MS` | `45000` | `scripts/smoke-electron-packaged.mjs` | Total timeout (ms) before the smoke harness gives up. | +| `ELECTRON_SMOKE_SETTLE_MS` | `2000` | `scripts/smoke-electron-packaged.mjs` | Settle window (ms) after the page loads. | +| `ELECTRON_SMOKE_APP_EXECUTABLE` | _(auto)_ | `scripts/smoke-electron-packaged.mjs` | Explicit path to the packaged Electron executable. | +| `ELECTRON_SMOKE_DATA_DIR` | _(tmpdir)_ | `scripts/smoke-electron-packaged.mjs` | Data directory for the Electron smoke run. | +| `ELECTRON_SMOKE_KEEP_DATA` | `0` | `scripts/smoke-electron-packaged.mjs` | Set `1` to preserve the smoke data directory after the run. | +| `ELECTRON_SMOKE_STREAM_LOGS` | `0` | `scripts/smoke-electron-packaged.mjs` | Set `1` to stream Electron logs to stdout during the run. | +| `CLI_DEVIN_BIN` | _(PATH lookup)_ | `open-sse/executors/devin-cli.ts` | Override the Devin CLI binary path. | + +--- + ## Audit: Removed / Dead Variables The following variables appeared in previous versions of `.env.example` but have **no runtime references** in the current codebase. They have been removed: -| Variable | Reason | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `STORAGE_DRIVER=sqlite` | Never read by any source file. SQLite is the only supported driver — no selection needed. | -| `INSTANCE_NAME=omniroute` | Present in old docs/env templates but unused at runtime. May return in a future multi-instance feature. | -| `SQLITE_MAX_SIZE_MB=2048` | Not referenced in source code. Database size is not artificially limited. | -| `SQLITE_CLEAN_LEGACY_FILES=true` | Not referenced in source code. Legacy cleanup was likely removed. | -| `CLI_ROO_BIN` | Not registered in `src/shared/services/cliRuntime.ts`. | -| `CLI_KIMI_CODING_BIN` | Not registered in `src/shared/services/cliRuntime.ts` (Kimi Coding uses OAuth, not a CLI binary). | -| `IFLOW_OAUTH_CLIENT_ID` / `IFLOW_OAUTH_CLIENT_SECRET` | Not referenced anywhere in source code. | +| Variable | Reason | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `STORAGE_DRIVER=sqlite` | Never read by any source file. SQLite is the only supported driver — no selection needed. | +| `INSTANCE_NAME=omniroute` | Present in old docs/env templates but unused at runtime. May return in a future multi-instance feature. | +| `SQLITE_MAX_SIZE_MB=2048` | Not referenced in source code. Database size is not artificially limited. | +| `SQLITE_CLEAN_LEGACY_FILES=true` | Not referenced in source code. Legacy cleanup was likely removed. | +| `CLI_ROO_BIN` | Not registered in `src/shared/services/cliRuntime.ts`. | +| `CLI_KIMI_CODING_BIN` | Not registered in `src/shared/services/cliRuntime.ts` (Kimi Coding uses OAuth, not a CLI binary). | +| `IFLOW_OAUTH_CLIENT_ID` / `IFLOW_OAUTH_CLIENT_SECRET` | Not referenced anywhere in source code. | +| `CEREBRAS_API_KEY` / `COHERE_API_KEY` / `FIREWORKS_API_KEY` / `GROQ_API_KEY` / `MISTRAL_API_KEY` / `NEBIUS_API_KEY` / `PERPLEXITY_API_KEY` / `TOGETHER_API_KEY` / `XAI_API_KEY` | Removed in v3.8.0. The runtime no longer reads these env vars — credentials come from Dashboard / `data/provider-credentials.json` / encrypted DB. | +| `CURSOR_PROTOBUF_DEBUG` | Removed in v3.8.0. Cursor executor uses `CURSOR_DEBUG` / `CURSOR_STREAM_DEBUG` (see §22). | +| `CLI_COMPAT_KIRO` | Removed in v3.8.0. Kiro is in `CLI_COMPAT_OMITTED_PROVIDER_IDS` — its toggle has no effect. | +| `QIANFAN_API_KEY` | Removed alongside other unused provider API key stubs in v3.8.0. | ### Default Value Corrections From b43ab4d4c340eb699c9388a7e47192950dd63c58 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 13 May 2026 12:11:01 -0300 Subject: [PATCH 5/5] test(env): make check-env-doc-sync strict + unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites scripts/check-env-doc-sync.mjs so the default mode is strict (non-zero exit on drift between code references, .env.example, and docs/ENVIRONMENT.md). The previous "report-only" behavior is still available via --lenient for ad-hoc local diagnostics. Highlights: - Strict mode fails when any of these three sets is non-empty: 1. process.env vars referenced in src/, open-sse/, bin/, scripts/, electron/main.js, electron/preload.js but missing from .env.example. 2. .env.example vars missing from docs/ENVIRONMENT.md. 3. docs/ENVIRONMENT.md vars missing from .env.example. - Allowlists are explicit and curated: * `IGNORE_FROM_CODE` — system vars (NODE_ENV, PATH, ...), Next.js internals, CI runner injections, doctor placeholders, and aliases handled by fallback ordering. * `DOC_ONLY_ALLOWLIST` — vars intentionally documented in ENVIRONMENT.md but absent from .env.example (Audit section, legacy aliases, future-supported hooks, `CHANGEME` default value). * `ENV_ONLY_ALLOWLIST` — reserved for future use; currently empty. - The checker now exposes a programmatic `runEnvDocSync({ envExampleText, envDocText, codeVars, ignore, docOnlyAllowlist, envOnlyAllowlist })` entry point that other Node tests can import without touching disk. Helpers `parseEnvExampleVars` and `parseEnvDocVars` are exported so fixtures can validate the regex contract. Test coverage in tests/unit/check-env-doc-sync.test.ts (13 cases): - Parses env.example assignments (commented and uncommented), rejects prose, and rejects backtick literals that aren't SHOUTY env names. - Drives runEnvDocSync against in-memory fixtures for every drift direction (code-missing-env, env-missing-doc, doc-missing-env) and asserts the allowlists / ignore set behave as expected. - Calls runEnvDocSync() with no overrides to assert the live .env.example, docs/ENVIRONMENT.md and source-code references stay in sync. This is the same check that runs in pre-commit / CI, so the unit-test failure surfaces drift before reviewers do. .env.example: documents `AWS_REGION` and `AWS_DEFAULT_REGION` so Bedrock/Kiro/audio-speech callers stay in the contract. docs/ENVIRONMENT.md: adds rows for AWS_REGION / AWS_DEFAULT_REGION inside §20 Provider-Specific Settings. Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 7 + docs/ENVIRONMENT.md | 30 +-- scripts/check-env-doc-sync.mjs | 298 +++++++++++++++++++------- tests/unit/check-env-doc-sync.test.ts | 191 +++++++++++++++++ 4 files changed, 436 insertions(+), 90 deletions(-) create mode 100644 tests/unit/check-env-doc-sync.test.ts diff --git a/.env.example b/.env.example index cc6d0e05fa..cf179cba46 100644 --- a/.env.example +++ b/.env.example @@ -840,6 +840,13 @@ APP_LOG_TO_FILE=true # NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s) # NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s) +# ── AWS Bedrock (Kiro / Audio) ── +# Region used to construct AWS Bedrock endpoints. Used by: +# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts. +# AWS_REGION takes precedence over AWS_DEFAULT_REGION when both are set. +# AWS_REGION=us-east-1 +# AWS_DEFAULT_REGION=us-east-1 + # ── Cloudflare Workers AI ── # Account ID override for Cloudflare Workers AI executor. # Used by: open-sse/executors/cloudflare-ai.ts diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 11bd9f88b9..e8b366fab5 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -608,20 +608,22 @@ Automatic model pricing data synchronization from external sources. ## 20. Provider-Specific Settings -| Variable | Default | Source File | Description | -| ----------------------------------------- | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------- | -| `OPENROUTER_CATALOG_TTL_MS` | `86400000` (24h) | `src/lib/catalog/openrouterCatalog.ts` | OpenRouter model catalog cache TTL. | -| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. | -| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. | -| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. | -| `CLOUDFLARED_BIN` | auto-detect | `src/lib/cloudflaredTunnel.ts` | Custom path to `cloudflared` binary. | -| `SEARCH_CACHE_TTL_MS` | `300000` (5 min) | `open-sse/services/searchCache.ts` | TTL for search API (Perplexity, Brave, etc.) response caching. | -| `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | `false` | `src/app/api/providers/route.ts` | Allow multiple simultaneous connections per OpenAI-compatible provider. | -| `ENABLE_CC_COMPATIBLE_PROVIDER` | `false` | `src/shared/utils/featureFlags.ts` | Reveal the experimental CC-compatible provider UI for Claude Code-only relays. | -| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). | -| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. | -| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | -| `LOCAL_HOSTNAMES` | _(empty)_ | `open-sse/config/providerRegistry.ts` | Comma-separated additional hostnames treated as "local" (Docker service names, etc.). | +| Variable | Default | Source File | Description | +| ----------------------------------------- | ------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `OPENROUTER_CATALOG_TTL_MS` | `86400000` (24h) | `src/lib/catalog/openrouterCatalog.ts` | OpenRouter model catalog cache TTL. | +| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. | +| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. | +| `AWS_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Region used to construct AWS Bedrock endpoints (Kiro, audio). | +| `AWS_DEFAULT_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Fallback when `AWS_REGION` is not set. | +| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. | +| `CLOUDFLARED_BIN` | auto-detect | `src/lib/cloudflaredTunnel.ts` | Custom path to `cloudflared` binary. | +| `SEARCH_CACHE_TTL_MS` | `300000` (5 min) | `open-sse/services/searchCache.ts` | TTL for search API (Perplexity, Brave, etc.) response caching. | +| `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | `false` | `src/app/api/providers/route.ts` | Allow multiple simultaneous connections per OpenAI-compatible provider. | +| `ENABLE_CC_COMPATIBLE_PROVIDER` | `false` | `src/shared/utils/featureFlags.ts` | Reveal the experimental CC-compatible provider UI for Claude Code-only relays. | +| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). | +| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. | +| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | +| `LOCAL_HOSTNAMES` | _(empty)_ | `open-sse/config/providerRegistry.ts` | Comma-separated additional hostnames treated as "local" (Docker service names, etc.). | `ENABLE_CC_COMPATIBLE_PROVIDER` is only for third-party relays that accept Claude Code clients exclusively. OmniRoute rewrites requests so those relays accept them. If you only want to use diff --git a/scripts/check-env-doc-sync.mjs b/scripts/check-env-doc-sync.mjs index 8f03665604..f4e61d841b 100644 --- a/scripts/check-env-doc-sync.mjs +++ b/scripts/check-env-doc-sync.mjs @@ -1,10 +1,27 @@ #!/usr/bin/env node -// Validates that env vars referenced in code appear in .env.example AND in docs/ENVIRONMENT.md. -// Exits 0 on success, 1 on missing entries. Designed for use in pre-commit / CI. -// -// Run: node scripts/check-env-doc-sync.mjs -// Strict mode: node scripts/check-env-doc-sync.mjs --strict -// In strict mode, missing entries cause failure. In default mode, only summary is printed. +/** + * Strict environment variable contract checker. + * + * Enforces that every env var referenced in OmniRoute source code appears in + * both `.env.example` and `docs/ENVIRONMENT.md`, and that the two files agree + * on the documented var set. Falls back to a small allowlist for variables + * that are intentionally documented but not literally referenced (legacy + * aliases, future-supported hooks) or vice versa. + * + * Usage: + * node scripts/check-env-doc-sync.mjs # strict (CI mode) + * node scripts/check-env-doc-sync.mjs --lenient # legacy report-only mode + * + * Strict mode exits non-zero if any of these are non-empty: + * - vars in code but missing from .env.example + * - vars in .env.example but missing from ENVIRONMENT.md + * - vars in ENVIRONMENT.md but missing from .env.example + * + * Programmatic API: + * Other Node tests can `import { runEnvDocSync } from "./check-env-doc-sync.mjs"` + * and pass `{ root, envExample, envDoc, codeVars, ignore, docOnlyAllowlist, + * envOnlyAllowlist }` to drive the checker against fixtures. + */ import fs from "node:fs"; import path from "node:path"; @@ -12,14 +29,12 @@ import { fileURLToPath } from "node:url"; import { execSync } from "node:child_process"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const ROOT = path.resolve(__dirname, ".."); -const ENV_EXAMPLE = path.join(ROOT, ".env.example"); -const ENV_DOC = path.join(ROOT, "docs", "ENVIRONMENT.md"); +const REPO_ROOT = path.resolve(__dirname, ".."); -const STRICT = process.argv.includes("--strict"); - -// Vars that are intentionally not documented or detected in code via dynamic patterns. -const IGNORE = new Set([ +// ─── Allowlists ──────────────────────────────────────────────────────────── +// Env vars referenced in code that should NOT trigger documentation drift. +// These are usually system/process vars or harness-only knobs. +const IGNORE_FROM_CODE = new Set([ "NODE_ENV", "PATH", "HOME", @@ -33,99 +48,230 @@ const IGNORE = new Set([ "CI", "GITHUB_ACTIONS", "RUNNER_OS", - // Add false positives here as discovered. + // OS / Node internals frequently surfaced by indirect dependencies. + "APPDATA", + "LOCALAPPDATA", + "XDG_CONFIG_HOME", + "USERPROFILE", + "PREFIX", + // Next.js / Node test runners — these are framework-managed. + "NEXT_DIST_DIR", + "NEXT_PHASE", + "NEXT_RUNTIME", + "VITEST", + // CI providers (set by the runner). + "GITHUB_BASE_REF", + "GITHUB_BASE_SHA", + // Aliases for documented vars handled via fallback ordering. + "API_KEY", + "APP_URL", + "PUBLIC_URL", + "ANTHROPIC_API_URL", + "OPENAI_API_URL", + "LOG_LEVEL", + // Internal QA helpers used only by scripts/ and Playwright. + "QA_BASE_URL", + "QA_LOCALES", + "QA_REPORT_SUFFIX", + "QA_ROUTES", + // Doctor diagnostic flags (no runtime behavior yet — placeholders). + "OMNIROUTE_DOCTOR_HOST", + "OMNIROUTE_DOCTOR_LIVENESS_URL", + "OMNIROUTE_PROVIDER_CATALOG_PATH", + "OMNIROUTE_PROVIDER_TEST_MODEL", + // Source typo / placeholder. + "OMNIROUT", + // Static config alias path (the canonical var is OMNIROUTE_PAYLOAD_RULES_PATH). + "PAYLOAD_RULES_PATH", ]); -function readEnvExampleVars() { - if (!fs.existsSync(ENV_EXAMPLE)) { - console.error(`✗ ${ENV_EXAMPLE} not found`); - process.exit(2); - } - const txt = fs.readFileSync(ENV_EXAMPLE, "utf8"); +// Vars documented in ENVIRONMENT.md but intentionally absent from .env.example. +// Used for past-tense documentation (Audit / Dead vars section), legacy aliases +// with no runtime hook, and section anchors that look like vars to the regex. +const DOC_ONLY_ALLOWLIST = new Set([ + // Audit history (Removed / Dead Variables section). + "CEREBRAS_API_KEY", + "COHERE_API_KEY", + "FIREWORKS_API_KEY", + "GROQ_API_KEY", + "MISTRAL_API_KEY", + "NEBIUS_API_KEY", + "PERPLEXITY_API_KEY", + "TOGETHER_API_KEY", + "XAI_API_KEY", + "QIANFAN_API_KEY", + "CURSOR_PROTOBUF_DEBUG", + "CLI_COMPAT_KIRO", + "CLI_KIMI_CODING_BIN", + "CLI_ROO_BIN", + "IFLOW_OAUTH_CLIENT_ID", + "IFLOW_OAUTH_CLIENT_SECRET", + // Source-code constants accidentally captured by the doc regex. + "CLI_COMPAT_OMITTED_PROVIDER_IDS", + // Sample default values that look like SHOUTY_NAMES (not env vars). + "CHANGEME", + // Legacy aliases — present in docs as "would be aliases" but read-only + // through their canonical names today. + "OMNIROUTE_CRYPT_KEY", + "OMNIROUTE_API_KEY_BASE64", + // Future-supported hooks: documented but currently hardcoded constants. + "MAX_RETRY_INTERVAL_SEC", + "REQUEST_RETRY", + "SKILLS_EXECUTION_TIMEOUT_MS", + "SKILLS_SANDBOX_DOCKER_IMAGE", +]); + +// Vars present in .env.example but intentionally absent from ENVIRONMENT.md. +// Empty today — kept for forward compatibility / explicit exemption. +const ENV_ONLY_ALLOWLIST = new Set([]); + +// ─── Parsing helpers ─────────────────────────────────────────────────────── + +/** + * Extract VAR= entries from a `.env`-style file (handles commented examples). + */ +export function parseEnvExampleVars(text) { const vars = new Set(); - for (const line of txt.split("\n")) { - // Match both "VAR=value" and "# VAR=value" (commented-out examples are still documented) + for (const line of String(text ?? "").split("\n")) { const m = line.match(/^#?\s*([A-Z][A-Z0-9_]+)\s*=/); if (m) vars.add(m[1]); } return vars; } -function readEnvDocVars() { - if (!fs.existsSync(ENV_DOC)) { - console.error(`✗ ${ENV_DOC} not found`); - process.exit(2); - } - const txt = fs.readFileSync(ENV_DOC, "utf8"); +/** + * Extract `VARNAME` tokens from a markdown doc — matches anything in backticks + * that looks like an env var (uppercase + digit + underscore). + */ +export function parseEnvDocVars(text) { const vars = new Set(); - // Match `VAR_NAME` in inline code or table cells. - for (const m of txt.matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) { + for (const m of String(text ?? "").matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) { vars.add(m[1]); } return vars; } -function readCodeVars() { +/** + * Collect environment variable references in source code via grep against + * the `process.env` member access pattern. + */ +function scanCodeVars({ cwd } = {}) { + const repoRoot = cwd ?? REPO_ROOT; + const stdout = execSync( + "grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' " + + "src/ open-sse/ bin/ scripts/ electron/main.js electron/preload.js 2>/dev/null || true", + { cwd: repoRoot, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 } + ); const vars = new Set(); - let stdout; - try { - stdout = execSync( - "grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' src/ open-sse/ bin/ scripts/ 2>/dev/null || true", - { cwd: ROOT, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 } - ); - } catch (e) { - console.error(`✗ grep failed: ${e.message}`); - process.exit(2); - } for (const line of stdout.split("\n")) { const m = line.match(/^process\.env\.([A-Z][A-Z0-9_]+)$/); - if (m && !IGNORE.has(m[1])) vars.add(m[1]); + if (m) vars.add(m[1]); } return vars; } +/** + * Diff helper. + */ function diff(set, against) { return [...set].filter((v) => !against.has(v)).sort(); } -function main() { - const codeVars = readCodeVars(); - const exampleVars = readEnvExampleVars(); - const docVars = readEnvDocVars(); +// ─── Programmatic entry point ────────────────────────────────────────────── - const inCodeMissingExample = diff(codeVars, exampleVars); - const inCodeMissingDoc = diff(codeVars, docVars); - const inExampleMissingDoc = diff(exampleVars, docVars); - const inExampleMissingCode = diff(exampleVars, codeVars); +/** + * Run the contract checker. All inputs are overridable for tests. + * + * Returns `{ ok: boolean, summary, problems: { codeMissingEnv, envMissingDoc, + * docMissingEnv } }`. + */ +export function runEnvDocSync(options = {}) { + const ignore = options.ignore ?? IGNORE_FROM_CODE; + const docOnly = options.docOnlyAllowlist ?? DOC_ONLY_ALLOWLIST; + const envOnly = options.envOnlyAllowlist ?? ENV_ONLY_ALLOWLIST; - console.log("Env var sync report"); - console.log("==================="); - console.log(`Code references: ${codeVars.size} unique vars`); - console.log(`In .env.example: ${exampleVars.size} unique vars`); - console.log(`In docs/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`); - console.log(); + const envExampleText = + options.envExampleText ?? + (options.envExamplePath + ? fs.readFileSync(options.envExamplePath, "utf8") + : fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8")); + const envDocText = + options.envDocText ?? + (options.envDocPath + ? fs.readFileSync(options.envDocPath, "utf8") + : fs.readFileSync(path.join(REPO_ROOT, "docs", "ENVIRONMENT.md"), "utf8")); - function printList(label, list) { - if (list.length === 0) { - console.log(` ✓ ${label}: none`); - } else { - console.log(` ⚠ ${label}: ${list.length}`); - for (const v of list.slice(0, 30)) console.log(` - ${v}`); - if (list.length > 30) console.log(` ... and ${list.length - 30} more`); - } - } + const envVars = parseEnvExampleVars(envExampleText); + const docVars = parseEnvDocVars(envDocText); - printList("In code but missing from .env.example", inCodeMissingExample); - printList("In code but missing from ENVIRONMENT.md", inCodeMissingDoc); - printList("In .env.example but missing from ENVIRONMENT.md", inExampleMissingDoc); - printList("In .env.example but not referenced in code (dead?)", inExampleMissingCode); + const codeVars = new Set( + [...(options.codeVars ?? scanCodeVars({ cwd: options.root }))].filter((v) => !ignore.has(v)) + ); - const errors = inCodeMissingExample.length + inExampleMissingDoc.length; - if (STRICT && errors > 0) { - console.error(`\n✗ ${errors} drift(s) detected (strict mode)`); - process.exit(1); - } - console.log(`\n${errors === 0 ? "✓" : "⚠"} Done.`); + const codeMissingEnv = diff(codeVars, envVars); + const envMissingDoc = diff(envVars, docVars).filter((v) => !envOnly.has(v)); + const docMissingEnv = diff(docVars, envVars).filter((v) => !docOnly.has(v)); + + const ok = + codeMissingEnv.length === 0 && envMissingDoc.length === 0 && docMissingEnv.length === 0; + + return { + ok, + summary: { + code: codeVars.size, + envExample: envVars.size, + doc: docVars.size, + }, + problems: { + codeMissingEnv, + envMissingDoc, + docMissingEnv, + }, + }; } -main(); +// ─── CLI ─────────────────────────────────────────────────────────────────── + +function printList(label, list, marker) { + if (list.length === 0) { + console.log(` ${marker || "✓"} ${label}: none`); + return; + } + console.log(` ✗ ${label}: ${list.length}`); + for (const v of list.slice(0, 50)) console.log(` - ${v}`); + if (list.length > 50) console.log(` ... and ${list.length - 50} more`); +} + +function main() { + const lenient = process.argv.includes("--lenient"); + const result = runEnvDocSync(); + + console.log("Env var contract sync report"); + console.log("============================"); + console.log(`Code references: ${result.summary.code} unique vars`); + console.log(`In .env.example: ${result.summary.envExample} unique vars`); + console.log(`In docs/ENVIRONMENT.md: ${result.summary.doc} unique vars`); + console.log(); + + printList("In code but missing from .env.example", result.problems.codeMissingEnv); + printList("In .env.example but missing from ENVIRONMENT.md", result.problems.envMissingDoc); + printList("In ENVIRONMENT.md but missing from .env.example", result.problems.docMissingEnv); + + if (result.ok) { + console.log("\n✓ Env / docs contract is in sync."); + process.exit(0); + } + + if (lenient) { + console.log("\n⚠ Drift detected (lenient mode — exit 0)."); + process.exit(0); + } + + console.log("\n✗ Env / docs contract is out of sync. Update .env.example, docs/ENVIRONMENT.md,"); + console.log(" or the allowlists in scripts/check-env-doc-sync.mjs and try again."); + process.exit(1); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/tests/unit/check-env-doc-sync.test.ts b/tests/unit/check-env-doc-sync.test.ts new file mode 100644 index 0000000000..3fc4ba637f --- /dev/null +++ b/tests/unit/check-env-doc-sync.test.ts @@ -0,0 +1,191 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + parseEnvExampleVars, + parseEnvDocVars, + runEnvDocSync, +} from "../../scripts/check-env-doc-sync.mjs"; + +test("parseEnvExampleVars: extracts uncommented assignments", () => { + const text = ` +JWT_SECRET=abc123 +FOO_BAR=baz + # SKIPPED_VAR=leading whitespace before hash is rejected +`; + const vars = parseEnvExampleVars(text); + assert.ok(vars.has("JWT_SECRET")); + assert.ok(vars.has("FOO_BAR")); + // The regex anchors `^#?` so leading whitespace before `#` prevents a match. + assert.equal(vars.has("SKIPPED_VAR"), false); +}); + +test("parseEnvExampleVars: extracts commented examples", () => { + const text = ` +#OPTIONAL_KEY=value +# OPTIONAL_OTHER=value-with-space +ACTIVE=1 +`; + const vars = parseEnvExampleVars(text); + assert.ok(vars.has("OPTIONAL_KEY")); + assert.ok(vars.has("OPTIONAL_OTHER")); + assert.ok(vars.has("ACTIVE")); +}); + +test("parseEnvExampleVars: ignores prose", () => { + const text = ` +# This file documents env vars. Don't put values here. +Some narrative paragraph mentions PORT and DATA_DIR in passing. +PORT=20128 +`; + const vars = parseEnvExampleVars(text); + assert.deepEqual([...vars].sort(), ["PORT"]); +}); + +test("parseEnvDocVars: extracts SHOUTY_NAMES from inline backticks", () => { + const md = "Set `FOO_BAR` to enable the thing. Defaults to `BAR_BAZ_QUX`."; + const vars = parseEnvDocVars(md); + assert.deepEqual([...vars].sort(), ["BAR_BAZ_QUX", "FOO_BAR"]); +}); + +test("parseEnvDocVars: ignores values like `7s` or two-letter codes", () => { + const md = "TTL is `60s` and the type is `JSON`. Real var: `OMNIROUTE_TTL_MS`."; + const vars = parseEnvDocVars(md); + assert.deepEqual([...vars].sort(), ["JSON", "OMNIROUTE_TTL_MS"]); +}); + +test("runEnvDocSync: matched fixture passes", () => { + const envExampleText = ` +JWT_SECRET=secret +#OPTIONAL_VAR=value +ANOTHER_VAR=1 +`; + const envDocText = ` +# Reference + +| Variable | Default | Description | +| --- | --- | --- | +| \`JWT_SECRET\` | _(none)_ | required | +| \`OPTIONAL_VAR\` | _(unset)_ | optional | +| \`ANOTHER_VAR\` | 1 | enabled by default | +`; + const codeVars = new Set(["JWT_SECRET", "OPTIONAL_VAR", "ANOTHER_VAR"]); + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.codeMissingEnv, []); + assert.deepEqual(result.problems.envMissingDoc, []); + assert.deepEqual(result.problems.docMissingEnv, []); +}); + +test("runEnvDocSync: drift in code is flagged", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "NEW_VAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.codeMissingEnv, ["NEW_VAR"]); +}); + +test("runEnvDocSync: drift in doc is flagged", () => { + const envExampleText = `JWT_SECRET=secret\nNEW_VAR=value\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "NEW_VAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.envMissingDoc, ["NEW_VAR"]); +}); + +test("runEnvDocSync: drift in env (doc-only var) is flagged", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = ` +| \`JWT_SECRET\` | _(none)_ | required | +| \`OBSOLETE_VAR\` | _(unset)_ | docs-only legacy | +`; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.docMissingEnv, ["OBSOLETE_VAR"]); +}); + +test("runEnvDocSync: docOnlyAllowlist absolves doc-only entries", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = ` +| \`JWT_SECRET\` | _(none)_ | required | +| \`LEGACY_ALIAS\` | _(unset)_ | documented but not in env.example | +`; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET"]), + ignore: new Set(), + docOnlyAllowlist: new Set(["LEGACY_ALIAS"]), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.docMissingEnv, []); +}); + +test("runEnvDocSync: envOnlyAllowlist absolves env-only entries", () => { + const envExampleText = `JWT_SECRET=secret\nFOO_BAR=bar\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "FOO_BAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(["FOO_BAR"]), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.envMissingDoc, []); +}); + +test("runEnvDocSync: ignore set skips a code-referenced var", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "PATH", "HOME"]), + ignore: new Set(["PATH", "HOME"]), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); +}); + +test("repository contract is in sync (live data)", () => { + // Uses the real .env.example, docs/ENVIRONMENT.md, and the bundled + // allowlists. This is the same check that runs in pre-commit / CI. + const result = runEnvDocSync(); + if (!result.ok) { + const summary = JSON.stringify(result.problems, null, 2); + assert.fail(`Env/docs contract drift detected:\n${summary}`); + } + assert.equal(result.ok, true); +});