feat: OPA driven denial and HITL elicitation - #36
Draft
thomaswinkler wants to merge 2 commits into
Draft
Conversation
schplitt
marked this pull request as draft
June 15, 2026 06:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work in progress...
OPA-driven governance: deny / elicit / allow for mutating operations
Summary
Adds a policy-governance layer that evaluates mutating API operations (
POST/PUT/DELETE) before they execute. Operations are dry-run in the sandbox, shaped into a transaction plan, and evaluated against a bundled Open Policy Agent (OPA) policy. The policy returns one of three outcomes:allow— proceed silently, no promptelicit— ask the user for approval via MCP elicitationdeny— auto-reject, with human-readable reasons returned to the clientThe rego rules are precompiled to WASM and bundled with the tool; operators supply only a
data.json(rules + limits) per session. When no policy data is supplied, behavior falls back to the existing "always elicit for mutating ops" flow — fully backward compatible.How it works
src/codemode/execute.ts,src/tools/intercept.ts) — mutating fetch calls are mocked (no real HTTP), capturing method/path/body. The mock echoes request bodies so downstream composition (const r = await POST(...); r.id) keeps working.src/policy/transaction-plan.ts) — normalizes paths into glob-matchable templates (trailing ID segments →*).src/policy/evaluate.ts) — loads the cached WASM policy once per process, sets the user's data, and returns a single{ action, reasons }decision.src/tools/intercept.ts) —allowproceeds,denyreturns a blocked message with reasons,elicitfalls through to the approval prompt.Policy model
A single
decisionentrypoint (src/policy/rego/main.rego) with priority deny > elicit > allow. Deny is the union of:allowed_tenantslimits.max_deletes_per_transactionrestricted_body_fieldskey present in a write bodydenypath policy matchPath policies match on method (or
*) and a/-delimited glob (*single-segment,**any-depth). Default outcome iselicit(the safe fallback).Example
data.json:{ "allowed_tenants": ["https://dtm-sb5.preprod.c8y.io"], "path_policies": [ { "action": "allow", "method": "GET", "path_glob": "/**" }, { "action": "allow", "method": "POST", "path_glob": "/**" }, { "action": "elicit", "method": "PUT", "path_glob": "/**" }, { "action": "elicit", "method": "DELETE", "path_glob": "/**" } ], "limits": { "max_deletes_per_transaction": 10 }, "restricted_body_fields": ["id", "self", "owner", "lastUpdated", "creationTime"] }Configuration
New CLI flag
--policy-data <file>/-p(src/cli/index.ts) — validated at startup (must exist and be valid JSON), threaded through to request context (src/types/mcp-context.ts). Absent → legacy elicit-always behavior.Build & artifacts
@open-policy-agent/opa-wasmadded as a dependency.scripts/compile-policy.shrecompilesbundle.wasmfrom rego (requiresopaCLI; only needed when the.regochanges — the artifact is committed).tsdown.config.tscopiesbundle.wasmintodist/so it resolves viaimport.meta.urlat runtime.Notes
--policy-data→ unchanged elicitation behavior.src/tools/intercept.tsfilters the approval set toPOST/PUT/DELETE, so a PATCH-only batch bypasses OPA. Intentional for now (approval scope unchanged) — flag if PATCH should be governed.bundle.wasmis a committed binary (~346 KB); regenerate via the script when editing rego.