Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 20 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,30 @@ 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`,
)
} else {
throw e
}
}
}
// altimate_change end

const native = nativeHandlers.get(method as string)

Expand Down
16 changes: 15 additions & 1 deletion packages/opencode/src/altimate/native/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ 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")
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
Copy Markdown

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
Loading