feat: [de-fork S3] HardPolicy kill gate — non-bypassable DDL denies at every dispatcher

Closed
AN
1mo ago
+2156-72

PINEAPPLE

Summary

Stage S3 of the de-fork spike — the kill gate. Enforces non-bypassable hard denies (sql_execute DDL: DROP DATABASE/DROP SCHEMA/TRUNCATE; bash DDL) at every model-invoked tool-execution dispatcher, relocating today's scattered per-tool safety checks into one audited chokepoint. Behavior-preserving — these operations already block; S3 consolidates and closes a bypass.

  • NEW packages/opencode/src/altimate/policy/hard-policy.ts — pure, synchronous, total HardPolicy.check(): malformed/missing args return a deny (policy_internal_error), never throw, never implicit-allow. Emits a structured audit record on every call — this (not the trace) is the enforcement oracle the tests assert against.
  • HardPolicy.check() inserted before tool.execute at every dispatcher from the S2 route matrix: D1 (registry loop) + D2 (MCP loop) + D6 (direct Task dispatch) in session/prompt.ts, D3/D4 in session/tools.ts, D5 (BatchTool inner dispatch) in tool/batch.ts. Fail-closed init via assertInitialized() at the app-runtime composition seam.
  • Closes a real bypass: the fork's resolvers were discarding tool.execute.before's return value, so a plugin hook that mutated args (e.g. rewrote a benign SELECT into DROP DATABASE) executed the mutation unchecked. Every site now captures the post-hook final args and checks those. The batch inner dispatch checks the inner tool ID + args (not toolID="batch"), closing the nested-dispatch gap.
  • On deny: returns a hard_policy_denied tool-error result (or throws in batch) — execute and tool.execute.after are both skipped.

Scope preserved: bash DDL patterns are byte-identical to today's agent.ts safety table; the sql check reuses sql-execute.ts's classifier + error string. rm -rf / git push --force / git reset --hard correctly stay ask-tier (not promoted to hard deny).

Also includes route-sentinels.test.ts made line-number- and arg-agnostic (matches args or S3's finalArgs) so it passes with or without S3 applied — shared identically with PR #1014, mergeable in any order.

Review

Two independent adversarial reviews (a Claude reviewer + the orchestrator's own trace of every .execute() site) found no bypass via malformed args, mutated args, or allow-all permission config, and confirmed all 7 safety properties (coverage, final-args, total-function, deny-terminal, config-independence, fail-closed init, behavior-preservation).

Test Plan

  • bun test test/altimate/defork/hardpolicy.test.ts: 19 pass / 0 fail — the bypass matrix across D1–D6: DDL denied + not executed at every route, near-miss controls allowed (proves rules aren't over-broad), allow-all config still denies, malformed args fail closed, deny is terminal, mutated-args checked on the post-hook value (audit finalArgsDigest asserted).
  • Always-allow red-proof: stubbing check() to always-allow fails every dispatcher-integration test.
  • bunx tsc --noEmit -p packages/opencode: clean for the changed files.

Checklist

  • Tests added/updated
  • Documentation updated (spec §S3 + route matrix)
  • CHANGELOG updated — N/A (internal safety guardrail)

Closes #1017


This is a security boundary and the spike's decision checkpoint — held for human go/no-go; do not auto-merge.


Summary by cubic

Adds a centralized kill gate (HardPolicy) that enforces non-bypassable denies for destructive SQL/Bash DDL across all tool-execution dispatchers, with audited decisions. Follow-up fixes close whitespace and MCP-prefix bypasses, harden audit hashing, and make tool-call metadata reflect post-hook args. Closes #1017.

  • New Features

    • New packages/opencode/src/altimate/policy/hard-policy.ts with pure, sync, total HardPolicy.check() that emits an audit record on every call.
    • Fail-closed init via HardPolicy.assertInitialized() in packages/opencode/src/effect/app-runtime.ts.
    • Enforcement added before execute in all routes; on deny, return hard_policy_denied (or throw in batch) and skip execute/tool.execute.after.
  • Bug Fixes

    • Closed bypasses: MCP-prefixed governed keys now resolve via _-suffix matching; bash DDL matcher trims leading/trailing whitespace before checks.
    • Audit hardened: finalArgsDigest is a SHA-256 of stable-stringified args; stable stringify now encodes Map/Set; audit sink wrapped in try/catch and log trimming is amortized O(1).
    • Final-args fidelity: post-hook args are used for HardPolicy.check(), execute, audit, and ctx.metadata; batch checks the inner tool ID + args.

Summary by CodeRabbit

  • New Features

    • Added centralized, fail-closed hard policy enforcement blocking dangerous SQL and Bash actions (DROP DATABASE/SCHEMA, TRUNCATE) across native, MCP, batch, and task execution.
    • Policy checks now use post-hook arguments, with consistent “Blocked by policy” results when denied.
    • Introduced deterministic audit logging for every decision and an optional audit sink; enforcement initialization is validated at module startup.
  • Tests

    • Added comprehensive coverage for allow/deny behavior, audit/oracle expectations, post-hook argument handling, MCP/batch/task enforcement, and execution-route sentinels.