Keys with non-empty allowedQuotas may only use models whose provider belongs
to their pools' provider set; anything outside → 403 QUOTA_ONLY.
Normal allowedModels/allowedCombos checks are bypassed for quota-exclusive keys.
Introduces src/lib/quota/quotaKey.ts with resolveQuotaKeyScope(), a
pure async helper that maps an API key's allowedQuotas pool-ID list to
the concrete connectionIds and provider slugs it is permitted to use.
Covers empty/null/undefined input, missing pools, orphaned connectionIds,
and multi-pool deduplication. No behaviour change to existing code paths.
- Move !enforceScopes guard before MCP_TOOL_MAP lookup in evaluateToolScopes()
- Add inlineScopes parameter for dynamic tool scope resolution
- Add scopes to all 33 dynamic tool definitions across 5 tool files
- Wire toolDef.scopes through withScopeEnforcement in server.ts
- Preserves existing behavior: tool_definition_missing returned when
enforceScopes=true and no scopes found anywhere
The agy provider was registered in providerRegistry.ts with
executor: "antigravity" but the executor map in executors/index.ts
only had an "antigravity" entry. getExecutor("agy") fell through to
DefaultExecutor, which returned undefined for baseUrl (agy only has
baseUrls), causing fetch(undefined) → TypeError: Cannot read properties
of undefined (reading 'toString').
Closesdiegosouzapw/OmniRoute#2932
The authz pipeline runs in the Next middleware runtime (proxy.ts -> runAuthzPipeline)
where ctx.request is a NextRequest with no .socket/.ip. requestPeerAddress therefore
returned null, so isLoopbackRequest was ALWAYS false and every LOCAL_ONLY path 403'd
even from loopback (Services/MCP/Traffic-Inspector were unusable). Read the Host
header instead — exactly what isLoopbackHost/isPrivateLanHost were built to parse —
which restores loopback and, combined with isPrivateLanHost, enables the
owner-authorized private-LAN access. Spawn-capable endpoints still require
manage-scope auth after this gate.
Services + Traffic-Inspector (LOCAL_ONLY, spawn-capable) returned 403 when the
dashboard was reached via the LAN IP (192.168.0.x) instead of loopback. Add
isPrivateLanHost (RFC1918 IPv4 + IPv6 ULA/link-local) and widen ONLY the
local-only PATH gate to accept private-LAN socket peer IPs — based on the real
socket peer address (not the spoofable Host header), so public-internet clients
present public IPs and stay blocked. The CLI-token gate stays strictly loopback;
paths remain LOCAL_ONLY-classified (Hard Rules 15/17 unchanged). Enforcement-layer
carve-out for a LAN-deployed instance, authorized by the operator.
The pool usage snapshot can come back without a dimensions array (e.g. when the
plan resolves to empty for catalog-only providers). PoolCard.computeStatus and
hasDimensions read usage.dimensions.length directly, crashing the whole page
("Cannot read properties of undefined (reading 'length')"). Normalize to [] in
PoolCard and in usePoolsUsageAggregate (dimensions/perKey).
5 new test files covering all 13 changed production files:
- estimateSizeFast.test.ts: 16 tests for fast size estimator (circular ref
protection, early exit, nested structures, Map safety)
- eviction-guards-apiKeyRotator.test.ts: 5 tests for Map eviction guards
(!has() check prevents evicting existing keys on update)
- eviction-guards-codexQuotaFetcher.test.ts: 4 tests for connectionRegistry
and quotaCache eviction guards
- rateLimitManager-idle-eviction.test.ts: 6 tests for idle limiter cleanup,
limiterLastUsed tracking, and shutdown behavior
- registry-direct-exports.test.ts: 20 tests verifying all 8 registries export
plain objects (no Proxy traps, no lazy getters, mutable entries)
Extract estimateSizeFast/isSmallEnoughForSemanticCache into standalone
open-sse/utils/estimateSize.ts to make them testable without importing
the entire chatCore.ts dependency tree.
- estimateSizeFast: add WeakSet cycle detection to prevent infinite
loop on circular object references
- trace(): wrap JSON.stringify(extra) in try-catch to handle BigInt,
circular refs, or other non-serializable values gracefully
- Registry API change (Comment 3): verified all callers already use
new getter functions — no broken call sites
- Add !has(key) guard before eviction to avoid evicting entries
that are about to be updated (combo.ts, apiKeyRotator.ts,
codexQuotaFetcher.ts)
- Use optional chaining for provider?.toUpperCase() null safety
- Replace Object.values() with for-in in estimateSizeFast hot path
- Capture full search results (title/url/snippet) per provider, not just URLs.
- Render one column per selected provider: metrics header + result list
(title link, snippet, url), horizontal scroll for N providers.
- Mark results whose URL appears across providers with a star (overlap).
- Remove the 4-provider cap (MAX_PROVIDERS); add Select all / Clear; compare
every configured provider. Raise max_results 5 -> 10.
- Chat (StudioConfigPane): add Provider + Model selects reusing the translator
hooks; order Endpoint -> Provider -> Model; ConfigState gains optional provider.
- Compare (CompareTab): add a user-prompt input and include it in the request
body; throttle per-column stream updates via requestAnimationFrame to stop the
UI freeze (was setColumns per chunk x N columns with an empty user message).
- Build (BuildTab + build/BuildWizard): redesign as a guided 3-step wizard
(What to test -> Configure -> Run) reusing ToolsBuilder/StructuredOutputEditor;
all run/tool-call handlers preserved.
- i18n: playground.build.* (18 keys) in en + pt-BR.
- MemoriesTab: auto-run health check on mount + poll every 30s (was manual-only).
- page: add enable/disable memory toggle (role=switch) via useMemorySettings.save({ enabled }).
- MemoryEngineStatus: show "npm install sqlite-vec" hint when vector store backend is "none".
- i18n: memory.memoryEnabled + memory.engine.vectorStoreInstallHint (en + pt-BR).
- Add compliance.eventTypes (36 labels) to en.json + pt-BR.json.
- ComplianceTab: render translated label via t.has/t fallback instead of raw entry.action.
- A2aAuditTab: render translated task state via a2aState* keys instead of raw task.state.
- Memory type/strategy dropdowns needed no i18n change — keys already exist; the
Phase 1 Select fix makes them render.
- search-tools: export modal no longer opens by default / stuck — guard on
exportOpen and drop the invalid isOpen prop the modal never read.
- logs: remove duplicate proxy/console tabs + SegmentedControl (dedicated
/dashboard/logs/proxy and /console pages already exist in the menu).
- memory: order tabs Memories -> Engine -> Playground.
- ui(Select): render children and suppress the placeholder/options branch when
children are provided — fixes the "empty" memory type/strategy dropdowns
(children were being shadowed by the component's own option list).
- test: source-level regression guards for all four.
Root cause: model string provider prefix (e.g. "xiaomi" from
"xiaomi/mimo-v2-flash") differs from the credential DB provider ID
(e.g. "opengate") when a combo target has a custom providerId. The
pre-selection and execution flows both looked up credentials using the
model-inferred provider, which didn't match any DB entry.
Fix A (chat.ts): checkModelAvailable now uses target.providerId when
available, falling back to modelInfo.provider.
Fix B (chat.ts): handleSingleModelChat now accepts runtimeOptions.providerId
and preferentially uses it for credential lookup instead of re-resolving
the provider from the model string.
Fix C (model.ts): add "xiaomi" alias for "xiaomi-mimo" so direct
(non-combo) model requests to xiaomi/mimo-* also resolve correctly.
The CPA executor now cloaks non-Claude-Code tool names (not just mcp_*), so the
prior "does not rewrite non-mcp_ tool names" assertion no longer holds:
my_tool is aliased to MyTool and restored on the response via _toolNameMap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses confirmed findings from an adversarial review of the prior commits:
- schema sanitizer: a truncation placeholder in a SCALAR annotation keyword
(description/title/pattern/format) was coerced to {}, which is itself invalid
draft-2020-12 and re-triggered the exact "input_schema is invalid" 400 the
sanitizer exists to prevent. Placeholders are now only coerced to {} in
subschema-expecting positions; scalar keywords are left untouched.
- schema sanitizer: numeric-string coercion is folded into
stripInvalidSchemaConstructs so it also covers contains / propertyNames /
additionalItems (which coerceSchemaNumericFields never visited).
- schema sanitizer: stop stripping the valid `default` keyword on the Claude
native/passthrough surface (the #1782 default-strip is a translator concern;
tool schemas here were previously forwarded verbatim). sanitizeClaudeToolSchema
is now a single stripInvalidSchemaConstructs pass.
- tool-name cloak: consult TOOL_RENAME_MAP / EXTRA_TOOL_RENAME_MAP before the
generic PascalCase fallback, so the CLIProxyAPI path uses the established
fingerprint-evasion aliases (subagents->SubDispatch, session_status->CheckStatus,
webfetch->WebFetch, ...) identically to the native path instead of weaker
first-letter casing.
- kill-switch: CLAUDE_DISABLE_TOOL_NAME_CLOAK is now honoured inside
cloakThirdPartyToolNames, so BOTH the native and CLIProxyAPI executor paths
respect it (previously only base.ts did); .env.example + ENVIRONMENT.md updated.
Regression tests added for each. Verified end-to-end through the live CPA path:
mixture_of_agents, subagents, and a tool carrying placeholder descriptions and
`default` values all return 200 with original names restored on the response.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>