Skip to content

fix(mcp): fall back to node:sqlite when better-sqlite3 binding is missing - #3887

Merged
diegosouzapw merged 14 commits into
diegosouzapw:release/v3.8.26from
megamen32:fix/mcp-audit-sqlite-fallback
Jun 15, 2026
Merged

diegosouzapw merged 14 commits into
diegosouzapw:release/v3.8.26from
megamen32:fix/mcp-audit-sqlite-fallback

Conversation

@megamen32

Copy link
Copy Markdown
Contributor

Summary

The MCP audit logger in open-sse/mcp-server/audit.ts was the only place in the SSE sidecar that imported better-sqlite3 without a fallback. When the bundled native binary failed to land in dist/node_modules/better-sqlite3/build/Release/ (which happens for some global npm installs and minimal Docker images where the postinstall binary-copy step does not run for whatever reason), the audit logger silently disabled itself and spammed the logs with:

[MCP Audit] Failed to connect to database: Could not locate the bindings file. Tried: .../better_sqlite3.node

The main app already supports a node:sqlite fallback via src/lib/db/adapters/driverFactory.ts:152-187. This PR mirrors that pattern for the MCP audit module so the same scenario no longer kills audit logging.

Repro

  1. Install the published tarball in a way that strips the better-sqlite3 native binary from dist/node_modules/ (e.g. npm i -g omniroute on a fresh image with build tools but an unusual Node ABI).
  2. Start the server.
  3. Invoke any MCP tool.
  4. Observe the [MCP Audit] error and the audit mcp_tool_audit table is empty.

Fix

  • Wrap the better-sqlite3 import in a try/catch.
  • If the error indicates a missing or ABI-mismatched binding, fall back to node:sqlite (Node 22.5+).
  • Wrap the node:sqlite DatabaseSync in a thin adapter that exposes the same AuditDatabase interface (prepare/pragma/close/open) used by the existing code, so closeAuditDb and the WAL checkpoint path keep working without further changes.
  • Only treat the native import as failed when the error message matches known binding-ABI failures ("Could not locate the bindings file", "NODE_MODULE_VERSION", "ERR_DLOPEN_FAILED"). Real errors (corrupt DB, permissions) still surface to the operator.

Test

Added falls back to node:sqlite when better-sqlite3 binding is missing to open-sse/mcp-server/__tests__/audit.test.ts that:

  • mocks better-sqlite3 to throw the exact "Could not locate the bindings file" error from the published package,
  • mocks node:sqlite with a DatabaseSync stub,
  • exercises the full logToolCall → closeAuditDb cycle,
  • asserts that exec("PRAGMA wal_checkpoint(TRUNCATE)") is called on the node:sqlite handle.

Notes

  • Targets release/v3.8.25 (the version on the affected installs). Easy to cherry-pick to main / release/v3.8.26 if needed.
  • The same fallback would also be useful in open-sse/utils/cursorVersionDetector.ts:49 and src/lib/copilot/codegraphKnowledge.ts:107, but those paths are not in the bug report — happy to send a follow-up if you want a single PR that covers all three.

diegosouzapw and others added 11 commits June 14, 2026 21:32
…nc (r5)

