From 5c84bf70fd19a09fe38f0c3b507b52bf67ad5a62 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Fri, 20 Mar 2026 00:02:20 -0700 Subject: [PATCH 1/3] fix: graceful degradation when native binding fails to load On systems with older GLIBC (e.g. Ubuntu 20.04 with GLIBC 2.31), `@altimateai/altimate-core` napi-rs binary requires GLIBC 2.35 and crashes with an unhandled stack trace on startup, blocking all CLI usage. Three changes to degrade gracefully instead of crashing: 1. `sql-classify.ts`: lazy-load `altimate-core` on first call instead of eager `require()` at import time. Falls back to "write" (safe default) when native binding is unavailable. 2. `native/index.ts`: catch `altimate-core` import failure separately so other handler modules (connections, schema, finops, dbt) still register. 3. `dispatcher.ts`: catch registration hook failures caused by native binding errors, log a clear warning, and continue. Non-native-binding errors still throw. Result: CLI starts normally. SQL analysis tools (validate, lint, transpile, lineage) are unavailable, but warehouse connections, schema indexing, dbt integration, and all other features work. Closes #310 --- .../src/altimate/native/dispatcher.ts | 22 ++++++++++++++-- .../opencode/src/altimate/native/index.ts | 16 +++++++++++- .../src/altimate/tools/sql-classify.ts | 25 +++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/altimate/native/dispatcher.ts b/packages/opencode/src/altimate/native/dispatcher.ts index 944647630a..377399c270 100644 --- a/packages/opencode/src/altimate/native/dispatcher.ts +++ b/packages/opencode/src/altimate/native/dispatcher.ts @@ -35,12 +35,30 @@ export async function call( method: M, params: (typeof BridgeMethods)[M] extends { params: infer P } ? P : never, ): Promise<(typeof BridgeMethods)[M] extends { result: infer R } ? R : never> { - // Lazy registration: load all handler modules on first call + // altimate_change start — graceful degradation when native binding unavailable + // Lazy registration: load all handler modules on first call. + // If the native binding fails to load (e.g. GLIBC mismatch on older Linux), + // log a warning and continue — tools that don't need native will still work. if (_ensureRegistered) { const fn = _ensureRegistered _ensureRegistered = null - await fn() + try { + await fn() + } catch (e: any) { + const msg = String(e?.message || e) + if (msg.includes("native binding") || msg.includes("GLIBC") || msg.includes("ERR_DLOPEN_FAILED")) { + console.error( + `\n⚠ Native module (@altimateai/altimate-core) failed to load.\n` + + ` SQL analysis tools (validate, lint, transpile, lineage, etc.) will be unavailable.\n` + + ` Other features (warehouse connections, schema indexing, dbt) still work.\n` + + ` Cause: ${msg.slice(0, 200)}\n`, + ) + } else { + throw e + } + } } + // altimate_change end const native = nativeHandlers.get(method as string) diff --git a/packages/opencode/src/altimate/native/index.ts b/packages/opencode/src/altimate/native/index.ts index c95a49bf7e..7df838e4f7 100644 --- a/packages/opencode/src/altimate/native/index.ts +++ b/packages/opencode/src/altimate/native/index.ts @@ -5,8 +5,21 @@ export * as Dispatcher from "./dispatcher" // Lazy handler registration — modules are loaded on first Dispatcher.call(), // not at import time. This prevents @altimateai/altimate-core napi binary // from loading in test environments where it's not needed. +// altimate_change start — graceful degradation when native binding unavailable setRegistrationHook(async () => { - await import("./altimate-core") + // altimate-core napi-rs binding may fail on systems with older GLIBC. + // Load it separately so other handlers (connections, schema, finops, dbt) still register. + try { + await import("./altimate-core") + } catch (e: any) { + const msg = String(e?.message || e) + if (msg.includes("native binding") || msg.includes("GLIBC") || msg.includes("ERR_DLOPEN_FAILED")) { + // Swallowed here — dispatcher.ts logs the user-facing warning + } else { + throw e + } + } + await import("./sql/register") await import("./connections/register") await import("./schema/register") @@ -14,3 +27,4 @@ setRegistrationHook(async () => { await import("./dbt/register") await import("./local/register") }) +// altimate_change end diff --git a/packages/opencode/src/altimate/tools/sql-classify.ts b/packages/opencode/src/altimate/tools/sql-classify.ts index 9127e86a17..2ab708e74e 100644 --- a/packages/opencode/src/altimate/tools/sql-classify.ts +++ b/packages/opencode/src/altimate/tools/sql-classify.ts @@ -1,10 +1,24 @@ -// altimate_change - SQL query classifier for write detection +// altimate_change start — SQL query classifier for write detection // // Uses altimate-core's AST-based getStatementTypes() for accurate classification. // Handles CTEs, string literals, procedural blocks, all dialects correctly. +// Lazy-loads altimate-core on first use to avoid crashing at import time +// when the native binary is unavailable (e.g. GLIBC mismatch). // eslint-disable-next-line @typescript-eslint/no-explicit-any -const core: any = require("@altimateai/altimate-core") +let _core: any = null + +function getCore(): any { + if (!_core) { + try { + _core = require("@altimateai/altimate-core") + } catch { + // Native binding unavailable — return null so callers can degrade gracefully + return null + } + } + return _core +} // Categories from altimate-core that indicate write operations const WRITE_CATEGORIES = new Set(["dml", "ddl", "dcl", "tcl"]) @@ -17,8 +31,11 @@ const HARD_DENY_TYPES = new Set(["DROP DATABASE", "DROP SCHEMA", "TRUNCATE", "TR /** * Classify a SQL string as "read" or "write" using AST parsing. * If ANY statement is a write, returns "write". + * Falls back to "write" (safe default) if native binding is unavailable. */ export function classify(sql: string): "read" | "write" { + const core = getCore() + if (!core) return "write" // fail-safe: treat as write when native unavailable const result = core.getStatementTypes(sql) if (!result?.categories?.length) return "read" // Treat unknown categories (not in WRITE or READ sets) as write to fail safe @@ -36,8 +53,11 @@ export function classifyMulti(sql: string): "read" | "write" { /** * Single-pass: classify and check for hard-denied statement types. * Returns both the overall query type and whether a hard-deny pattern was found. + * Falls back to write + not-blocked when native binding is unavailable. */ export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } { + const core = getCore() + if (!core) return { queryType: "write", blocked: false } const result = core.getStatementTypes(sql) if (!result?.statements?.length) return { queryType: "read", blocked: false } @@ -50,3 +70,4 @@ export function classifyAndCheck(sql: string): { queryType: "read" | "write"; bl const queryType = categories.some((c: string) => !READ_CATEGORIES.has(c)) ? "write" : "read" return { queryType: queryType as "read" | "write", blocked } } +// altimate_change end From 209a0d2216c5b6c3c14e628687248360ae98c4bd Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Fri, 20 Mar 2026 08:53:15 -0700 Subject: [PATCH 2/3] fix: wrap all core-dependent modules in try/catch for graceful degradation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sql/register`, `schema/register`, `dbt/register`, and `local/register` all transitively import `@altimateai/altimate-core`. Previously only the direct `altimate-core` import was wrapped — if any register module failed due to GLIBC mismatch, the remaining modules wouldn't load either. Now each core-dependent module is individually wrapped so a native binding failure in one doesn't prevent the others from registering. Modules that don't depend on core (`connections/register`, `finops/register`) load unconditionally. Adds CI guard tests to `build-integrity.test.ts` that verify: - `isNativeBindingError` helper exists - `altimate-core` import is wrapped in try/catch - All 4 core-dependent modules are NOT bare `await import()` calls - Both safe modules are still referenced Co-Authored-By: Claude Opus 4.6 (1M context) --- .../opencode/src/altimate/native/index.ts | 36 ++++++++++++--- .../test/branding/build-integrity.test.ts | 45 ++++++++++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/altimate/native/index.ts b/packages/opencode/src/altimate/native/index.ts index 7df838e4f7..cf59c21b73 100644 --- a/packages/opencode/src/altimate/native/index.ts +++ b/packages/opencode/src/altimate/native/index.ts @@ -6,25 +6,47 @@ export * as Dispatcher from "./dispatcher" // not at import time. This prevents @altimateai/altimate-core napi binary // from loading in test environments where it's not needed. // altimate_change start — graceful degradation when native binding unavailable +function isNativeBindingError(e: any): boolean { + const msg = String(e?.message || e) + return msg.includes("native binding") || msg.includes("GLIBC") || msg.includes("ERR_DLOPEN_FAILED") +} + setRegistrationHook(async () => { // altimate-core napi-rs binding may fail on systems with older GLIBC. - // Load it separately so other handlers (connections, schema, finops, dbt) still register. + // Load it separately so other handlers still register. try { await import("./altimate-core") } catch (e: any) { - const msg = String(e?.message || e) - if (msg.includes("native binding") || msg.includes("GLIBC") || msg.includes("ERR_DLOPEN_FAILED")) { + if (isNativeBindingError(e)) { // Swallowed here — dispatcher.ts logs the user-facing warning } else { throw e } } - await import("./sql/register") + // These modules transitively import @altimateai/altimate-core (via pii-detector, + // lineage, test-local, or directly). Wrap each so a native binding failure in one + // doesn't prevent the others from registering. + const coreDependent = [ + () => import("./sql/register"), + () => import("./schema/register"), + () => import("./dbt/register"), + () => import("./local/register"), + ] + for (const load of coreDependent) { + try { + await load() + } catch (e: any) { + if (isNativeBindingError(e)) { + // Core-dependent module failed — skip silently, main warning already logged + } else { + throw e + } + } + } + + // These modules don't depend on altimate-core and should always load. await import("./connections/register") - await import("./schema/register") await import("./finops/register") - await import("./dbt/register") - await import("./local/register") }) // altimate_change end diff --git a/packages/opencode/test/branding/build-integrity.test.ts b/packages/opencode/test/branding/build-integrity.test.ts index e4977c21c4..36c12d009a 100644 --- a/packages/opencode/test/branding/build-integrity.test.ts +++ b/packages/opencode/test/branding/build-integrity.test.ts @@ -338,7 +338,50 @@ describe("Bundle Completeness", () => { }) // --------------------------------------------------------------------------- -// 8. Install Script +// 8. Graceful Native Binding Degradation +// --------------------------------------------------------------------------- + +// altimate_change start — CI guard: core-dependent modules must be try/catch wrapped +describe("Graceful Native Binding Degradation", () => { + const nativeIndex = readFileSync( + join(repoRoot, "packages/opencode/src/altimate/native/index.ts"), + "utf-8", + ) + + test("native/index.ts has isNativeBindingError helper", () => { + expect(nativeIndex).toContain("isNativeBindingError") + }) + + test("altimate-core import is wrapped in try/catch", () => { + // The altimate-core import must be inside a try block + expect(nativeIndex).toMatch(/try\s*\{[^}]*import\(["']\.\/altimate-core["']\)/) + }) + + const coreDepModules = ["sql/register", "schema/register", "dbt/register", "local/register"] + + for (const mod of coreDepModules) { + test(`${mod} import is wrapped in try/catch (not bare await)`, () => { + // Each core-dependent module should appear inside the coreDependent array or + // a try/catch, NOT as a bare `await import("./module")`. + const barePattern = new RegExp(`^\\s*await import\\(["']\\.\\/${mod}["']\\)`, "m") + expect(nativeIndex).not.toMatch(barePattern) + // Must still be referenced somewhere in the file + expect(nativeIndex).toContain(mod) + }) + } + + const safeMods = ["connections/register", "finops/register"] + + for (const mod of safeMods) { + test(`${mod} is imported (does not depend on altimate-core)`, () => { + expect(nativeIndex).toContain(mod) + }) + } +}) +// altimate_change end + +// --------------------------------------------------------------------------- +// 9. Install Script // --------------------------------------------------------------------------- describe("Install Script", () => { From 35f913e8cf1cc9a029c2228bf3b08842c9910fb8 Mon Sep 17 00:00:00 2001 From: anandgupta42 Date: Fri, 20 Mar 2026 08:59:28 -0700 Subject: [PATCH 3/3] fix: add remediation steps to native binding warning message Users seeing the GLIBC mismatch warning now get actionable fix steps: which distro versions have GLIBC >= 2.35, Docker workaround, and how to check their current GLIBC version. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/opencode/src/altimate/native/dispatcher.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/altimate/native/dispatcher.ts b/packages/opencode/src/altimate/native/dispatcher.ts index 377399c270..f564c265f9 100644 --- a/packages/opencode/src/altimate/native/dispatcher.ts +++ b/packages/opencode/src/altimate/native/dispatcher.ts @@ -51,7 +51,12 @@ export async function call( `\n⚠ Native module (@altimateai/altimate-core) failed to load.\n` + ` SQL analysis tools (validate, lint, transpile, lineage, etc.) will be unavailable.\n` + ` Other features (warehouse connections, schema indexing, dbt) still work.\n` + - ` Cause: ${msg.slice(0, 200)}\n`, + ` Cause: ${msg.slice(0, 200)}\n` + + `\n` + + ` To fix this, upgrade to a system with GLIBC >= 2.35:\n` + + ` • Ubuntu 22.04+ / Debian 12+ / Fedora 36+ / RHEL 9+\n` + + ` • Or run inside a Docker container with a newer base image\n` + + ` • Check your version: ldd --version\n`, ) } else { throw e