Skip to content

5-minute @governed walkthrough

The shortest path from pip install to a real allow/block decision on a tool you wrote yourself. No agent framework, no LLM API key, no adapter. Just a Python function, a Rego rule, and the policy gate.

What you'll have at the end

A refund.py script that calls approve_refund(...) twice — once inside policy, once outside. The inside call returns its result; the outside call raises GovernanceError with the policy decision attached.

Prerequisites

  • Python 3.11+
  • Docker for the OPA policy engine
  • ~5 minutes

Step 1 — Install

bash
pip install kitelogik

Step 2 — Scaffold OPA + a starter project (optional but easiest)

bash
kitelogik init governed-tutorial
cd governed-tutorial
docker compose up -d         # starts OPA on http://localhost:8181

The init command writes a policies/policy.yaml, compiles it to policies/policy.rego, generates an agent.py, and a docker-compose.yml. We'll ignore the generated agent.py and write our own tiny script.

You can also skip init and run OPA directly:

bash
mkdir policies && cd policies   # for the rego we'll write below
docker run -d --name opa -p 8181:8181 \
    -v "$(pwd):/policies:ro" \
    openpolicyagent/opa:latest run --server --addr :8181 /policies

Step 3 — Write a tiny policy

Replace the contents of policies/policy.yaml with:

yaml
version: 1

rules:
  - name: allow_small_refund
    when:
      action: approve_refund
      role: support_agent
      scope: approve_refund
      args:
        amount: { lte: 100 }
    then: allow

  - name: block_large_refund
    when:
      action: approve_refund
      args:
        amount: { gt: 100 }
    then: deny
    reason: "Refunds over $100 require manager approval"

Compile it to Rego:

bash
kitelogik compile policies/policy.yaml

OPA was started with --watch (the compose file does this), so the new Rego loads automatically — no restart needed.

Step 4 — Write the script

Create refund.py next to policies/:

python
# refund.py
import asyncio
from kitelogik import (
    GovernanceError,
    OPAClient,
    PolicyGate,
    SessionContext,
    governed,
)

gate = PolicyGate(opa_client=OPAClient())          # default: http://localhost:8181
context = SessionContext(
    session_id="tutorial_001",
    user_role="support_agent",
    session_scopes=["approve_refund"],
)


# Your business function — no governance code in the body.
@governed(gate=gate, context=context)
async def approve_refund(customer_id: str, amount: float) -> str:
    return f"Refunded ${amount:.2f} to {customer_id}"


async def main():
    # Within scope — runs normally
    print(await approve_refund(customer_id="cust_001", amount=42.0))

    # Out of scope — policy denies, GovernanceError is raised
    try:
        await approve_refund(customer_id="cust_001", amount=5000.0)
    except GovernanceError as exc:
        print(f"BLOCKED: {exc}")
        print(f"  risk_tier   = {exc.decision.risk_tier}")
        print(f"  rule_matched = {exc.decision.rule_matched}")


asyncio.run(main())

Step 5 — Run it

bash
python refund.py

Expected output:

text
Refunded $42.00 to cust_001
BLOCKED: Tool 'approve_refund' denied by policy: Refunds over $100 require manager approval
  risk_tier   = OPERATIONAL
  rule_matched = data.kitelogik.main.deny[_]

The first call ran the function body normally. The second never reached it — GovernanceError was raised before the function body executed, with the full PolicyDecision attached on exc.decision.

Iterating

Edit policies/policy.yaml (e.g. raise the threshold to lte: 200), recompile:

bash
kitelogik compile policies/policy.yaml

OPA hot-reloads. Re-run python refund.py to see the new threshold take effect — no restart anywhere.

What you just built

text
┌──────────────────┐         ┌──────────────────┐         ┌──────────────────┐
│  refund.py       │  call   │  @governed       │  HTTP   │  OPA :8181       │
│  approve_refund  │ ──────▶ │  evaluates input │ ──────▶ │  policy.rego     │
└──────────────────┘         └────────┬─────────┘         └──────────────────┘

                                allow / deny / hitl


                              fn body runs (or
                              GovernanceError)

Three pieces, ~20 lines of code, no framework.

Where to go next

  • GovernedToolbox — when you have many tools, register them all on one toolbox and call by name
  • Your first policy — cover the full YAML DSL, hand-written Rego, and opa test
  • Adapter overview — wire @governed's underlying gate into OpenAI, Agents SDK, LangChain, LangGraph, etc.
  • Risk tiers & HITL — when policy should escalate to a human reviewer instead of denying outright

Released under the Apache 2.0 License.