main fast-forwarded to release/v3.8.25 (diegosouzapw#3863): unblocked Build+Docker via
diegosouzapw#3864, plus diegosouzapw#3837 (mimocode proxy) and diegosouzapw#3862 (trivy bump). This marker
re-opens the umbrella PR for further v3.8.25 work. No version bump.
…s + combos editor + menu + WS default-on (diegosouzapw#3860)

Integrated into release/v3.8.25 — feat(compression-ui): unified compression configuration UI (Compression Hub + per-engine Lite/Aggressive/Ultra pages + combos editor + sidebar entry + live-WS default-on). File-size re-baselined for sidebarVisibility.ts/chatCore.ts growth; orphan ws test relocated to a collected path.
…ributors

Audited every commit since v3.8.24 and filled the gaps the [3.8.25] section
was missing: a New Features section (compression engines + Compression Studios
diegosouzapw#3848, compression UI diegosouzapw#3860, injection-guard diegosouzapw#3857, kiro discovery diegosouzapw#3836, Veo
diegosouzapw#3839, mimocode proxy diegosouzapw#3837, Arena ELO flag diegosouzapw#3821), 9 more Fixed entries
(diegosouzapw#3811/diegosouzapw#3807/diegosouzapw#3759/diegosouzapw#3849/diegosouzapw#3838/diegosouzapw#3835/diegosouzapw#3814/diegosouzapw#3820/diegosouzapw#3819), a Security section
(CCR IDOR diegosouzapw#3859, supply-chain diegosouzapw#3824), and an Internal/Quality section. Every
contributor and issue reporter is now credited.
Re-adds CHANGELOG.md (a prior server-side commit accidentally dropped it) with
the complete, audited [3.8.25] section: New Features, the full Fixed list,
Security & Hardening, and Internal/Quality — every contributor and issue
reporter credited.
…, document OMNIROUTE_MAX_PENDING_MIGRATIONS, green the unit suite

Release-gate reconciliation for v3.8.25:
- CHANGELOG: dated 2026-06-14, linked diegosouzapw#3826, rolled up file-size re-baselines (diegosouzapw#3823/diegosouzapw#3833),
  recorded the test-greening; re-synced all 41 i18n CHANGELOG mirrors.
- Documented OMNIROUTE_MAX_PENDING_MIGRATIONS (diegosouzapw#3416) in .env.example + ENVIRONMENT.md.
- Greened the unit suite (was merged red on 4 CI shards): aligned 10 stale tests to this
  cycle's intended behavior (diegosouzapw#3838/diegosouzapw#3822/diegosouzapw#3501/SOCKS5/Vertex-Express/Antigravity) and the
  same-provider 503 fall-through test; de-flaked the compression benchmark reproducibility
  and ServiceSupervisor crash tests. No production code changed.
…rkflow token permissions

The Security tab held 155 open alerts, ALL from the advisory OpenSSF Scorecard tool
(diegosouzapw#3824) — supply-chain/posture scores, not code vulnerabilities — which drowned out
real CodeQL findings.

- scorecard.yml: stop uploading SARIF to the code-scanning tab (drop the upload-sarif
  step + the now-unused security-events: write). The run still produces the OpenSSF
  badge (publish_results) and a downloadable SARIF artifact.
- TokenPermissions hardening (the high-severity, genuinely-valuable subset): set each
  workflow's top-level token to read-only and grant the exact writes at the job level
  that needs them — npm-publish (id-token/packages on publish jobs), docker-publish
  (packages on build), electron-release (contents on build/release, id-token/packages
  on publish-npm), build-fork (packages on build), claude (empty top-level; job grants
  its own). The 155 existing alerts were dismissed.

Not adopting repo-wide SHA-pinning (143 PinnedDependencies advisories) — declined.
…s cycle's behavior

These were red on the CI Integration job (pre-existing). No production code changed:
- integration-wiring: the combos page no longer renders a per-page EmailPrivacyToggle
  (diegosouzapw#3822 consolidated it into Settings → Appearance); the provider-detail test-result
  masking and upstream-proxy copy moved to decomposed components (diegosouzapw#3501
  BatchTestResultsModal / UpstreamProxyCard) — assertions now read the owning files.
- api-routes-critical: SOCKS5 is now enabled by default (opt-out), so the disabled-
  rejection test must set ENABLE_SOCKS5_PROXY=false explicitly (an unset env now means
  enabled).

(The ~32 live-Gemini integration tests are gated on OMNIROUTE_API_KEY and skip in CI;
they only 'fail' locally when that key is present without a running server.)
…sing

The MCP audit logger was the only place in open-sse that imported
better-sqlite3 without a fallback. When the bundled native binary failed
to land in dist/node_modules/better-sqlite3/build/Release/ (which
happens for some global npm installs and Docker images), the audit
logger silently disabled itself and spammed the log with:

  [MCP Audit] Failed to connect to database: Could not locate the
  bindings file. Tried: .../better_sqlite3.node

The main app already supports a node:sqlite fallback via
src/lib/db/adapters/driverFactory.ts. Mirror that pattern here so
audit logging keeps working — node:sqlite ships in Node 22.5+ which
already covers all supported runtimes.

Only treat the native import as failed when the error message matches
known binding-ABI failures; corrupt DB / permission errors still
surface to the operator.
@megamen32
megamen32 requested a review from diegosouzapw as a code owner June 15, 2026 10:31

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a transparent fallback to Node's built-in node:sqlite module (Node 22.5+) for the MCP server's audit logger when the native better-sqlite3 binding is missing or fails to load. While the fallback logic and accompanying unit tests are well-structured, two key issues were identified: the isBindingIssue check is too narrow and should be expanded to cover other common ABI mismatch errors, and the DatabaseSync class lacks an open property, meaning the open state must be tracked locally within the adapter to avoid violating the AuditDatabase interface contract.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread open-sse/mcp-server/audit.ts Outdated
Comment on lines +235 to +238
const isBindingIssue =
nativeMessage.includes("Could not locate the bindings file") ||
nativeMessage.includes("NODE_MODULE_VERSION") ||
nativeMessage.includes("ERR_DLOPEN_FAILED");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The error messages for native binding mismatches can also include phrases like "was compiled against a different Node.js version" or "Module did not self-register" (as defined in scripts/dev/ensure-native-sqlite.mjs). If these occur, the fallback to node:sqlite will not be triggered.

We should expand the isBindingIssue check to cover all common ABI mismatch and binding failure messages to ensure the fallback is robust.

      const isBindingIssue = 
        nativeMessage.includes("Could not locate the bindings file") ||
        nativeMessage.includes("NODE_MODULE_VERSION") ||
        nativeMessage.includes("was compiled against a different Node.js version") ||
        nativeMessage.includes("Module did not self-register") ||
        nativeMessage.includes("ERR_DLOPEN_FAILED");

Comment on lines +27 to 62
interface NodeSqliteDatabase {
prepare: (sql: string) => {
run: (...params: unknown[]) => { changes: number | bigint; lastInsertRowid: number | bigint };
get: (...params: unknown[]) => unknown;
all: (...params: unknown[]) => unknown[];
};
exec: (sql: string) => void;
close: () => void;
open: boolean;
}

function createNodeSqliteAuditAdapter(db: NodeSqliteDatabase): AuditDatabase {
return {
driver: "node:sqlite",
open: db.open,
prepare<TRow = unknown>(sql: string) {
const stmt = db.prepare(sql);
return {
get: (...params: unknown[]) => stmt.get(...params) as TRow | undefined,
all: (...params: unknown[]) => stmt.all(...params) as TRow[],
run: (...params: unknown[]) => stmt.run(...params),
};
},
pragma(pragmaSql: string) {
// node:sqlite has no .pragma() helper — route through .exec() for
// statement-shaped PRAGMAs (e.g. "wal_checkpoint(TRUNCATE)").
try {
db.exec(`PRAGMA ${pragmaSql}`);
return null;
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
},
close: () => db.close(),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The native DatabaseSync class from node:sqlite does not have an open property. Declaring open: boolean on NodeSqliteDatabase and assigning open: db.open results in undefined at runtime, which violates the AuditDatabase interface contract and fails to track the open/closed state of the database adapter.

Instead, track the open state locally within the adapter, similar to how it is done in src/lib/db/adapters/nodeSqliteAdapter.ts.

interface NodeSqliteDatabase {
  prepare: (sql: string) => {
    run: (...params: unknown[]) => { changes: number | bigint; lastInsertRowid: number | bigint };
    get: (...params: unknown[]) => unknown;
    all: (...params: unknown[]) => unknown[];
  };
  exec: (sql: string) => void;
  close: () => void;
}

function createNodeSqliteAuditAdapter(db: NodeSqliteDatabase): AuditDatabase {
  let isOpen = true;
  return {
    driver: "node:sqlite",
    get open() {
      return isOpen;
    },
    prepare<TRow = unknown>(sql: string) {
      const stmt = db.prepare(sql);
      return {
        get: (...params: unknown[]) => stmt.get(...params) as TRow | undefined,
        all: (...params: unknown[]) => stmt.all(...params) as TRow[],
        run: (...params: unknown[]) => stmt.run(...params),
      };
    },
    pragma(pragmaSql: string) {
      // node:sqlite has no .pragma() helper — route through .exec() for
      // statement-shaped PRAGMAs (e.g. "wal_checkpoint(TRUNCATE)").
      try {
        db.exec("PRAGMA " + pragmaSql);
        return null;
      } catch (err) {
        return err instanceof Error ? err.message : String(err);
      }
    },
    close: () => {
      try {
        db.close();
      } finally {
        isOpen = false;
      }
    },
  };
}

… open state

Round 2 on diegosouzapw#3887, addressing @gemini-code-assist's two review points:

1. Replace the inline 3-pattern `isBindingIssue` with the canonical
   `isNativeSqliteLoadError` helper from `src/lib/db/core.ts`. The
   main app already covers every binding/ABI failure mode there
   (MODULE_NOT_FOUND, ERR_DLOPEN_FAILED, 'Module did not self-register',
   'Cannot find module better-sqlite3', 'was compiled against a
   different Node.js version', and 'Could not locate the bindings
   file'). Reusing it keeps the two paths from drifting.

2. `node:sqlite` DatabaseSync does not expose a boolean `open` —
   `open`/`close` are prototype methods, and the only state
   surface is the `isOpen` getter. Track open state in a closure
   inside the adapter and expose `open` as a getter. `close()`
   is now idempotent (no-op if already closed) so a stray double
   close on the same handle can't throw. The cache is still cleared
   by the existing `closeAuditDb()` so a re-open always builds a
   fresh adapter.

Test mock no longer carries the bogus `open: true` field, and an
extra assertion verifies that `closeAuditDb()` returns false the
second time (cache cleared, handle not re-closed).
@megamen32

Copy link
Copy Markdown
Contributor Author

Thanks @gemini-code-assist — both points were correct, fixed in fc6a28c:

1. isBindingIssue too narrow — replaced the inline 3-pattern check with the canonical isNativeSqliteLoadError from src/lib/db/core.ts. That helper already covers every binding/ABI failure mode the rest of the codebase knows about:

  • MODULE_NOT_FOUND / ERR_DLOPEN_FAILED error codes
  • "Module did not self-register"
  • "Cannot find module 'better-sqlite3'"
  • "was compiled against a different Node.js version" (V8 standard dlopen message)
  • "Could not locate the bindings file"

Reusing it keeps the MCP audit path from drifting out of sync with the main app's driver factory. Real errors (corrupt db, permission denied) still bubble up to the operator unchanged.

2. DatabaseSync.open does not exist — you're right, open/close on node:sqlite DatabaseSync are prototype methods, and the only boolean state surface is the isOpen getter. The adapter now tracks open state in a local closure and exposes open as a getter, matching the AuditDatabase contract. close() is idempotent so a stray double-close on the same handle can't throw. closeAuditDb() still clears the cache, so any re-open always builds a fresh adapter.

Test updated: removed the now-stale open: true field from the node:sqlite mock, and added an assertion that closeAuditDb() returns false on a second call (cache cleared, mockNodeDb.close not called again).

Both files syntax-checked with node --check. Same branch, same PR, no extra commits on your side. Ready for another pass.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a transparent fallback to Node's built-in node:sqlite module when the native better-sqlite3 bindings are missing or fail to load, ensuring the MCP audit logger remains functional in environments like Docker or global installations. It includes an adapter to align node:sqlite with the existing AuditDatabase interface and adds corresponding unit tests. The feedback suggests letting errors propagate directly from the adapted pragma method to avoid silently swallowing execution failures, and casting the dynamic import path of node:sqlite to never to prevent static analysis or TypeScript compilation issues in older environments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +61 to +70
pragma(pragmaSql: string) {
// node:sqlite has no .pragma() helper — route through .exec() for
// statement-shaped PRAGMAs (e.g. "wal_checkpoint(TRUNCATE)").
try {
db.exec(`PRAGMA ${pragmaSql}`);
return null;
} catch (err) {
return err instanceof Error ? err.message : String(err);
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pragma method in createNodeSqliteAuditAdapter catches errors internally and returns them as strings instead of letting them propagate. This deviates from the expected behavior of pragma (and the behavior of better-sqlite3), which throws on execution failure. Because of this, the try/catch block in closeAuditDb will not catch any WAL checkpoint failures, and the warning log will be silently skipped. We should let the error propagate directly.

    pragma(pragmaSql: string) {
      // node:sqlite has no .pragma() helper — route through .exec() for
      // statement-shaped PRAGMAs (e.g. "wal_checkpoint(TRUNCATE)").
      db.exec(`PRAGMA ${pragmaSql}`);
      return null;
    },

return null;
}
try {
const { DatabaseSync } = (await import("node:sqlite")) as {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a static import("node:sqlite") can cause TypeScript compilation errors if the build environment uses an older version of @types/node that does not yet declare node:sqlite. It can also cause issues with some bundlers that attempt to statically resolve the module. To prevent this and maintain consistency with how node:sqlite is imported elsewhere in the codebase (e.g., in nodeSqliteAdapter.ts), we should cast the import path as never or string to bypass static analysis.

Suggested change
const { DatabaseSync } = (await import("node:sqlite")) as {
const { DatabaseSync } = (await import("node:sqlite" as never)) as {

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.25 to release/v3.8.26 June 15, 2026 15:26
diegosouzapw and others added 2 commits June 15, 2026 12:30
…dit-sqlite-fallback

# Conflicts:
#	.github/workflows/npm-publish.yml
#	config/quality/file-size-baseline.json
… test

The node:sqlite fallback warning referenced `nativeMessage`, but it was
declared with `const` inside the `if (!isNativeSqliteLoadError)` bail-out
block (which returns) — so it was out of scope in the fallback path
(TS2304 / runtime ReferenceError). Fast Quality Gates does not run
typecheck:core, so it slipped past green CI. Hoisted the declaration to
the top of the catch block.

Also repaired the fallback regression test: a `vi.doMock` factory that
itself throws is reported by vitest as a mock-setup error and never
reaches the code under test, so the test could not actually exercise the
node:sqlite fallback. Switched to a throwing `default` constructor
(matches reality: `new Database()` throws when the prebuilt .node is
absent). The test now genuinely guards the scope fix — it fails without it.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks, @megamen32! 🙏 A node:sqlite fallback for the MCP audit logger is exactly the right safety net for global-install / Docker scenarios where the better-sqlite3 prebuilt .node doesn't resolve — and reusing the canonical isNativeSqliteLoadError helper keeps the failure-mode detection consistent with the rest of the codebase.

Two adjustments while integrating (co-authored, on your branch):

  1. Scope fix: nativeMessage was declared with const inside the if (!isNativeSqliteLoadError) bail-out block but referenced in the fallback warning below it → out of scope (TS2304 / runtime ReferenceError). Hoisted it to the top of the catch. Fast Quality Gates doesn't run typecheck, so this had slipped past green CI.
  2. Test repair: a vi.doMock factory that itself throws is reported by vitest as a mock-setup error and never reaches your code, so the fallback test wasn't actually exercising the path. Switched to a throwing default constructor — the test now genuinely guards the fix (3/3 green).

Also rebased onto release/v3.8.26 (the base was the now-released v3.8.25). Merging for the next release. 🚀

@diegosouzapw
diegosouzapw merged commit 05862fa into diegosouzapw:release/v3.8.26 Jun 15, 2026
1 check passed
@diegosouzapw diegosouzapw mentioned this pull request Jun 16, 2026
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants