Skip to content
Browse documentation

Python SDK

`cirvix` is a second implementation of the same engine, not an HTTP client. Wrap once at the executor boundary and catch a typed `CirvixDenied`.

Install

TerminalShell
$ pip install cirvix

Zero dependencies, standard library only. Python 3.9 or later.

guard.wrap()

agent.pyPython
import jsonfrom cirvix import guard, parse_rules, CirvixDenied, CirvixHeld with open("cirvix.policy.json", encoding="utf-8") as fh:    rules = parse_rules(json.load(fh)) tools = guard.wrap(    my_tools,    agent="support-triage",    environment=os.environ.get("CIRVIX_ENV", "local"),    rules=rules,) try:    crew.kickoff()except CirvixHeld as err:    print(err.approvers)     # ["platform-oncall"]    raiseexcept CirvixDenied as err:    print(err.policy)        # "deny-dotenv-read"    print(err.remediation)   # 'secrets.get("STRIPE_KEY")'    print(err.decision_id)   # pass to `cirvix why`    raise
Catch CirvixHeld first
CirvixHeld is a subclass of CirvixDenied, so a broad except CirvixDenied swallows it — and teaches the agent that a call a person was about to approve is a failure. The keyword is rules, not policy_dir; an unrecognised keyword raises TypeError.

Why this is a real evaluator

cirvix contains a second implementation of the policy engine, not a wrapper that calls the Node one over a socket. Decisions are made in-process, with no network on the enforcement path.

Two engines that can silently disagree about a security decision are worse than one engine and an honest gap: an agent denied by the gateway and permitted by the SDK is a bypass nobody would find until it mattered. Both load the same 44-case conformance fixture from the same file, and neither is allowed a private copy.

Async is preserved

An async tool stays async, or the framework's await receives a coroutine-returning wrapper it does not expect. __name__, __doc__, and __wrapped__ are all set on the governed callable.

Testing a policy

test_policy.pyPython
from cirvix.testing import evaluate def test_production_writes_are_held():    decision = evaluate(        policy_dir="./policies",        agent="deploy-bot",        action="k8s.apply",        resource="production/checkout",        context={"environment": "production"},    )     assert decision.verdict == "hold"    assert "platform-oncall" in decision.approvers

What wrap does not do

  • It does not govern tools you did not hand it. The gateway does, because it sits on the wire.
  • It does not broker secrets. The Python Guard has no secrets parameter — route through the gateway for handle substitution.
  • It does not write an audit chain. Use on_decision to forward records, or run the gateway.
  • It does not reach the network. Policy sync and telemetry are the daemon's job.
Edit this page on GitHub
Was this page helpful?