Skip to content

Commit 0f91f3c

Browse files
mhallidaclaude
andcommitted
fix: altimate_core_policy no longer rejects valid DML before checking policies
When no schema context is provided, the policy checker now extracts table names from the SQL via extractMetadata() and builds a minimal schema so DML statements (DELETE, UPDATE, INSERT, MERGE) pass validation. As a fallback, if validation still fails, the handler parses the policy JSON directly to check forbidden_operations rules against the statement type, ensuring policy violations are reported even when full validation isn't possible. Fixes TICKET-018 in #422. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4da972c commit 0f91f3c

1 file changed

Lines changed: 60 additions & 2 deletions

File tree

packages/opencode/src/altimate/native/altimate-core.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,14 +374,72 @@ register("altimate_core.fix", async (params) => {
374374
}
375375
})
376376

377-
// 8. altimate_core.policy
377+
// 8. altimate_core.policy — with DML-aware schema fallback
378378
register("altimate_core.policy", async (params) => {
379379
try {
380-
const schema = schemaOrEmpty(params.schema_path, params.schema_context)
380+
let schema = schemaOrEmpty(params.schema_path, params.schema_context)
381+
382+
// If no explicit schema provided, extract table names from SQL and build
383+
// a minimal schema so DML statements (DELETE, UPDATE, INSERT, MERGE)
384+
// don't fail validation before policy checks can run.
385+
if (!params.schema_path && !params.schema_context) {
386+
try {
387+
const meta = core.extractMetadata(params.sql)
388+
if (meta.tables?.length) {
389+
const ddl = meta.tables
390+
.map((t: string) => `CREATE TABLE ${t} (id INT);`)
391+
.join("\n")
392+
schema = core.Schema.fromDdl(ddl)
393+
}
394+
} catch {
395+
// Fall back to empty schema if metadata extraction fails
396+
}
397+
}
398+
381399
const raw = await core.checkPolicy(params.sql, schema, params.policy_json)
382400
const data = toData(raw)
383401
return ok(data.allowed !== false, data)
384402
} catch (e) {
403+
// If the error is a validation error, try to extract policy-relevant info
404+
// and provide a useful response instead of a raw error
405+
const errStr = String(e)
406+
if (errStr.includes("validation") || errStr.includes("parse")) {
407+
// Parse the policy JSON to do a basic statement-type check
408+
try {
409+
const policy = JSON.parse(params.policy_json)
410+
const rules = policy.rules ?? []
411+
const violations: Array<Record<string, any>> = []
412+
413+
// Check forbidden_operations rules against statement type
414+
for (const rule of rules) {
415+
if (rule.type === "forbidden_operations" && Array.isArray(rule.operations)) {
416+
const sqlUpper = params.sql.trim().toUpperCase()
417+
for (const op of rule.operations) {
418+
if (sqlUpper.startsWith(op.toUpperCase())) {
419+
violations.push({
420+
rule: "forbidden_operations",
421+
category: "query_patterns",
422+
severity: "error",
423+
message: `${op} is a forbidden operation`,
424+
detail: { type: "blocked_statement", statement: op },
425+
})
426+
}
427+
}
428+
}
429+
}
430+
431+
if (violations.length > 0) {
432+
return ok(false, {
433+
allowed: false,
434+
violations,
435+
warnings: [],
436+
policies_evaluated: rules.length,
437+
})
438+
}
439+
} catch {
440+
// If policy parsing fails too, return the original error
441+
}
442+
}
385443
return fail(e)
386444
}
387445
})

0 commit comments

Comments
 (0)