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
$ pip install cirvixZero dependencies, standard library only. Python 3.9 or later.
guard.wrap()
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` raiseCirvixHeld 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
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.approversWhat 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
Guardhas nosecretsparameter — route through the gateway for handle substitution. - It does not write an audit chain. Use
on_decisionto forward records, or run the gateway. - It does not reach the network. Policy sync and telemetry are the daemon's job.
