Skip to content

feat: OPA driven denial and HITL elicitation - #36

Draft
thomaswinkler wants to merge 2 commits into
schplitt:mainfrom
thomaswinkler:feat/opa-driven-governance-elicitation
Draft

feat: OPA driven denial and HITL elicitation#36
thomaswinkler wants to merge 2 commits into
schplitt:mainfrom
thomaswinkler:feat/opa-driven-governance-elicitation

Conversation

@thomaswinkler

@thomaswinkler thomaswinkler commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

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 prompt
  • elicit — ask the user for approval via MCP elicitation
  • deny — auto-reject, with human-readable reasons returned to the client

The 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

  1. Dry-run interception (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.
  2. Transaction plan (src/policy/transaction-plan.ts) — normalizes paths into glob-matchable templates (trailing ID segments → *).
  3. Policy evaluation (src/policy/evaluate.ts) — loads the cached WASM policy once per process, sets the user's data, and returns a single { action, reasons } decision.
  4. Enforcement (src/tools/intercept.ts) — allow proceeds, deny returns a blocked message with reasons, elicit falls through to the approval prompt.

Policy model

A single decision entrypoint (src/policy/rego/main.rego) with priority deny > elicit > allow. Deny is the union of:

  • tenant not in allowed_tenants
  • bulk-DELETE count over limits.max_deletes_per_transaction
  • a restricted_body_fields key present in a write body
  • an explicit deny path policy match

Path policies match on method (or *) and a /-delimited glob (* single-segment, ** any-depth). Default outcome is elicit (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-wasm added as a dependency.
  • scripts/compile-policy.sh recompiles bundle.wasm from rego (requires opa CLI; only needed when the .rego changes — the artifact is committed).
  • tsdown.config.ts copies bundle.wasm into dist/ so it resolves via import.meta.url at runtime.

Notes

  • Backward compatible: no --policy-data → unchanged elicitation behavior.
  • PATCH gap: the rego covers PATCH for restricted-field checks, but src/tools/intercept.ts filters the approval set to POST/PUT/DELETE, so a PATCH-only batch bypasses OPA. Intentional for now (approval scope unchanged) — flag if PATCH should be governed.
  • bundle.wasm is a committed binary (~346 KB); regenerate via the script when editing rego.

@schplitt
schplitt marked this pull request as draft June 15, 2026 06:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant