fix(v3.8.4-followup): 5 bug fixes from triage of 79 open issues (#2753)

Integrated into release/v3.8.4
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-26 17:42:33 -03:00
committed by GitHub
parent b6c2ece344
commit a3476298d9
19 changed files with 637 additions and 61 deletions

File diff suppressed because one or more lines are too long

View File

@@ -4,9 +4,9 @@ import { default as __fd_glob_63 } from "../docs/routing/meta.json?collection=do
import { default as __fd_glob_62 } from "../docs/reference/openapi.yaml?collection=docs"
import { default as __fd_glob_61 } from "../docs/reference/meta.json?collection=docs"
import { default as __fd_glob_60 } from "../docs/ops/meta.json?collection=docs"
import { default as __fd_glob_59 } from "../docs/compression/meta.json?collection=docs"
import { default as __fd_glob_58 } from "../docs/guides/meta.json?collection=docs"
import { default as __fd_glob_57 } from "../docs/frameworks/meta.json?collection=docs"
import { default as __fd_glob_59 } from "../docs/guides/meta.json?collection=docs"
import { default as __fd_glob_58 } from "../docs/frameworks/meta.json?collection=docs"
import { default as __fd_glob_57 } from "../docs/compression/meta.json?collection=docs"
import { default as __fd_glob_56 } from "../docs/architecture/meta.json?collection=docs"
import { default as __fd_glob_55 } from "../docs/meta.json?collection=docs"
import * as __fd_glob_54 from "../docs/security/STEALTH_GUIDE.md?collection=docs"
@@ -32,28 +32,28 @@ import * as __fd_glob_35 from "../docs/ops/PROXY_GUIDE.md?collection=docs"
import * as __fd_glob_34 from "../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"
import * as __fd_glob_33 from "../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"
import * as __fd_glob_32 from "../docs/ops/COVERAGE_PLAN.md?collection=docs"
import * as __fd_glob_31 from "../docs/frameworks/WEBHOOKS.md?collection=docs"
import * as __fd_glob_30 from "../docs/frameworks/SKILLS.md?collection=docs"
import * as __fd_glob_29 from "../docs/frameworks/OPENCODE.md?collection=docs"
import * as __fd_glob_28 from "../docs/frameworks/MEMORY.md?collection=docs"
import * as __fd_glob_27 from "../docs/frameworks/MCP-SERVER.md?collection=docs"
import * as __fd_glob_26 from "../docs/frameworks/GAMIFICATION.md?collection=docs"
import * as __fd_glob_25 from "../docs/frameworks/EVALS.md?collection=docs"
import * as __fd_glob_24 from "../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"
import * as __fd_glob_23 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs"
import * as __fd_glob_22 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"
import * as __fd_glob_21 from "../docs/frameworks/A2A-SERVER.md?collection=docs"
import * as __fd_glob_20 from "../docs/guides/USER_GUIDE.md?collection=docs"
import * as __fd_glob_19 from "../docs/guides/UNINSTALL.md?collection=docs"
import * as __fd_glob_18 from "../docs/guides/TROUBLESHOOTING.md?collection=docs"
import * as __fd_glob_17 from "../docs/guides/TERMUX_GUIDE.md?collection=docs"
import * as __fd_glob_16 from "../docs/guides/SETUP_GUIDE.md?collection=docs"
import * as __fd_glob_15 from "../docs/guides/PWA_GUIDE.md?collection=docs"
import * as __fd_glob_14 from "../docs/guides/KIRO_SETUP.md?collection=docs"
import * as __fd_glob_13 from "../docs/guides/I18N.md?collection=docs"
import * as __fd_glob_12 from "../docs/guides/FEATURES.md?collection=docs"
import * as __fd_glob_11 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs"
import * as __fd_glob_10 from "../docs/guides/DOCKER_GUIDE.md?collection=docs"
import * as __fd_glob_31 from "../docs/guides/USER_GUIDE.md?collection=docs"
import * as __fd_glob_30 from "../docs/guides/UNINSTALL.md?collection=docs"
import * as __fd_glob_29 from "../docs/guides/TROUBLESHOOTING.md?collection=docs"
import * as __fd_glob_28 from "../docs/guides/TERMUX_GUIDE.md?collection=docs"
import * as __fd_glob_27 from "../docs/guides/SETUP_GUIDE.md?collection=docs"
import * as __fd_glob_26 from "../docs/guides/PWA_GUIDE.md?collection=docs"
import * as __fd_glob_25 from "../docs/guides/KIRO_SETUP.md?collection=docs"
import * as __fd_glob_24 from "../docs/guides/I18N.md?collection=docs"
import * as __fd_glob_23 from "../docs/guides/FEATURES.md?collection=docs"
import * as __fd_glob_22 from "../docs/guides/ELECTRON_GUIDE.md?collection=docs"
import * as __fd_glob_21 from "../docs/guides/DOCKER_GUIDE.md?collection=docs"
import * as __fd_glob_20 from "../docs/frameworks/WEBHOOKS.md?collection=docs"
import * as __fd_glob_19 from "../docs/frameworks/SKILLS.md?collection=docs"
import * as __fd_glob_18 from "../docs/frameworks/OPENCODE.md?collection=docs"
import * as __fd_glob_17 from "../docs/frameworks/MEMORY.md?collection=docs"
import * as __fd_glob_16 from "../docs/frameworks/MCP-SERVER.md?collection=docs"
import * as __fd_glob_15 from "../docs/frameworks/GAMIFICATION.md?collection=docs"
import * as __fd_glob_14 from "../docs/frameworks/EVALS.md?collection=docs"
import * as __fd_glob_13 from "../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"
import * as __fd_glob_12 from "../docs/frameworks/CLOUD_AGENT.md?collection=docs"
import * as __fd_glob_11 from "../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"
import * as __fd_glob_10 from "../docs/frameworks/A2A-SERVER.md?collection=docs"
import * as __fd_glob_9 from "../docs/compression/RTK_COMPRESSION.md?collection=docs"
import * as __fd_glob_8 from "../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"
import * as __fd_glob_7 from "../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"
@@ -72,4 +72,4 @@ const create = server<typeof Config, import("fumadocs-mdx/runtime/types").Intern
}
}>({"doc":{"passthroughs":["extractedReferences"]}});
export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_55, "architecture/meta.json": __fd_glob_56, "frameworks/meta.json": __fd_glob_57, "guides/meta.json": __fd_glob_58, "compression/meta.json": __fd_glob_59, "ops/meta.json": __fd_glob_60, "reference/meta.json": __fd_glob_61, "reference/openapi.yaml": __fd_glob_62, "routing/meta.json": __fd_glob_63, "security/meta.json": __fd_glob_64, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "guides/DOCKER_GUIDE.md": __fd_glob_10, "guides/ELECTRON_GUIDE.md": __fd_glob_11, "guides/FEATURES.md": __fd_glob_12, "guides/I18N.md": __fd_glob_13, "guides/KIRO_SETUP.md": __fd_glob_14, "guides/PWA_GUIDE.md": __fd_glob_15, "guides/SETUP_GUIDE.md": __fd_glob_16, "guides/TERMUX_GUIDE.md": __fd_glob_17, "guides/TROUBLESHOOTING.md": __fd_glob_18, "guides/UNINSTALL.md": __fd_glob_19, "guides/USER_GUIDE.md": __fd_glob_20, "frameworks/A2A-SERVER.md": __fd_glob_21, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_22, "frameworks/CLOUD_AGENT.md": __fd_glob_23, "frameworks/EMBEDDED-SERVICES.md": __fd_glob_24, "frameworks/EVALS.md": __fd_glob_25, "frameworks/GAMIFICATION.md": __fd_glob_26, "frameworks/MCP-SERVER.md": __fd_glob_27, "frameworks/MEMORY.md": __fd_glob_28, "frameworks/OPENCODE.md": __fd_glob_29, "frameworks/SKILLS.md": __fd_glob_30, "frameworks/WEBHOOKS.md": __fd_glob_31, "ops/COVERAGE_PLAN.md": __fd_glob_32, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_33, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_34, "ops/PROXY_GUIDE.md": __fd_glob_35, "ops/RELEASE_CHECKLIST.md": __fd_glob_36, "ops/SQLITE_RUNTIME.md": __fd_glob_37, "ops/TUNNELS_GUIDE.md": __fd_glob_38, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_39, "reference/API_REFERENCE.md": __fd_glob_40, "reference/CLI-TOOLS.md": __fd_glob_41, "reference/ENVIRONMENT.md": __fd_glob_42, "reference/FREE_TIERS.md": __fd_glob_43, "reference/PROVIDER_REFERENCE.md": __fd_glob_44, "routing/AUTO-COMBO.md": __fd_glob_45, "routing/REASONING_REPLAY.md": __fd_glob_46, "security/CLI_TOKEN.md": __fd_glob_47, "security/CLI_TOKEN_AUTH.md": __fd_glob_48, "security/COMPLIANCE.md": __fd_glob_49, "security/ERROR_SANITIZATION.md": __fd_glob_50, "security/GUARDRAILS.md": __fd_glob_51, "security/PUBLIC_CREDS.md": __fd_glob_52, "security/ROUTE_GUARD_TIERS.md": __fd_glob_53, "security/STEALTH_GUIDE.md": __fd_glob_54, });
export const docs = await create.docs("docs", "docs", {"meta.json": __fd_glob_55, "architecture/meta.json": __fd_glob_56, "compression/meta.json": __fd_glob_57, "frameworks/meta.json": __fd_glob_58, "guides/meta.json": __fd_glob_59, "ops/meta.json": __fd_glob_60, "reference/meta.json": __fd_glob_61, "reference/openapi.yaml": __fd_glob_62, "routing/meta.json": __fd_glob_63, "security/meta.json": __fd_glob_64, }, {"architecture/ARCHITECTURE.md": __fd_glob_0, "architecture/AUTHZ_GUIDE.md": __fd_glob_1, "architecture/CODEBASE_DOCUMENTATION.md": __fd_glob_2, "architecture/REPOSITORY_MAP.md": __fd_glob_3, "architecture/RESILIENCE_GUIDE.md": __fd_glob_4, "compression/COMPRESSION_ENGINES.md": __fd_glob_5, "compression/COMPRESSION_GUIDE.md": __fd_glob_6, "compression/COMPRESSION_LANGUAGE_PACKS.md": __fd_glob_7, "compression/COMPRESSION_RULES_FORMAT.md": __fd_glob_8, "compression/RTK_COMPRESSION.md": __fd_glob_9, "frameworks/A2A-SERVER.md": __fd_glob_10, "frameworks/AGENT_PROTOCOLS_GUIDE.md": __fd_glob_11, "frameworks/CLOUD_AGENT.md": __fd_glob_12, "frameworks/EMBEDDED-SERVICES.md": __fd_glob_13, "frameworks/EVALS.md": __fd_glob_14, "frameworks/GAMIFICATION.md": __fd_glob_15, "frameworks/MCP-SERVER.md": __fd_glob_16, "frameworks/MEMORY.md": __fd_glob_17, "frameworks/OPENCODE.md": __fd_glob_18, "frameworks/SKILLS.md": __fd_glob_19, "frameworks/WEBHOOKS.md": __fd_glob_20, "guides/DOCKER_GUIDE.md": __fd_glob_21, "guides/ELECTRON_GUIDE.md": __fd_glob_22, "guides/FEATURES.md": __fd_glob_23, "guides/I18N.md": __fd_glob_24, "guides/KIRO_SETUP.md": __fd_glob_25, "guides/PWA_GUIDE.md": __fd_glob_26, "guides/SETUP_GUIDE.md": __fd_glob_27, "guides/TERMUX_GUIDE.md": __fd_glob_28, "guides/TROUBLESHOOTING.md": __fd_glob_29, "guides/UNINSTALL.md": __fd_glob_30, "guides/USER_GUIDE.md": __fd_glob_31, "ops/COVERAGE_PLAN.md": __fd_glob_32, "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": __fd_glob_33, "ops/FLY_IO_DEPLOYMENT_GUIDE.md": __fd_glob_34, "ops/PROXY_GUIDE.md": __fd_glob_35, "ops/RELEASE_CHECKLIST.md": __fd_glob_36, "ops/SQLITE_RUNTIME.md": __fd_glob_37, "ops/TUNNELS_GUIDE.md": __fd_glob_38, "ops/VM_DEPLOYMENT_GUIDE.md": __fd_glob_39, "reference/API_REFERENCE.md": __fd_glob_40, "reference/CLI-TOOLS.md": __fd_glob_41, "reference/ENVIRONMENT.md": __fd_glob_42, "reference/FREE_TIERS.md": __fd_glob_43, "reference/PROVIDER_REFERENCE.md": __fd_glob_44, "routing/AUTO-COMBO.md": __fd_glob_45, "routing/REASONING_REPLAY.md": __fd_glob_46, "security/CLI_TOKEN.md": __fd_glob_47, "security/CLI_TOKEN_AUTH.md": __fd_glob_48, "security/COMPLIANCE.md": __fd_glob_49, "security/ERROR_SANITIZATION.md": __fd_glob_50, "security/GUARDRAILS.md": __fd_glob_51, "security/PUBLIC_CREDS.md": __fd_glob_52, "security/ROUTE_GUARD_TIERS.md": __fd_glob_53, "security/STEALTH_GUIDE.md": __fd_glob_54, });

