CodeQL re-flagged the HMAC variant from #2394 at high severity (#247) —
its data-flow analysis still sees an OAuth bearer reaching .update(token)
and applies js/insufficient-password-hash, regardless of whether the hash
primitive is createHash or createHmac.
Stop hashing the token at all. The session-pool Map is keyed by the
token verbatim, falling back to "anonymous" when the input is missing
or empty. This is safe because:
- The token is already held in CopilotSession.cookies for every pool
entry, so the Map key adds no new in-memory exposure.
- The pool is bounded by MAX_POOL_SIZE with LRU eviction, so memory
stays bounded regardless of how many distinct tokens appear.
- bcrypt/scrypt/argon2 — the only forms CodeQL accepts here — are the
wrong tool, since their slowness exists to thwart brute-force of
low-entropy human passwords we do not have.
Tests
- Replace the "16-char hex" shape assertion with verbatim-equality
assertions and an explicit "empty string → anonymous" case.
- Keep the regression guard that fails if anyone ever re-introduces
createHash/createHmac on the token (catches the alert reappearing
before CodeQL does).
- 16/16 copilot-web tests pass.
CodeQL re-flagged the SHA-256 inside sessionPoolKey at high severity even
after the rename/dedup in #2391 — its data-flow analysis still tracks the
OAuth bearer (accessToken → token) into createHash and applies the
js/insufficient-password-hash rule. Bcrypt/scrypt/argon2 would be wrong
here (the input is a high-entropy bearer, not a low-entropy human password
that needs brute-force protection).
Switch to HMAC-SHA-256 with a process-scope key generated at startup
(randomBytes(32)). HMAC is a MAC primitive, not a password hash, so the
CodeQL rule no longer applies; uniqueness, determinism-within-process,
and the 16-char hex shape all still hold, and as a small bonus an
off-process attacker can no longer precompute pool keys from a token
alone.
Tests
- Replace the "exact SHA-256 prefix" assertion with shape/uniqueness
checks and a regression guard that fails if the implementation ever
reverts to plain createHash.
- All 15 copilot-web tests pass.
#243 (js/request-forgery, high) — providers/bulk/route.ts
- Replace `fetch(\${origin}/api/providers/validate)` (where origin came from
spoofable `new URL(request.url).origin`) with a direct in-process call to
validateProviderApiKey. Eliminates the SSRF vector and the HTTP round-trip
through the same app.
- Resolve proxy once outside the loop and reuse via runWithProxyContext.
- Drop now-unused passthroughAuthHeaders helper.
#244 (js/resource-exhaustion, warn) — copilot-web.ts::solveHashcash
- Clamp upstream-supplied `difficulty` to [1, 8] before `"0".repeat(difficulty)`
so a malicious/buggy server can't force a huge prefix allocation or push the
10M-iteration loop into effectively unbounded work.
#245 (js/insufficient-password-hash, warn) — copilot-web.ts::getSession
- Dedupe the inline `createHash("sha256").update(accessToken)` call by reusing
the existing sessionPoolKey helper.
- Rename its parameter from `accessToken` to `token` and document that the
input is a high-entropy OAuth bearer used only as an in-memory Map key —
bcrypt/scrypt/argon2 would be incorrect here, and SHA-256:16 is an
appropriate fingerprint per docs/security/PUBLIC_CREDS.md.
Tests
- Export solveHashcash and add unit tests asserting it returns null for
out-of-range / non-integer difficulty and produces a numeric nonce for the
common difficulty=1 case.
- All 26 tests in copilot-web-executor.test.ts and providers-bulk-route.test.ts
continue to pass; sessionPoolKey contract (SHA-256:16) preserved.
Claude requires tool_result in the IMMEDIATELY next message after tool_use.
Existing fixToolPairs() checks global ID presence but not adjacency —
keeping tool_use blocks whose tool_result exists later in the array but
not in the next message, causing 400 errors.
Add fixToolAdjacency() that runs after fixToolPairs() to remove tool_use
blocks where the next message has no matching tool_result.
Pipeline: fixToolPairs → fixToolAdjacency → stripTrailingAssistantOrphanToolUse
Closes#2382
Co-Authored-By: OpenClaude (mimo-v2.5-pro) <openclaude@gitlawb.com>
Docker images launched without a sibling Redis container used to spam
'[REDIS] Error: connect ECONNREFUSED 127.0.0.1:6379' for every
rate-limit and API-key-auth cache lookup. The root cause was a default
of process.env.REDIS_URL || 'redis://localhost:6379' that turned the
opt-in cache into a hard dependency.
Three coordinated changes:
1. src/shared/utils/rateLimiter.ts — gate Redis on REDIS_URL being
explicitly set. getRedisClient() returns null when disabled; the
single connection-error handler dedupes via a redisErrorLogged latch
so a sustained outage produces one warn instead of per-request flood.
checkRateLimit() routes to the existing in-memory store on both the
'disabled' and 'test' paths.
2. src/lib/db/apiKeys.ts — short-circuit Redis-backed auth cache reads
and writes when getRedisClient() returns null. SQLite remains
authoritative; the cache is purely an optimization.
3. Same file — replace the wildcard scope matcher's dynamic RegExp
compilation with a deterministic segment walker. Eliminates the
ReDoS surface on operator-supplied scope patterns and silences the
Semgrep js/regex-injection advisory that previously blocked edits to
this file.
Single-instance deployments now work silently out of the box;
multi-instance setups continue to use Redis when REDIS_URL is set.
Combo dispatch and the combo test button used to crash with
'TypeError: e.startsWith is not a function' when a step's modelStr
failed to resolve (regression after #2338 added per-account LKGP
routing for local/Docker providers). The TypeScript annotation says
the field is always a string, but malformed combo rows leaked through
to the dispatch path.
Two defensive boundary guards:
1. open-sse/services/combo.ts LKGP fallback findIndex — type-check
target.modelStr before calling startsWith.
2. src/app/api/combos/test/route.ts testComboTarget — coerce
target.modelStr at the entry, surface a clean 'Combo step is
missing a model id' error instead of crashing.
Regression test parses the source and asserts no unguarded
target.modelStr.<string-method> usages remain in combo.ts, so a
future refactor that reintroduces the pattern fails loudly.
Two related provider-registry fixes:
#2361: LLM7.io was visible in the dashboard provider catalog (entry in
src/shared/constants/providers.ts) but the executor registry in
open-sse/config/providerRegistry.ts had no llm7 entry. Every connection
test fell through with a credential error because there was no baseUrl
or authType configured. Added the standard OpenAI-compatible v1
endpoint with a small seed model catalogue.
#2360: Cohere was pointed at the native /v2/chat upstream which returns
the proprietary { message: { content: [{type:'text', text:...}] } }
shape. The combo test validator (extractComboTestResponseText) only
reads the OpenAI choices[] envelope, so a successful Cohere call
surfaced as 'Provider returned HTTP 200 but no text content.' Cohere
publishes an OpenAI-compatible compatibility layer at
/compatibility/v1 that returns { choices: [{ message: { content }}] }
so we route there instead of needing a Cohere-specific translator.
Regression test asserts both registry entries match the expected
shape so a future refactor that reverts the URLs fails loudly.
The native claude OAuth path (base.ts:794 applySystemTransformPipeline)
early-exited because DEFAULT_SYSTEM_TRANSFORMS_CONFIG.providers[claude]
was {enabled:false, pipeline:[]}. Result: third-party-agent fingerprints
(github.com/anomalyco/opencode, 'You are OpenCode', 'Here is some useful
information about the environment...') leaked into /v1/messages system
blocks, triggering Anthropic billing-gate:
[400] Third-party apps now draw from extra usage, not plan limits.
Mirror the cc-bridge defaults:
- DEFAULT_PARAGRAPH_REMOVAL_ANCHORS + OPENWEBUI_PARAGRAPH_ANCHORS
- DEFAULT_IDENTITY_PREFIXES + OPENWEBUI_IDENTITY_PREFIXES
- DEFAULT_TEXT_REPLACEMENTS as replace_text ops (allOccurrences:true)
- obfuscate_words
Omit prepend_system_block + inject_billing_header — base.ts:759-784
already handles those on the native claude OAuth path.
UI mirror RoutingTab.tsx and snapshot in system-transforms.test.ts
updated for parity. 56/56 transform-related tests pass.
Verified end-to-end 2026-05-18 after npm run build:cli + restart:
- request v8tq04 -> 200 in 1.84s
- request shape {max_tokens:32000, reasoning_effort:high} -> 200 in 1.99s
- x-omniroute-provider=cc (claude-code OAuth pool, account 62875e01)
- telemetry: [SystemTransforms] claude-native: drop_paragraph_if_contains,
drop_paragraph_if_starts_with, replace_text, replace_text, obfuscate_words
When the shared <Tooltip> appeared inside a modal (combo editor) or any
ancestor with overflow:hidden/auto, long labels were clipped — the
absolute-positioned <span> stayed inside the modal's stacking context.
Render the tooltip via createPortal to document.body by default
(usePortal prop, defaults to true). Coordinates are computed from the
trigger's getBoundingClientRect on each show and written directly to
the tooltip ref's .style — that avoids the cascading-render warning
that setState-inside-effect triggers, while still doing one synchronous
measure-and-position pass before the user sees any flicker.
Coordinates are clamped to the viewport bounds so a trigger near the
right edge produces a tooltip that stays on-screen instead of bleeding
off. Adds an optional multiline prop that swaps the legacy
whitespace-nowrap clamp for max-w-xs whitespace-normal break-words for
explanation strings (combo strategy help, etc.).
Backward compat: existing call sites do not need to change; the portal
+ clamp behavior is transparent to consumers.
Adds a deterministic test for the rawEffort resolution chain in
open-sse/executors/codex.ts so a future refactor that flips the priority
back (the bug we just fixed) trips an explicit assertion. Also verifies
end-to-end behavior of the priority order: modelEffort > explicitReasoning
> requestReasoningEffort > fallbackReasoningEffort.
The source fix itself landed earlier on this branch; this commit only
adds the test coverage.
Claude Code Pro/Team users hit a 429 cascade where every Claude account
got marked rate-limited for only a few seconds, retried, hit 429 again,
and the cycle exhausted all accounts until the 5h subscription window
finally reset. Root cause: Anthropic OAuth 429 bodies carry phrases like
'Usage Limit Reached' or 'Claude Pro usage limit reached' that don't
contain the word 'quota'. classifyErrorText fell through to
RATE_LIMIT_EXCEEDED with the OAuth ~5s base cooldown.
Three targeted changes in open-sse/services/accountFallback.ts:
1. classifyErrorText: recognize the Anthropic OAuth phrases ('usage limit
reached', 'claude pro usage limit', 'you've reached your usage limit'
and variants) and return QUOTA_EXHAUSTED.
2. checkFallbackError: new branch that, when the classifier flags
QUOTA_EXHAUSTED for a non-credits/non-daily case, applies a deliberate
1h cooldown (subscription quotas need real time to reset; the existing
COOLDOWN_MS.paymentRequired is only 2 minutes).
3. parseRetryFromErrorText: parse absolute ISO 8601 timestamps in the
body ('Try again at 2026-05-17T10:00:00Z', 'Please wait until ...').
When present, honor the upstream's stated recovery time instead of
the 1h default.
Cross-provider fallback for direct (non-combo) calls remains a separate
feature — operators wanting that should route Claude through a combo
that includes openrouter/claude or another fallback target.
`validateResponseQuality` flagged any response with `content: null` as
empty and triggered a false-positive 502 combo fallback, even when the
upstream returned the entire answer in `reasoning_content`. This
affected every reasoning model that omits `content` by design:
moonshotai/Kimi-K2.5-TEE, zai-org/GLM-5-TEE, zai-org/GLM-4.7-TEE,
DeepSeek-R1 and Qwen-thinking variants via Chutes.ai, Nvidia, and other
OpenAI-compatible gateways.
Treat a non-empty `reasoning_content` (or its legacy `reasoning` alias)
as valid content. Empty/whitespace-only strings still fall through to
the existing empty-content rejection so we don't weaken the guard.
Backward compat verified: regular content-only and tool_calls-only
responses still validate without change.
`.dockerignore` excluded everything under `docs/` except `openapi.yaml`,
which broke the in-product Docs viewer at `/docs/*` with "ENOENT: no
such file or directory, open '/app/docs/SETUP_GUIDE.md'" for every help
page. The previous block-list was meant to keep the image small but it
hid the ~5 MB English markdown tree that the viewer actually reads.
Replace the block with a targeted exclude of the heavy assets that
account for ~45 MB of the original ~50 MB:
- docs/i18n/** (translated copies — viewer falls back to English)
- docs/screenshots/**
- docs/diagrams/exported/** plus raster/SVG variants
Note: Go filepath.Match (Docker's matcher) treats `*` as not crossing
`/`, so the existing `*.md` rule still excludes only root-level
markdown — nested `docs/**/*.md` is implicitly kept without a
re-include rule that would have brought i18n back.
Added a regression test that parses .dockerignore and asserts the
critical English docs survive the filter while the heavy paths still
do not.
The auto-routing path in src/sse/handlers/chat.ts called `getSettings()`
on every `auto` / `auto/\*` request, but the symbol was never imported in
this module (only `getCachedSettings` was). The runtime ReferenceError
crashed the request with a 500 before any routing could happen.
Replace the call with `getCachedSettings` (already imported, identical
return shape, plus the cached variant benefits the auto-routing hot path).
A regression-guard test scans chat.ts to fail loudly if a future refactor
re-introduces the unimported call pattern.
Adds an import flow that lets users bring an existing Codex auth.json
into OmniRoute without a fresh OAuth login. Both a file-upload tab and
a paste-JSON tab are supported.
- `codexAuthImport.ts`: pure parser + createConnectionFromAuthFile
(conflict detection, overwriteExisting, JWT email/exp extraction)
- `POST /api/providers/codex-auth/import`: Zod-validated endpoint with
audit log (`provider.credentials.imported`)
- `importCodexAuthSchema` in schemas.ts (discriminated union json/text,
256 KB cap on paste source)
- `<ImportCodexAuthModal>` in providers/[id]/page.tsx with upload/paste
tabs, email auto-detection, name/email/overwrite fields
- "Import auth" toolbar button shown only on the Codex provider page
- 29 unit tests (17 parser + 12 schema) — all passing
Export filename change:
- Drop the redundant `codex-` prefix; embed the account email so multiple
exported files can coexist in the same downloads folder.
- Email is extracted from the id_token JWT `email` claim, with fallback
to connection.email and finally to the sanitized connection label.
- sanitizeFileNamePart now preserves @ so addresses survive intact
(e.g. `auth-diego@example.com.json`).
Apply Local refinement:
- ApplyCodexAuthModal: confirmation modal showing the resolved target
path, the side-by-side .bak location, and the centralized backup
trail. User must tick a confirmation checkbox before Apply enables.
- writeCodexAuthFileToLocalCli now writes a side-by-side
`auth-<timestamp>.bak` inside the .codex/ directory before replacing
the live file, in addition to the existing centralized backup. Both
inputs to the .bak path are server-controlled (dirname from the
static CLI_TOOLS table; basename from a server-generated ISO
timestamp), so no user input touches path APIs.
- apply-local route now emits a `provider.credentials.applied` audit
event with the resolved authPath and savedBakPath, and routes all
errors through sanitizeErrorMessage() per the security guide.
Tests: tests/unit/codexAuthFile.test.ts covers sanitization, JWT email
extraction, filename format for both branches (email/label), and the
ISO-timestamp .bak basename safety.
Scope: this is PR1 of the import/export work tracked under
_tasks/features-v3.8.0/importexport/. PR2 (import single) and PR3
(import bulk) will follow.
- Replace replace(/\/+$/, "") with explicit while-endsWith loop to avoid
polynomial backtracking on inputs with repeated trailing slashes
(CodeQL js/polynomial-redos #233-240, 8 alerts):
- @omniroute/opencode-provider/src/index.ts (normalizeBaseURL)
- bin/cli/api.mjs (stripTrailingSlash)
- src/lib/cli-helper/config-generator/{claude,cline,codex,continue,
kilocode,opencode}.ts (6 generators with identical pattern)
- tests/live/deepseek-web-live.test.ts: assert hostname via URL parsing
instead of String.includes() so the check is exact-match rather than
substring (CodeQL js/incomplete-url-substring-sanitization #241).
Alert #242 (Array.prototype.includes against fixed needle constant
OPENWEBUI_PARAGRAPH_ANCHORS) dismissed as CodeQL false-positive — not a
URL sanitization callsite.
Resolves conflicts in 9 files to bring 181 commits from release/v3.8.0 into
the dashboard refactor branch ahead of merging back to release.
Layout strategy: our pages overhaul (tabs→pages, restructured sidebar,
removed redundant headers, OpenCode Free no-auth card) is the source of
truth. Release's functional additions are adapted into our layout.
Conflict resolution:
- package.json/package-lock.json: take release's deps (axios bump, CLI v4
deps, tls-client-node/wreq-js move to optionalDependencies); re-add our
@xyflow/react addition; regenerate lockfile.
- src/shared/constants/sidebarVisibility.ts: keep our 9-section restructure
— release's new IDs (limits, media, cli-tools, agents, cloud-agents,
memory, skills, agent-skills, context-*) are all already present in our
groups.
- src/i18n/messages/en.json: auto-merge picked up all release's new keys
(autoCatalog*, quotaCutoffs*, systemTransforms*, schema-coercion, vision);
only naming conflict was OmniSkills/AgentSkills — kept ours (no space).
- src/app/(dashboard)/dashboard/HomePageClient.tsx: kept our Provider
Topology card; ported release's TierCoverageWidget (placed before
topology).
- src/app/(dashboard)/dashboard/settings/page.tsx: kept our redirect to
/settings/general (we moved tabs to separate pages); release's sticky
tab CSS change is moot in our structure.
- src/app/(dashboard)/dashboard/skills/page.tsx: rerere applied — release
hardcoded "OmniSkills" h1 was already removed by our header-cleanup
refactor.
- src/app/(dashboard)/dashboard/agent-skills/page.tsx: both branches
created this file independently with identical data source; kept our
Tailwind-themed 2-column grid (release's version used inline styles).
- src/app/(dashboard)/dashboard/batch/page.tsx: kept our single-tab
structure (FilesListTab moved to /batch/files page); ported release's
onRefresh prop addition.
- src/app/(dashboard)/dashboard/batch/files/page.tsx (not in conflict but
updated): added batches fetch + batches prop to preserve release's
feature of showing related batches in the file detail modal.
Pre-existing typecheck errors in open-sse/services/contextManager.ts
(lines 141, 154, 167) come from release/v3.8.0 and are not introduced by
this merge.
Add two OpenAI-compatible API-key providers via the Gitlawb Opengateway
gateway at opengateway.gitlawb.com:
- gitlawb (alias glb): xiaomi-mimo endpoint with 5 MiMo models
- gitlawb-gmi (alias glb-gmi): gmi-cloud endpoint with 40+ models
including GPT-5.x, Claude 4.x, DeepSeek, Gemini, Qwen, GLM, Kimi
Both providers include CLI-mimicking headers (User-Agent, X-Title,
HTTP-Referer) to avoid upstream rate limiting. GMI Cloud provider
has passthroughModels enabled since model access varies per API key.
Propagate invalid refresh-token errors instead of collapsing them to null
so callers can distinguish expired credentials from transient failures.
Mark affected connections as expired and inactive when refresh fails
with an unrecoverable error, and persist that state during credential
updates. Add tests covering retry bail-out and Claude/Codex refresh
error handling.