Skip to main content

Regent Control — Python SDK

Runtime policy enforcement for AI agent actions. Every action your agent takes — a refund, a wire, a DB write, an API call — is authorized first: allow / deny / escalate, with a short-lived scoped token, the human it's acting for, the intent, and an immutable audit record.

pip install regent-control

Requires Python 3.10+. The only dependency is httpx.

Two ways to integrate

1. Gate-direct — RegentControl (you hold the provider key)

Authorize the action, get a scoped token, perform it, report the outcome.

from regent_control import RegentControl, MandateExceeded, Escalated

control = RegentControl(api_key="rgnt_ctrl_…", agent_id="agent_refund_bot")

decision = control.authorize(
    tool="payments", action="refund.create",
    amount_usd=50, mandate_id="mnd_support_refunds",
    idempotency_key="T-8842:ch_aaa",          # a retry won't double-refund
    user_token=rep_id_token,                  # the rep's OIDC token — verified by the gate
    intent="refund the duplicate charge on ticket 8842",
    facts={"account_status": "active", "refund_to_original": True},
)

if not decision.allowed:
    ...                                        # decision.code / decision.reason
do_refund(scoped_token=decision.token)         # the token authorizes the downstream call
control.complete(decision.decision_id, status="success", downstream_ref=refund_id)

A deny is a normal return value, not an exception. Opt into raising:

decision.raise_for_status()                    # raises MandateExceeded / PolicyDenied / Escalated / …
# or
control.authorize_or_raise(tool="payments", action="refund.create", amount_usd=999)

2. Sidecar-routing — SidecarSession (the agent holds no key)

Point your normal HTTP call at the sidecar; it injects the real vaulted credential and forwards. A policy deny comes back as a typed error.

from regent_control import SidecarSession, MandateExceeded

sc = SidecarSession(base_url="http://localhost:8080",
                    user_token=rep_id_token, intent="refund duplicate charge")
try:
    resp = sc.call("payments", "refunds", method="POST",
                   json={"charge": "ch_aaa", "amount": 50},
                   facts={"account_status": "active", "refund_to_original": True},
                   idempotency_key="T-8842:ch_aaa")
    refund = resp.json()
except MandateExceeded as e:
    ...                                        # e.code, e.reason, e.decision_id

Both clients have async twins: AsyncRegentControl and AsyncSidecarSession.

Wrap a tool in one line — @guarded

from regent_control import RegentControl, guarded

control = RegentControl(api_key="rgnt_ctrl_…", agent_id="agent_refund_bot")

@guarded(control, tool="payments", action="refund.create",
         amount_arg="amount_usd", mandate_id="mnd_support_refunds")
def issue_refund(*, charge: str, amount_usd: float, scoped_token: str = "") -> str:
    return provider.refund(charge, amount_usd, token=scoped_token)

issue_refund(charge="ch_1", amount_usd=50)     # authorized → runs → completed
issue_refund(charge="ch_1", amount_usd=999)    # raises MandateExceeded; never runs

The decorator authorizes before the call, injects the scoped token (if the function declares scoped_token), and reports success/failed to close the audit. Pass per-call context via a control_context={...} keyword.

Verify the scoped token at your service edge — regent_control.verify

The complement to the client: your downstream service validates the gate-issued scoped JWT against Regent's JWKS and enforces the scope itself (least privilege at the edge — no shared static key, no trusting the caller).

pip install "regent-control[verify]"   # adds PyJWT
from regent_control.verify import TokenVerifier, ScopeError

verifier = TokenVerifier(jwks_url="https://control-api.regentprotocol.org/v1/control/.well-known/jwks.json")
scoped = verifier.verify(token, expected_tool="payments", expected_action="refund.create")
# raises TokenError (bad sig / expired / wrong issuer|audience) or ScopeError (wrong scope)
# scoped.agent_id / scoped.decision_id are audit-ready

See examples/verify_service.py for a FastAPI dependency.

Develop locally with zero prod — regent-control dev

regent-control dev --port 8009 --deny-over 50 --escalate-over 500
# point any client at  base_url="http://localhost:8009"

In tests, force a verdict with the in-process mock:

from regent_control import RegentControl, MandateExceeded
from regent_control.dev import run_mock_control

def test_over_limit_is_denied():
    with run_mock_control(deny_over=50) as base_url:
        control = RegentControl(api_key="dev", agent_id="a", base_url=base_url)
        d = control.authorize("stripe", "refund.create", amount_usd=999)
        assert d.code == "MANDATE_LIMIT_EXCEEDED"

Coding agents — regent-control hook (Claude Code, Codex, Hermes)

Claude Code runs shell hooks around every tool call. regent-control hook is those hooks: the money tool calls your agent makes (mcp__stripe__*, mcp__get4agent__buy, a curl to a payment API…) go through the Regent gate before they run, and every completed one leaves a signed receipt you can verify offline.

pip install regent-control
regent-control hook install claude-code --agent-id agt_… --key-file ~/.regent/control-key.txt
regent-control hook install codex          # same hooks, ~/.codex/hooks.json
regent-control hook install hermes         # pre_tool_call / post_tool_call shell hooks in ~/.hermes/config.yaml (needs pyyaml)
regent-control hook status [--target codex|hermes]

One rule file, one gate, four agents: Claude Code and Codex speak the same hook JSON; Hermes sends {tool_name, args, task_id} and reads {"decision": "block", "reason"}, which --format hermes translates (no context channel there, so observe-mode verdicts are log-only); OpenClaw uses the @regent-protocol/openclaw-regent plugin instead of a shell hook.

That writes three hooks into ~/.claude/settings.json (PreToolUse, PostToolUse, PostToolUseFailure, matcher mcp__.*) and a rule file at ~/.regent/hook.json:

{
  "mode": "observe",
  "agent_id": "agt_…",
  "mandate_id": "man_…",
  "rules": [
    {"match": "^mcp__stripe__.*", "amount": "amount", "divisor": 100, "currency": "currency", "payee": "customer"},
    {"match": "^mcp__get4agent__buy$", "amount": ["price", "amount"], "payee": "seller"},
    {"match": "^Bash$", "input_match": {"command": "api\\.stripe\\.com"}, "tool": "stripe", "action": "charge",
     "amount": "amount_cents", "divisor": 100, "currency": "const:USD"}
  ]
}
  • A tool call that matches no rule never touches the network.
  • observe (default): the gate decides and records, nothing is blocked; the verdict is shown to the agent as context. enforce: a deny or an escalation blocks the call with the gate's reason, and an unreachable gate fails closed (fail_open: true to change that).
  • An allow never bypasses Claude Code's own permission prompt — the gate adds a mandate check and evidence on top of the user's consent, it does not replace it.
  • PostToolUse closes the decision and saves the receipt to ~/.regent/receipts/<decision_id>.jwt; verify it with regent-verify (pip install regent-receipt-verify).
  • regent-control hook uninstall removes the hooks; the key is read from REGENT_CONTROL_KEY or the --key-file, never written into settings.json.

The same gate, from OpenClaw: the @regent-protocol/openclaw-regent plugin registers a before_tool_call hook with the identical rules (see plugins/openclaw-regent in this repo).

Typed errors

Exception Decision code Meaning
IdentityNotResolved IDENTITY_NOT_RESOLVED the agent id didn't resolve
AgentNotActive AGENT_NOT_ACTIVE the agent is suspended/revoked
PolicyDenied POLICY_DENIED a policy rule forbade it
ToolNotAllowed TOOL_NOT_ALLOWED the tool isn't in the agent's catalog
MandateNotFound MANDATE_NOT_FOUND a money action with no mandate
MandateExceeded MANDATE_LIMIT_EXCEEDED over the spend cap
RiskThresholdExceeded RISK_THRESHOLD_EXCEEDED risk score too high
VelocityExceeded VELOCITY_EXCEEDED too many decisions for this agent in the window; back off
DuplicateRequest DUPLICATE_REQUEST an identical money request was just allowed; pass an idempotency_key for a deliberate repeat
Escalated ESCALATION_REQUIRED needs a human approval (carries .escalation)

ControlError (transport/auth/5xx) and ControlNetworkError (never reached the plane) are always raised; everything above subclasses ControlDenied.

Request signing (optional)

Pass sign_requests=True to HMAC-sign the request body with the per-agent secret HMAC-SHA256(api_key, agent_id) (X-Agent-Signature). Identical to @regent/control-sdk (TypeScript), so an agent moves between the two unchanged.

Examples

The agent's own signing key

By default a request proves that the caller knows the organization's control key and named an agent id. With its own Ed25519 key an agent proves which agent called: Regent keeps only the public half, and the receipt names the key (assertion_kid).

regent-control keys generate                       # ~/.regent/agent-key.json, chmod 600
regent-control keys register --api-key rgnt_… --org <org_id> --agent <agent_id>
regent-control keys require  --api-key rgnt_… --org <org_id> --agent <agent_id> --on

From then on the hooks sign every decision request with it (the key file is picked up automatically), and so does the client when you pass it:

control = RegentControl(api_key=key, agent_id=agent_id, signing_key_file="~/.regent/agent-key.json")

The signature is a standard RFC 9421 HTTP Message Signature (Signature-Input, Signature, Content-Digest; tag regent-agent, keyid = the key's RFC 7638 thumbprint) covering the method, host, path and body. Anyone can verify it with any RFC 9421 library against the key Regent publishes for the agent at GET /v1/agents/{agent_id}/jwks.json or in the agent's DID document (verificationMethod, JsonWebKey2020). The same key works for Web Bot Auth and AAuth.

keys require --on makes the gate refuse this agent's unsigned requests. Rotate by registering a new key, moving the agent to it, then revoking the old one (console → agent → Signing keys, or DELETE …/keys/{kid}).

Enrol with an invite (no organization key on the agent's machine)

In the console (agent → Enrolment) mint a one-time code and run it where the agent lives:

regent-control enroll rgnt_inv_…

That generates the agent's own key here (or reuses ~/.regent/agent-key.json), proves possession, and writes ~/.regent/credentials.json: a control key bound to this agent only (rgnt_agt_…, the gate refuses it for any other agent) plus the agent id and the control URL. The hooks pick it up automatically; in code:

control = RegentControl.from_credentials()

Metadata

Release files for regent-control 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for regent-control 0.6.0
File Size Uploaded
regent_control-0.6.0.tar.gz 52.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for regent-control 0.6.0
File Interpreter ABI Platform
regent_control-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.4 kB

Release files / regent_control-0.6.0.tar.gz

Download URL regent_control-0.6.0.tar.gz
Size 52.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4899b8694ed1b8a295ed8810b676a4f770e2e7b74cfce8ea593a49e6f2c1c2ef
BLAKE2b-256 checksum
How to use checksums
e4b50a4bdb2527fb145aa73f7614f0978391b1d6c480c28fe02baff2d88cf3ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / regent_control-0.6.0-py3-none-any.whl

Download URL regent_control-0.6.0-py3-none-any.whl
Size 46.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
800dd14cdb482f23bc43aa88bb6f9b18c8500c2d5f4df40b7d02b6e5b6e467b1
BLAKE2b-256 checksum
How to use checksums
e6b72e1c68995264ae58f14d783d5258413e2f67679aeb8210fa6cf8350f59d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

This release

0.6.0 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page