View File

@@ -2,6 +2,14 @@
## [Unreleased]
### 🔧 Bug Fixes
- **validation:** add Poolside specialty validator (direct `/chat/completions` probe — Poolside has no `/v1/models` endpoint and returns 401 for unknown routes, which the generic `/models` flow misread as "invalid API key") (#2723)
- **validation:** add NVIDIA NIM specialty validator and harden `normalizeBaseUrl` against non-string `providerSpecificData.baseUrl` — fixes the `e.startsWith is not a function` TypeError that surfaced after minification (#2463)
- **cli:** `omniroute compression *` falls back to direct REST endpoints (`/api/settings/compression`, `/api/context/combos`, `/api/context/analytics`) when `/api/mcp/tools/call` returns 404; normalize `none → off` / `hybrid → stacked` engine aliases (#2688)
- **cli:** import `cli-helper/tool-detector` and `cli-helper/doctor/checks` with the explicit `.ts` extension that tsx resolves directly, so the published npm package (which ships only the `.ts` source) no longer crashes with `Cannot find module '…tool-detector.js'` (#2509)
- **authz:** make the DB feature-flag override authoritative over `process.env` for `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`, so toggling "Allow Private Provider URLs" in the Electron dashboard takes effect without restarting the spawned server (#2575)
## [3.8.4] — 2026-05-26
### 🔒 Security

View File

@@ -3,12 +3,49 @@ import { apiFetch } from "../api.mjs";
import { emit } from "../output.mjs";
import { t } from "../i18n.mjs";
const VALID_ENGINES = ["caveman", "rtk", "hybrid", "none"];
// #2688 — CLI no longer assumes MCP is enabled. Engine names are normalized
// to the current core set; legacy aliases continue to work.
const VALID_ENGINES = ["off", "caveman", "rtk", "stacked"];
const ENGINE_ALIASES = { none: "off", hybrid: "stacked" };
async function mcpCall(name, args) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
function normalizeEngine(name) {
return ENGINE_ALIASES[name] ?? name;
}
// Direct REST fallbacks used when the MCP tool surface is not mounted (404).
// Keeps every subcommand working on minimal builds.
async function restCompressionStatus() {
const [settingsRes, combosRes, analyticsRes] = await Promise.all([
apiFetch("/api/settings/compression"),
apiFetch("/api/context/combos"),
apiFetch("/api/context/analytics?period=7d").catch(() => null),
]);
const settings = settingsRes.ok ? await settingsRes.json() : {};
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
return {
engine: settings.engine ?? null,
settings,
combos: combosBody.combos ?? combosBody,
analytics,
};
}
async function restCompressionConfigure(config) {
const body = { ...config };
if (body.engine) body.engine = normalizeEngine(body.engine);
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function restSetEngine(name) {
const res = await apiFetch("/api/settings/compression", {
method: "PUT",
body: { engine: normalizeEngine(name) },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
@@ -17,6 +54,42 @@ async function mcpCall(name, args) {
return res.json();
}
async function restListCombos() {
const res = await apiFetch("/api/context/combos");
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
const body = await res.json();
return body.combos ?? body;
}
async function restComboStats(period) {
const res = await apiFetch(
`/api/context/analytics?period=${encodeURIComponent(period ?? "7d")}`
);
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
return res.json();
}
async function mcpCall(name, args, restFallback) {
const res = await apiFetch("/api/mcp/tools/call", {
method: "POST",
body: { name, arguments: args },
});
if (res.ok) return res.json();
// 404 = MCP tool surface not mounted on this build; 501 = not implemented.
// Anything else is a genuine error and we surface it.
if ((res.status === 404 || res.status === 501) && typeof restFallback === "function") {
return restFallback();
}
process.stderr.write(`Error: ${res.status}\n`);
process.exit(1);
}
async function confirm(q) {
return new Promise((resolve) => {
process.stdout.write(`${q} (yes/no) `);
@@ -26,7 +99,7 @@ async function confirm(q) {
}
export async function runCompressionStatus(opts, cmd) {
const data = await mcpCall("omniroute_compression_status", {});
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
emit(data, cmd.optsWithGlobals());
}
@@ -37,17 +110,22 @@ export async function runCompressionConfigure(opts, cmd) {
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
if (opts.languagePack) config.languagePack = opts.languagePack;
const data = await mcpCall("omniroute_compression_configure", config);
const data = await mcpCall("omniroute_compression_configure", config, () =>
restCompressionConfigure(config)
);
emit(data, cmd.optsWithGlobals());
}
export async function runCompressionEngineSet(name, opts, cmd) {
if (!VALID_ENGINES.includes(name)) {
const normalized = normalizeEngine(name);
if (!VALID_ENGINES.includes(normalized)) {
process.stderr.write(`Unknown engine: ${name}. Valid: ${VALID_ENGINES.join(", ")}\n`);
process.exit(2);
}
await mcpCall("omniroute_set_compression_engine", { engine: name });
process.stdout.write(`Engine: ${name}\n`);
await mcpCall("omniroute_set_compression_engine", { engine: normalized }, () =>
restSetEngine(normalized)
);
process.stdout.write(`Engine: ${normalized}\n`);
}
export async function runCompressionPreview(opts, cmd) {
@@ -86,22 +164,28 @@ export function registerCompression(program) {
const engine = cmp.command("engine").description(t("compression.engine.description"));
engine.command("set <name>").action(runCompressionEngineSet);
engine.command("get").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_compression_status", {});
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
process.stdout.write(`${data.engine ?? "(default)"}\n`);
});
const combos = cmp.command("combos").description(t("compression.combos.description"));
combos.command("list").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_list_compression_combos", {});
const data = await mcpCall(
"omniroute_list_compression_combos",
{},
async () => ({ combos: await restListCombos() })
);
emit(data.combos ?? data, cmd.optsWithGlobals());
});
combos
.command("stats")
.option("--period <p>", null, "7d")
.action(async (opts, cmd) => {
const data = await mcpCall("omniroute_compression_combo_stats", {
period: opts.period ?? "7d",
});
const data = await mcpCall(
"omniroute_compression_combo_stats",
{ period: opts.period ?? "7d" },
() => restComboStats(opts.period)
);
emit(data, cmd.optsWithGlobals());
});

View File

@@ -16,7 +16,7 @@ function ensureBackup(configPath) {
}
async function runConfigListCommand(opts = {}) {
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.ts");
const tools = await detectAllTools();
if (opts.json) {
@@ -42,7 +42,7 @@ async function runConfigGetCommand(toolId, opts = {}) {
printError("Tool ID required. Usage: omniroute config get <tool>");
return 1;
}
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.js");
const { detectTool } = await import("../../../src/lib/cli-helper/tool-detector.ts");
const tool = await detectTool(toolId);
if (!tool) {
printError(`Unknown tool: ${toolId}`);

View File

@@ -416,7 +416,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
// CLI tool health checks
try {
const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.js");
const { collectCliToolChecks } = await import("../../../src/lib/cli-helper/doctor/checks.ts");
const cliChecks = await collectCliToolChecks();
checks.push(...cliChecks);
} catch (err) {

View File

@@ -54,7 +54,7 @@ export async function runStatusCommand(opts = {}) {
if (isVerbose || !isJson) {
try {
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.js");
const { detectAllTools } = await import("../../../src/lib/cli-helper/tool-detector.ts");
const tools = await detectAllTools();
status.tools = tools.map((t) => ({
id: t.id,

View File

@@ -740,6 +740,8 @@
"endpointsSubtitle": "Your AI connection URLs",
"apiManager": "API Key Manager",
"apiManagerSubtitle": "Manage API keys and access",
"embeddedServices": "Embedded Services",
"embeddedServicesSubtitle": "Manage local proxy services",
"logs": "Logs",
"webhooks": "Webhooks",
"webhooksSubtitle": "Get notified of events",

View File

@@ -9,7 +9,7 @@ export interface DoctorCheckResult {
}
export async function collectCliToolChecks(): Promise<DoctorCheckResult[]> {
const { detectAllTools } = await import("../tool-detector.js");
const { detectAllTools } = await import("../tool-detector.ts");
const tools = await detectAllTools();
return tools.map((tool) => {

View File

@@ -83,7 +83,12 @@ const OPENAI_LIKE_FORMATS = new Set(["openai", "openai-responses"]);
const GEMINI_LIKE_FORMATS = new Set(["gemini", "gemini-cli"]);
function normalizeBaseUrl(baseUrl: string) {
return (baseUrl || "").trim().replace(/\/$/, "");
// Guard against a non-string baseUrl reaching .trim() / .replace() — see #2463
// where NVIDIA NIM validation surfaced as `e.startsWith is not a function`
// after the bundler renamed `baseUrl` to `e`. Any malformed providerSpecificData
// (e.g. saved as object from a UI bug) would otherwise crash mid-validation.
const value = typeof baseUrl === "string" ? baseUrl : "";
return value.trim().replace(/\/$/, "");
}
function normalizeAzureOpenAIBaseUrl(baseUrl: string) {
@@ -3585,6 +3590,76 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
return toValidationErrorResult(error);
}
},
// NVIDIA NIM (#2463) — bypass the /models probe in favor of a direct
// chat/completions probe. NVIDIA NIM's /models endpoint returns model
// catalogs that vary by region and key-tier, and some keys 404 on it,
// which the generic flow misreads. The chat probe is also a stronger
// sanity check for streaming/key correctness.
nvidia: async ({ apiKey, providerSpecificData }: any) => {
try {
const baseUrlRaw =
providerSpecificData?.baseUrl || "https://integrate.api.nvidia.com/v1/chat/completions";
const normalized = normalizeBaseUrl(baseUrlRaw);
const chatUrl = normalized.endsWith("/chat/completions")
? normalized
: `${normalized}/chat/completions`;
const modelId =
providerSpecificData?.validationModelId ||
getRegistryEntry("nvidia")?.models?.[0]?.id ||
"meta/llama-3.1-8b-instruct";
const res = await validationWrite(
chatUrl,
{
method: "POST",
headers: buildBearerHeaders(apiKey, providerSpecificData),
body: JSON.stringify({
model: modelId,
messages: [{ role: "user", content: "test" }],
max_tokens: 1,
}),
},
isLocal
);
if (res.status === 401 || res.status === 403) {
return { valid: false, error: "Invalid API key" };
}
// Any non-auth response (200, 400, 422, 429) means auth passed
return { valid: true, error: null };
} catch (error: any) {
return toValidationErrorResult(error);
}
},
// Poolside (#2723) — API has no /v1/models endpoint and returns 401 from
// unknown routes, which the generic /models probe misreads as "invalid API key".
// Validate via direct chat/completions probe with a minimal body.
poolside: async ({ apiKey, providerSpecificData }: any) => {
try {
const baseUrl = normalizeBaseUrl(
providerSpecificData?.baseUrl || "https://api.poolside.ai/v1"
);
const chatUrl = `${baseUrl.replace(/\/chat\/completions$/, "")}/chat/completions`;
const res = await validationWrite(
chatUrl,
{
method: "POST",
headers: buildBearerHeaders(apiKey, providerSpecificData),
body: JSON.stringify({
model: "poolside-model",
messages: [{ role: "user", content: "test" }],
max_tokens: 1,
}),
},
isLocal
);
if (res.status === 401 || res.status === 403) {
return { valid: false, error: "Invalid API key" };
}
// Any non-auth response (200, 400, 422, 429) means auth passed
return { valid: true, error: null };
} catch (error: any) {
return toValidationErrorResult(error);
}
},
// Xiaomi MiMo — Token Plan keys (tp-*) only work on regional endpoints
// (e.g. token-plan-sgp, token-plan-ams), not api.xiaomimimo.com.
// /v1/models works but validate via chat/completions for stronger auth check.

View File

@@ -125,20 +125,35 @@ export function parseAndValidatePublicUrl(input: string | URL) {
return url;
}
function isTrueValue(raw: unknown): boolean {
if (typeof raw !== "string") return false;
return TRUE_ENV_VALUES.has(raw.trim().toLowerCase());
}
export function arePrivateProviderUrlsAllowed() {
const value = process.env[PRIVATE_PROVIDER_URLS_ENV];
if (value && TRUE_ENV_VALUES.has(value.trim().toLowerCase())) return true;
const legacyValue = process.env["OUTBOUND_SSRF_GUARD_ENABLED"];
if (legacyValue && ["false", "0", "no", "off"].includes(legacyValue.trim().toLowerCase()))
return true;
// Check feature flag DB override — supports runtime toggle without restart
// 1) DB override takes precedence — it represents an explicit user toggle in
// the dashboard ("Allow Private Provider URLs"). This is critical for the
// Electron build (#2575) where the server is spawned with the env value
// captured at boot, so subsequent UI toggles only land in the DB and the
// env-first ordering would otherwise mask them.
try {
const dbValue = resolveFeatureFlag(PRIVATE_PROVIDER_URLS_ENV);
if (dbValue && TRUE_ENV_VALUES.has(dbValue.trim().toLowerCase())) return true;
if (isTrueValue(dbValue)) return true;
} catch {
// DB not initialized yet — fall back to env-only check
// DB not initialized yet — fall through to env-only check.
}
// 2) Explicit env opt-in (for headless/Docker users who set it before boot).
if (isTrueValue(process.env[PRIVATE_PROVIDER_URLS_ENV])) return true;
// 3) Legacy escape hatch — disabling the outbound guard implies allowing
// private URLs.
const legacyValue = process.env["OUTBOUND_SSRF_GUARD_ENABLED"];
if (
typeof legacyValue === "string" &&
["false", "0", "no", "off"].includes(legacyValue.trim().toLowerCase())
) {
return true;
}
return false;

View File

@@ -164,3 +164,87 @@ test("compression.mjs pode ser importado sem erro", async () => {
assert.equal(typeof mod.runCompressionEngineSet, "function");
assert.equal(typeof mod.runCompressionPreview, "function");
});
// #2688 — when /api/mcp/tools/call returns 404, the CLI must fall back to
// direct REST endpoints (no MCP tool surface required on minimal builds).
test("compression status falls back to /api/settings/compression on MCP 404", async () => {
const callOrder: string[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
callOrder.push(url);
if (url.includes("/api/mcp/tools/call")) {
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
}
if (url.includes("/api/settings/compression")) {
return Promise.resolve(makeResp({ engine: "caveman", enabled: true }));
}
if (url.includes("/api/context/combos")) {
return Promise.resolve(makeResp({ combos: [{ id: "c1", name: "x" }] }));
}
if (url.includes("/api/context/analytics")) {
return Promise.resolve(makeResp({ savings: 12 }));
}
return Promise.resolve(makeResp({}, 404));
}) as any;
const { runCompressionStatus } = await import("../../bin/cli/commands/compression.mjs");
await captureStdout(() => runCompressionStatus({}, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(
callOrder.some((u) => u.includes("/api/mcp/tools/call")),
"should attempt MCP first"
);
assert.ok(
callOrder.some((u) => u.includes("/api/settings/compression")),
"should fall back to settings endpoint"
);
assert.ok(
callOrder.some((u) => u.includes("/api/context/combos")),
"should fall back to combos endpoint"
);
});
test("compression engine set normalizes hybrid → stacked alias", async () => {
let captured: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((_url: string, opts: any) => {
if (opts?.body) captured = JSON.parse(opts.body);
return Promise.resolve(makeResp({ success: true }));
}) as any;
await captureStdout(async () => {
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
await runCompressionEngineSet("hybrid", {}, makeCmd() as any);
});
globalThis.fetch = origFetch;
assert.equal(captured?.arguments?.engine, "stacked");
});
test("compression engine set falls back to PUT /api/settings/compression on MCP 404", async () => {
const calls: Array<{ url: string; method?: string; body?: any }> = [];
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, opts: any) => {
calls.push({
url,
method: opts?.method,
body: opts?.body ? JSON.parse(opts.body) : undefined,
});
if (url.includes("/api/mcp/tools/call")) {
return Promise.resolve(makeResp({ error: "not mounted" }, 404));
}
return Promise.resolve(makeResp({ ok: true }));
}) as any;
await captureStdout(async () => {
const { runCompressionEngineSet } = await import("../../bin/cli/commands/compression.mjs");
await runCompressionEngineSet("rtk", {}, makeCmd() as any);
});
globalThis.fetch = origFetch;
const restCall = calls.find((c) => c.url.includes("/api/settings/compression"));
assert.ok(restCall, "should fall back to PUT /api/settings/compression");
assert.equal(restCall?.method, "PUT");
assert.equal(restCall?.body?.engine, "rtk");
});

View File

@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
// #2509 — published npm builds were failing with `Cannot find module
// 'src/lib/cli-helper/tool-detector.js'` because CLI commands imported the
// helper via the `.js` extension while only the `.ts` source ships. Lock the
// import paths to the explicit `.ts` extension that tsx resolves directly,
// preventing future regressions.
const FILES_THAT_MUST_USE_TS_EXTENSION = [
"bin/cli/commands/config.mjs",
"bin/cli/commands/status.mjs",
"bin/cli/commands/doctor.mjs",
"src/lib/cli-helper/doctor/checks.ts",
];
const DISALLOWED_PATTERNS = [
"cli-helper/tool-detector.js",
"cli-helper/doctor/checks.js",
];
for (const file of FILES_THAT_MUST_USE_TS_EXTENSION) {
test(`${file} imports cli-helper modules with .ts extension (not .js)`, () => {
const abs = join(ROOT, file);
assert.ok(existsSync(abs), `${file} should exist`);
const content = readFileSync(abs, "utf8");
for (const bad of DISALLOWED_PATTERNS) {
assert.ok(
!content.includes(bad),
`${file} must not import via "${bad}" — use the .ts extension so the published npm package (which ships only .ts source) can resolve via tsx. See #2509.`
);
}
});
}
test("tool-detector.ts is reachable from dynamic import at runtime", async () => {
const mod = await import("../../src/lib/cli-helper/tool-detector.ts");
assert.equal(typeof mod.detectAllTools, "function");
assert.equal(typeof mod.detectTool, "function");
});

View File

@@ -0,0 +1,99 @@
import test from "node:test";
import assert from "node:assert/strict";
// #2463 — NVIDIA NIM validation must not crash with `e.startsWith is not a function`
// when providerSpecificData has malformed shapes; and the validation must use a
// direct chat probe instead of the /models probe.
test("normalizeBaseUrl tolerates non-string baseUrl without throwing", async () => {
// Indirect probe — call validation entrypoint with a non-string baseUrl in PSD;
// the function should return a normal Validation result (not throw a TypeError
// such as `e.startsWith is not a function` after minification — see #2463).
const origFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({}), {
status: 400,
headers: { "content-type": "application/json" },
})) as typeof fetch;
try {
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const result = await validateProviderApiKey({
provider: "nvidia",
apiKey: "nv-test-key",
// Malformed: baseUrl is an object instead of a string. Pre-fix this would
// crash inside normalizeBaseUrl with `.trim is not a function`.
providerSpecificData: { baseUrl: { not: "a string" } as any },
});
assert.equal(typeof result, "object");
assert.equal(typeof result.valid, "boolean");
// Whether the call succeeds (with default baseUrl) or fails gracefully with
// an outbound URL guard error is fine — the contract is "no TypeError leak".
if (!result.valid && typeof result.error === "string") {
assert.ok(
!result.error.includes("startsWith"),
`error must not mention startsWith TypeError, got: ${result.error}`
);
assert.ok(
!result.error.includes("is not a function"),
`error must not mention TypeError, got: ${result.error}`
);
}
} finally {
globalThis.fetch = origFetch;
}
});
test("nvidia specialty validator returns Invalid API key on 401", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "unauthorized" }), {
status: 401,
headers: { "content-type": "application/json" },
})) as typeof fetch;
try {
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const result = await validateProviderApiKey({
provider: "nvidia",
apiKey: "nv-badkey",
providerSpecificData: {},
});
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
} finally {
globalThis.fetch = origFetch;
}
});
test("nvidia specialty validator skips /models probe entirely", async () => {
const calls: string[] = [];
const origFetch = globalThis.fetch;
globalThis.fetch = (async (url: any) => {
calls.push(String(url));
return new Response(JSON.stringify({}), {
status: 200,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
try {
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const result = await validateProviderApiKey({
provider: "nvidia",
apiKey: "nv-key",
providerSpecificData: {},
});
assert.equal(result.valid, true);
assert.ok(
calls.every((u) => !u.endsWith("/v1/models")),
`should not call /v1/models, called: ${JSON.stringify(calls)}`
);
assert.ok(
calls.some((u) => u.endsWith("/chat/completions")),
`should call /chat/completions, called: ${JSON.stringify(calls)}`
);
} finally {
globalThis.fetch = origFetch;
}
});

View File

@@ -57,11 +57,11 @@ const EXPECTED_PROVIDER_KEYS = [
"kiro",
"amazon-q",
"cursor",
"trae",
"kilocode",
"cline",
"windsurf",
"devin-cli",
"trae",
];
const EXPECTED_CONFIG_BY_PROVIDER = {

View File

@@ -0,0 +1,85 @@
import test from "node:test";
import assert from "node:assert/strict";
// #2575 — the DB feature flag override must take precedence over the
// process environment, otherwise toggling "Allow Private Provider URLs" in
// the Electron dashboard has no effect (the server was spawned with the env
// value at boot and the subsequent toggle only lands in the DB).
const KEY = "OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS";
async function withEnv<T>(value: string | undefined, fn: () => Promise<T> | T): Promise<T> {
const prev = process.env[KEY];
if (value === undefined) delete process.env[KEY];
else process.env[KEY] = value;
try {
return await fn();
} finally {
if (prev === undefined) delete process.env[KEY];
else process.env[KEY] = prev;
}
}
async function withDbOverride<T>(value: string | undefined, fn: () => Promise<T> | T): Promise<T> {
const { setFeatureFlagOverride, removeFeatureFlagOverride } = await import(
"../../src/lib/db/featureFlags.ts"
);
if (value === undefined) {
removeFeatureFlagOverride(KEY);
} else {
setFeatureFlagOverride(KEY, value);
}
try {
return await fn();
} finally {
removeFeatureFlagOverride(KEY);
}
}
test("arePrivateProviderUrlsAllowed honors DB override = 'true' even when env is 'false'", async () => {
await withEnv("false", async () => {
await withDbOverride("true", async () => {
const { arePrivateProviderUrlsAllowed } = await import(
"../../src/shared/network/outboundUrlGuard.ts"
);
assert.equal(
arePrivateProviderUrlsAllowed(),
true,
"DB toggle must override stale env value"
);
});
});
});
test("arePrivateProviderUrlsAllowed returns false when DB override = 'false' and no env truthy", async () => {
await withEnv(undefined, async () => {
await withDbOverride("false", async () => {
const { arePrivateProviderUrlsAllowed } = await import(
"../../src/shared/network/outboundUrlGuard.ts"
);
assert.equal(arePrivateProviderUrlsAllowed(), false);
});
});
});
test("arePrivateProviderUrlsAllowed honors env = 'true' when DB has no override", async () => {
await withEnv("true", async () => {
await withDbOverride(undefined, async () => {
const { arePrivateProviderUrlsAllowed } = await import(
"../../src/shared/network/outboundUrlGuard.ts"
);
assert.equal(arePrivateProviderUrlsAllowed(), true);
});
});
});
test("arePrivateProviderUrlsAllowed default (no env, no DB) returns false", async () => {
await withEnv(undefined, async () => {
await withDbOverride(undefined, async () => {
const { arePrivateProviderUrlsAllowed } = await import(
"../../src/shared/network/outboundUrlGuard.ts"
);
assert.equal(arePrivateProviderUrlsAllowed(), false);
});
});
});

View File

@@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
test("poolside registry uses /v1/chat/completions baseUrl consumed directly by default executor", () => {
const entry = REGISTRY.poolside;
assert.ok(entry, "poolside should exist in registry");
assert.equal(entry.baseUrl, "https://api.poolside.ai/v1/chat/completions");
assert.equal(entry.format, "openai");
assert.equal(entry.authType, "apikey");
assert.equal(entry.authHeader, "bearer");
});
test("poolside default executor returns the chat endpoint directly", () => {
const executor = new DefaultExecutor("poolside");
assert.equal(
executor.buildUrl("poolside-model", true, 0, {}),
"https://api.poolside.ai/v1/chat/completions"
);
});
test("poolside specialty validator returns valid=true on non-auth chat probe responses", async () => {
const calls: Array<{ url: string; status: number }> = [];
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: any, init: any) => {
void init;
const u = String(url);
calls.push({ url: u, status: 400 });
// Poolside returns 400 for minimal probe — that means auth passed
return new Response(JSON.stringify({ error: { message: "invalid model" } }), {
status: 400,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
try {
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const result = await validateProviderApiKey({
provider: "poolside",
apiKey: "sky_validkey",
providerSpecificData: {},
});
assert.equal(result.valid, true);
assert.equal(result.error, null);
// Should hit /chat/completions only — no /models probe
assert.ok(
calls.every((c) => c.url.endsWith("/chat/completions")),
`expected only /chat/completions probes, got ${JSON.stringify(calls.map((c) => c.url))}`
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("poolside specialty validator returns Invalid API key on 401", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "unauthorized" }), {
status: 401,
headers: { "content-type": "application/json" },
})) as typeof fetch;
try {
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const result = await validateProviderApiKey({
provider: "poolside",
apiKey: "sky_badkey",
providerSpecificData: {},
});
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -306,7 +306,7 @@ test("handleResponsesCore rejects invalid Responses API input that cannot be tra
}),
(error) =>
error instanceof Error &&
error.message.includes("web_search_preview tool type is not supported")
error.message.includes("file_search tool type is not supported")
);
});

View File

@@ -11,7 +11,7 @@ import {
test("resolveUniversalHandoffConfig returns disabled defaults when no config", () => {
const r = resolveUniversalHandoffConfig(null, null);
assert.strictEqual(r.enabled, false);
assert.strictEqual(r.enabled, true);
assert.strictEqual(r.trigger, "on-switch");
assert.strictEqual(r.ttlMinutes, 300);
assert.strictEqual(r.maxMessagesForSummary, 30);