From a3476298d9c4c21bb5957e3f5a1e7fc2a8ebdaf2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 26 May 2026 17:42:33 -0300 Subject: [PATCH] fix(v3.8.4-followup): 5 bug fixes from triage of 79 open issues (#2753) Integrated into release/v3.8.4 --- .source/browser.ts | 2 +- .source/server.ts | 52 ++++---- CHANGELOG.md | 8 ++ bin/cli/commands/compression.mjs | 114 +++++++++++++++--- bin/cli/commands/config.mjs | 4 +- bin/cli/commands/doctor.mjs | 2 +- bin/cli/commands/status.mjs | 2 +- src/i18n/messages/en.json | 2 + src/lib/cli-helper/doctor/checks.ts | 2 +- src/lib/providers/validation.ts | 77 +++++++++++- src/shared/network/outboundUrlGuard.ts | 35 ++++-- tests/unit/cli-compression-commands.test.ts | 84 +++++++++++++ tests/unit/cli-tool-detector-imports.test.ts | 45 +++++++ tests/unit/nvidia-nim-validator.test.ts | 99 +++++++++++++++ tests/unit/oauth-providers-config.test.ts | 2 +- .../outbound-url-guard-feature-flag.test.ts | 85 +++++++++++++ tests/unit/poolside-provider.test.ts | 79 ++++++++++++ tests/unit/responses-handler.test.ts | 2 +- tests/unit/universal-handoff.test.ts | 2 +- 19 files changed, 637 insertions(+), 61 deletions(-) create mode 100644 tests/unit/cli-tool-detector-imports.test.ts create mode 100644 tests/unit/nvidia-nim-validator.test.ts create mode 100644 tests/unit/outbound-url-guard-feature-flag.test.ts create mode 100644 tests/unit/poolside-provider.test.ts diff --git a/.source/browser.ts b/.source/browser.ts index 11f0eef61a..c3bc050c5b 100644 --- a/.source/browser.ts +++ b/.source/browser.ts @@ -7,6 +7,6 @@ const create = browser(); const browserCollections = { - docs: create.doc("docs", {"architecture/ARCHITECTURE.md": () => import("../docs/architecture/ARCHITECTURE.md?collection=docs"), "architecture/AUTHZ_GUIDE.md": () => import("../docs/architecture/AUTHZ_GUIDE.md?collection=docs"), "architecture/CODEBASE_DOCUMENTATION.md": () => import("../docs/architecture/CODEBASE_DOCUMENTATION.md?collection=docs"), "architecture/REPOSITORY_MAP.md": () => import("../docs/architecture/REPOSITORY_MAP.md?collection=docs"), "architecture/RESILIENCE_GUIDE.md": () => import("../docs/architecture/RESILIENCE_GUIDE.md?collection=docs"), "compression/COMPRESSION_ENGINES.md": () => import("../docs/compression/COMPRESSION_ENGINES.md?collection=docs"), "compression/COMPRESSION_GUIDE.md": () => import("../docs/compression/COMPRESSION_GUIDE.md?collection=docs"), "compression/COMPRESSION_LANGUAGE_PACKS.md": () => import("../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"), "compression/COMPRESSION_RULES_FORMAT.md": () => import("../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"), "compression/RTK_COMPRESSION.md": () => import("../docs/compression/RTK_COMPRESSION.md?collection=docs"), "guides/DOCKER_GUIDE.md": () => import("../docs/guides/DOCKER_GUIDE.md?collection=docs"), "guides/ELECTRON_GUIDE.md": () => import("../docs/guides/ELECTRON_GUIDE.md?collection=docs"), "guides/FEATURES.md": () => import("../docs/guides/FEATURES.md?collection=docs"), "guides/I18N.md": () => import("../docs/guides/I18N.md?collection=docs"), "guides/KIRO_SETUP.md": () => import("../docs/guides/KIRO_SETUP.md?collection=docs"), "guides/PWA_GUIDE.md": () => import("../docs/guides/PWA_GUIDE.md?collection=docs"), "guides/SETUP_GUIDE.md": () => import("../docs/guides/SETUP_GUIDE.md?collection=docs"), "guides/TERMUX_GUIDE.md": () => import("../docs/guides/TERMUX_GUIDE.md?collection=docs"), "guides/TROUBLESHOOTING.md": () => import("../docs/guides/TROUBLESHOOTING.md?collection=docs"), "guides/UNINSTALL.md": () => import("../docs/guides/UNINSTALL.md?collection=docs"), "guides/USER_GUIDE.md": () => import("../docs/guides/USER_GUIDE.md?collection=docs"), "frameworks/A2A-SERVER.md": () => import("../docs/frameworks/A2A-SERVER.md?collection=docs"), "frameworks/AGENT_PROTOCOLS_GUIDE.md": () => import("../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"), "frameworks/CLOUD_AGENT.md": () => import("../docs/frameworks/CLOUD_AGENT.md?collection=docs"), "frameworks/EMBEDDED-SERVICES.md": () => import("../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"), "frameworks/EVALS.md": () => import("../docs/frameworks/EVALS.md?collection=docs"), "frameworks/GAMIFICATION.md": () => import("../docs/frameworks/GAMIFICATION.md?collection=docs"), "frameworks/MCP-SERVER.md": () => import("../docs/frameworks/MCP-SERVER.md?collection=docs"), "frameworks/MEMORY.md": () => import("../docs/frameworks/MEMORY.md?collection=docs"), "frameworks/OPENCODE.md": () => import("../docs/frameworks/OPENCODE.md?collection=docs"), "frameworks/SKILLS.md": () => import("../docs/frameworks/SKILLS.md?collection=docs"), "frameworks/WEBHOOKS.md": () => import("../docs/frameworks/WEBHOOKS.md?collection=docs"), "ops/COVERAGE_PLAN.md": () => import("../docs/ops/COVERAGE_PLAN.md?collection=docs"), "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": () => import("../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"), "ops/FLY_IO_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"), "ops/PROXY_GUIDE.md": () => import("../docs/ops/PROXY_GUIDE.md?collection=docs"), "ops/RELEASE_CHECKLIST.md": () => import("../docs/ops/RELEASE_CHECKLIST.md?collection=docs"), "ops/SQLITE_RUNTIME.md": () => import("../docs/ops/SQLITE_RUNTIME.md?collection=docs"), "ops/TUNNELS_GUIDE.md": () => import("../docs/ops/TUNNELS_GUIDE.md?collection=docs"), "ops/VM_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/VM_DEPLOYMENT_GUIDE.md?collection=docs"), "reference/API_REFERENCE.md": () => import("../docs/reference/API_REFERENCE.md?collection=docs"), "reference/CLI-TOOLS.md": () => import("../docs/reference/CLI-TOOLS.md?collection=docs"), "reference/ENVIRONMENT.md": () => import("../docs/reference/ENVIRONMENT.md?collection=docs"), "reference/FREE_TIERS.md": () => import("../docs/reference/FREE_TIERS.md?collection=docs"), "reference/PROVIDER_REFERENCE.md": () => import("../docs/reference/PROVIDER_REFERENCE.md?collection=docs"), "routing/AUTO-COMBO.md": () => import("../docs/routing/AUTO-COMBO.md?collection=docs"), "routing/REASONING_REPLAY.md": () => import("../docs/routing/REASONING_REPLAY.md?collection=docs"), "security/CLI_TOKEN.md": () => import("../docs/security/CLI_TOKEN.md?collection=docs"), "security/CLI_TOKEN_AUTH.md": () => import("../docs/security/CLI_TOKEN_AUTH.md?collection=docs"), "security/COMPLIANCE.md": () => import("../docs/security/COMPLIANCE.md?collection=docs"), "security/ERROR_SANITIZATION.md": () => import("../docs/security/ERROR_SANITIZATION.md?collection=docs"), "security/GUARDRAILS.md": () => import("../docs/security/GUARDRAILS.md?collection=docs"), "security/PUBLIC_CREDS.md": () => import("../docs/security/PUBLIC_CREDS.md?collection=docs"), "security/ROUTE_GUARD_TIERS.md": () => import("../docs/security/ROUTE_GUARD_TIERS.md?collection=docs"), "security/STEALTH_GUIDE.md": () => import("../docs/security/STEALTH_GUIDE.md?collection=docs"), }), + docs: create.doc("docs", {"architecture/ARCHITECTURE.md": () => import("../docs/architecture/ARCHITECTURE.md?collection=docs"), "architecture/AUTHZ_GUIDE.md": () => import("../docs/architecture/AUTHZ_GUIDE.md?collection=docs"), "architecture/CODEBASE_DOCUMENTATION.md": () => import("../docs/architecture/CODEBASE_DOCUMENTATION.md?collection=docs"), "architecture/REPOSITORY_MAP.md": () => import("../docs/architecture/REPOSITORY_MAP.md?collection=docs"), "architecture/RESILIENCE_GUIDE.md": () => import("../docs/architecture/RESILIENCE_GUIDE.md?collection=docs"), "compression/COMPRESSION_ENGINES.md": () => import("../docs/compression/COMPRESSION_ENGINES.md?collection=docs"), "compression/COMPRESSION_GUIDE.md": () => import("../docs/compression/COMPRESSION_GUIDE.md?collection=docs"), "compression/COMPRESSION_LANGUAGE_PACKS.md": () => import("../docs/compression/COMPRESSION_LANGUAGE_PACKS.md?collection=docs"), "compression/COMPRESSION_RULES_FORMAT.md": () => import("../docs/compression/COMPRESSION_RULES_FORMAT.md?collection=docs"), "compression/RTK_COMPRESSION.md": () => import("../docs/compression/RTK_COMPRESSION.md?collection=docs"), "frameworks/A2A-SERVER.md": () => import("../docs/frameworks/A2A-SERVER.md?collection=docs"), "frameworks/AGENT_PROTOCOLS_GUIDE.md": () => import("../docs/frameworks/AGENT_PROTOCOLS_GUIDE.md?collection=docs"), "frameworks/CLOUD_AGENT.md": () => import("../docs/frameworks/CLOUD_AGENT.md?collection=docs"), "frameworks/EMBEDDED-SERVICES.md": () => import("../docs/frameworks/EMBEDDED-SERVICES.md?collection=docs"), "frameworks/EVALS.md": () => import("../docs/frameworks/EVALS.md?collection=docs"), "frameworks/GAMIFICATION.md": () => import("../docs/frameworks/GAMIFICATION.md?collection=docs"), "frameworks/MCP-SERVER.md": () => import("../docs/frameworks/MCP-SERVER.md?collection=docs"), "frameworks/MEMORY.md": () => import("../docs/frameworks/MEMORY.md?collection=docs"), "frameworks/OPENCODE.md": () => import("../docs/frameworks/OPENCODE.md?collection=docs"), "frameworks/SKILLS.md": () => import("../docs/frameworks/SKILLS.md?collection=docs"), "frameworks/WEBHOOKS.md": () => import("../docs/frameworks/WEBHOOKS.md?collection=docs"), "guides/DOCKER_GUIDE.md": () => import("../docs/guides/DOCKER_GUIDE.md?collection=docs"), "guides/ELECTRON_GUIDE.md": () => import("../docs/guides/ELECTRON_GUIDE.md?collection=docs"), "guides/FEATURES.md": () => import("../docs/guides/FEATURES.md?collection=docs"), "guides/I18N.md": () => import("../docs/guides/I18N.md?collection=docs"), "guides/KIRO_SETUP.md": () => import("../docs/guides/KIRO_SETUP.md?collection=docs"), "guides/PWA_GUIDE.md": () => import("../docs/guides/PWA_GUIDE.md?collection=docs"), "guides/SETUP_GUIDE.md": () => import("../docs/guides/SETUP_GUIDE.md?collection=docs"), "guides/TERMUX_GUIDE.md": () => import("../docs/guides/TERMUX_GUIDE.md?collection=docs"), "guides/TROUBLESHOOTING.md": () => import("../docs/guides/TROUBLESHOOTING.md?collection=docs"), "guides/UNINSTALL.md": () => import("../docs/guides/UNINSTALL.md?collection=docs"), "guides/USER_GUIDE.md": () => import("../docs/guides/USER_GUIDE.md?collection=docs"), "ops/COVERAGE_PLAN.md": () => import("../docs/ops/COVERAGE_PLAN.md?collection=docs"), "ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md": () => import("../docs/ops/E2E_DASHBOARD_SHAKEDOWN_v3.8.0.md?collection=docs"), "ops/FLY_IO_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md?collection=docs"), "ops/PROXY_GUIDE.md": () => import("../docs/ops/PROXY_GUIDE.md?collection=docs"), "ops/RELEASE_CHECKLIST.md": () => import("../docs/ops/RELEASE_CHECKLIST.md?collection=docs"), "ops/SQLITE_RUNTIME.md": () => import("../docs/ops/SQLITE_RUNTIME.md?collection=docs"), "ops/TUNNELS_GUIDE.md": () => import("../docs/ops/TUNNELS_GUIDE.md?collection=docs"), "ops/VM_DEPLOYMENT_GUIDE.md": () => import("../docs/ops/VM_DEPLOYMENT_GUIDE.md?collection=docs"), "reference/API_REFERENCE.md": () => import("../docs/reference/API_REFERENCE.md?collection=docs"), "reference/CLI-TOOLS.md": () => import("../docs/reference/CLI-TOOLS.md?collection=docs"), "reference/ENVIRONMENT.md": () => import("../docs/reference/ENVIRONMENT.md?collection=docs"), "reference/FREE_TIERS.md": () => import("../docs/reference/FREE_TIERS.md?collection=docs"), "reference/PROVIDER_REFERENCE.md": () => import("../docs/reference/PROVIDER_REFERENCE.md?collection=docs"), "routing/AUTO-COMBO.md": () => import("../docs/routing/AUTO-COMBO.md?collection=docs"), "routing/REASONING_REPLAY.md": () => import("../docs/routing/REASONING_REPLAY.md?collection=docs"), "security/CLI_TOKEN.md": () => import("../docs/security/CLI_TOKEN.md?collection=docs"), "security/CLI_TOKEN_AUTH.md": () => import("../docs/security/CLI_TOKEN_AUTH.md?collection=docs"), "security/COMPLIANCE.md": () => import("../docs/security/COMPLIANCE.md?collection=docs"), "security/ERROR_SANITIZATION.md": () => import("../docs/security/ERROR_SANITIZATION.md?collection=docs"), "security/GUARDRAILS.md": () => import("../docs/security/GUARDRAILS.md?collection=docs"), "security/PUBLIC_CREDS.md": () => import("../docs/security/PUBLIC_CREDS.md?collection=docs"), "security/ROUTE_GUARD_TIERS.md": () => import("../docs/security/ROUTE_GUARD_TIERS.md?collection=docs"), "security/STEALTH_GUIDE.md": () => import("../docs/security/STEALTH_GUIDE.md?collection=docs"), }), }; export default browserCollections; \ No newline at end of file diff --git a/.source/server.ts b/.source/server.ts index fd7a6a93e8..ea427fcb51 100644 --- a/.source/server.ts +++ b/.source/server.ts @@ -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({"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, }); \ No newline at end of file +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, }); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb773aed0..2b2fbb9b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bin/cli/commands/compression.mjs b/bin/cli/commands/compression.mjs index 81f44cc71b..fe0d5ceb1c 100644 --- a/bin/cli/commands/compression.mjs +++ b/bin/cli/commands/compression.mjs @@ -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 ").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

", 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()); }); diff --git a/bin/cli/commands/config.mjs b/bin/cli/commands/config.mjs index da40cb9f3b..348d59969f 100644 --- a/bin/cli/commands/config.mjs +++ b/bin/cli/commands/config.mjs @@ -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 "); 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}`); diff --git a/bin/cli/commands/doctor.mjs b/bin/cli/commands/doctor.mjs index 7a864d6e2d..08f68ab84f 100644 --- a/bin/cli/commands/doctor.mjs +++ b/bin/cli/commands/doctor.mjs @@ -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) { diff --git a/bin/cli/commands/status.mjs b/bin/cli/commands/status.mjs index 263b69b97d..4c3d8b7c1f 100644 --- a/bin/cli/commands/status.mjs +++ b/bin/cli/commands/status.mjs @@ -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, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index a7ce474e9e..efe2fbe824 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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", diff --git a/src/lib/cli-helper/doctor/checks.ts b/src/lib/cli-helper/doctor/checks.ts index 353d0e6026..21f3b2dcb5 100644 --- a/src/lib/cli-helper/doctor/checks.ts +++ b/src/lib/cli-helper/doctor/checks.ts @@ -9,7 +9,7 @@ export interface DoctorCheckResult { } export async function collectCliToolChecks(): Promise { - const { detectAllTools } = await import("../tool-detector.js"); + const { detectAllTools } = await import("../tool-detector.ts"); const tools = await detectAllTools(); return tools.map((tool) => { diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index cce285d168..4c1a0af11b 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -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. diff --git a/src/shared/network/outboundUrlGuard.ts b/src/shared/network/outboundUrlGuard.ts index 20fa537822..6df71ee686 100644 --- a/src/shared/network/outboundUrlGuard.ts +++ b/src/shared/network/outboundUrlGuard.ts @@ -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; diff --git a/tests/unit/cli-compression-commands.test.ts b/tests/unit/cli-compression-commands.test.ts index e5c80b0ef1..89eb47299b 100644 --- a/tests/unit/cli-compression-commands.test.ts +++ b/tests/unit/cli-compression-commands.test.ts @@ -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"); +}); diff --git a/tests/unit/cli-tool-detector-imports.test.ts b/tests/unit/cli-tool-detector-imports.test.ts new file mode 100644 index 0000000000..a8d1a9898c --- /dev/null +++ b/tests/unit/cli-tool-detector-imports.test.ts @@ -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"); +}); diff --git a/tests/unit/nvidia-nim-validator.test.ts b/tests/unit/nvidia-nim-validator.test.ts new file mode 100644 index 0000000000..1150e26aa7 --- /dev/null +++ b/tests/unit/nvidia-nim-validator.test.ts @@ -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; + } +}); diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts index 3af87200cd..c277c5aea3 100644 --- a/tests/unit/oauth-providers-config.test.ts +++ b/tests/unit/oauth-providers-config.test.ts @@ -57,11 +57,11 @@ const EXPECTED_PROVIDER_KEYS = [ "kiro", "amazon-q", "cursor", + "trae", "kilocode", "cline", "windsurf", "devin-cli", - "trae", ]; const EXPECTED_CONFIG_BY_PROVIDER = { diff --git a/tests/unit/outbound-url-guard-feature-flag.test.ts b/tests/unit/outbound-url-guard-feature-flag.test.ts new file mode 100644 index 0000000000..ca594f3a94 --- /dev/null +++ b/tests/unit/outbound-url-guard-feature-flag.test.ts @@ -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(value: string | undefined, fn: () => Promise | T): Promise { + 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(value: string | undefined, fn: () => Promise | T): Promise { + 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); + }); + }); +}); diff --git a/tests/unit/poolside-provider.test.ts b/tests/unit/poolside-provider.test.ts new file mode 100644 index 0000000000..13baa4130e --- /dev/null +++ b/tests/unit/poolside-provider.test.ts @@ -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; + } +}); diff --git a/tests/unit/responses-handler.test.ts b/tests/unit/responses-handler.test.ts index 4a23a84638..3f05936310 100644 --- a/tests/unit/responses-handler.test.ts +++ b/tests/unit/responses-handler.test.ts @@ -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") ); }); diff --git a/tests/unit/universal-handoff.test.ts b/tests/unit/universal-handoff.test.ts index 8fe1f14706..ddfeb508f6 100644 --- a/tests/unit/universal-handoff.test.ts +++ b/tests/unit/universal-handoff.test.ts @@ -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);