feat: [de-fork S3] HardPolicy kill gate — non-bypassable DDL denies at every dispatcher
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, totalHardPolicy.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 beforetool.executeat every dispatcher from the S2 route matrix: D1 (registry loop) + D2 (MCP loop) + D6 (direct Task dispatch) insession/prompt.ts, D3/D4 insession/tools.ts, D5 (BatchTool inner dispatch) intool/batch.ts. Fail-closed init viaassertInitialized()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 benignSELECTintoDROP 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 (nottoolID="batch"), closing the nested-dispatch gap. - On deny: returns a
hard_policy_deniedtool-error result (or throws in batch) —executeandtool.execute.afterare 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 (auditfinalArgsDigestasserted).- 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.tswith pure, sync, totalHardPolicy.check()that emits an audit record on every call. - Fail-closed init via
HardPolicy.assertInitialized()inpackages/opencode/src/effect/app-runtime.ts. - Enforcement added before
executein all routes; on deny, returnhard_policy_denied(or throw in batch) and skipexecute/tool.execute.after.
- New
-
Bug Fixes
- Closed bypasses: MCP-prefixed governed keys now resolve via
_-suffix matching; bash DDL matcher trims leading/trailing whitespace before checks. - Audit hardened:
finalArgsDigestis a SHA-256 of stable-stringified args; stable stringify now encodesMap/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, andctx.metadata; batch checks the inner tool ID + args.
- Closed bypasses: MCP-prefixed governed keys now resolve via
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.