Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions packages/opencode/src/altimate/native/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,35 @@ export async function call<M extends BridgeMethod>(
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` +
`\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
}
}
}
// altimate_change end

const native = nativeHandlers.get(method as string)

Expand Down
46 changes: 41 additions & 5 deletions packages/opencode/src/altimate/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,48 @@ 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
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 () => {
await import("./altimate-core")
await import("./sql/register")
// altimate-core napi-rs binding may fail on systems with older GLIBC.
// Load it separately so other handlers still register.
try {
await import("./altimate-core")
} catch (e: any) {
if (isNativeBindingError(e)) {
// Swallowed here — dispatcher.ts logs the user-facing warning
} else {
throw e
}
}

// 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
25 changes: 23 additions & 2 deletions packages/opencode/src/altimate/tools/sql-classify.ts
Original file line number Diff line number Diff line change
@@ -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"])
Expand All @@ -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
Expand All @@ -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 }
Comment on lines 58 to +60
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hard-deny safety is bypassed when native core is unavailable.

On Line 60, fallback returns blocked: false, which allows hard-deny statements to proceed through the write-permission path instead of being unconditionally blocked. Keep a conservative hard-deny fallback even without native parsing.

🔧 Proposed fix
 const HARD_DENY_TYPES = new Set(["DROP DATABASE", "DROP SCHEMA", "TRUNCATE", "TRUNCATE TABLE"])
+const HARD_DENY_FALLBACK_REGEX = /\bDROP\s+(DATABASE|SCHEMA)\b|\bTRUNCATE(?:\s+TABLE)?\b/i
@@
 export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } {
   const core = getCore()
-  if (!core) return { queryType: "write", blocked: false }
+  if (!core) {
+    return { queryType: "write", blocked: HARD_DENY_FALLBACK_REGEX.test(sql) }
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } {
const core = getCore()
if (!core) return { queryType: "write", blocked: false }
export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } {
const core = getCore()
if (!core) {
return { queryType: "write", blocked: HARD_DENY_FALLBACK_REGEX.test(sql) }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/opencode/src/altimate/tools/sql-classify.ts` around lines 58 - 60,
The fallback in classifyAndCheck currently returns { queryType: "write",
blocked: false } when getCore() is null, which allows hard-deny SQL to pass;
change the fallback to conservatively block by returning { queryType: "write",
blocked: true } (or otherwise ensure blocked is true) so that when native
parsing via getCore() is unavailable, classifyAndCheck treats statements as
writes and enforces hard-deny blocking.

const result = core.getStatementTypes(sql)
if (!result?.statements?.length) return { queryType: "read", blocked: false }

Expand All @@ -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
45 changes: 44 additions & 1 deletion packages/opencode/test/branding/build-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading