Regent ControlEnrol an agent

Enrol an agent

An agent that runs on a machine you do not sit at — a cloud agent, a server, a teammate’s laptop — should not hold your organization’s key. Enrolment gives it two things of its own:

  • its own signing key (Ed25519), generated on that machine; Regent keeps only the public half;
  • a credential bound to this one agent (rgnt_agt_…): the gate refuses it for any other agent, and it cannot manage the organization, create mandates or approve its own escalations.

The owner mints a one-time code in the console; the machine turns it into both. Nothing organization-wide is ever typed on the agent’s side, and the code can travel through a chat: it is single-use and expires.

Mint the code in the console

Open the agent (Agents → the agent) and find the Enrolment card. Choose whether this machine’s requests must be signed (the default, and the right choice when the agent will use the SDK or the hooks), then New invite. The console shows the code — rgnt_inv_…, shown once, valid for 24 hours — together with the exact command to run.

Run the command on the agent’s machine

pip install regent-control
regent-control enroll rgnt_inv_… --label "where this runs"

The command generates the agent’s key if the machine has none, registers the public half with a proof of possession, and writes ~/.regent/agent-key.json and ~/.regent/credentials.json (both mode 600). Its last lines name the registered key id and the agent. Options: --key-file to reuse an existing key, --credentials to write elsewhere, --platform-url for another Regent host.

Use the credentials

The SDK and the hooks pick the files up by themselves:

from regent_control import RegentControl
 
rc = RegentControl.from_credentials()          # ~/.regent/credentials.json + agent-key.json
d = rc.authorize("stripe", "pay", resource="acct_supplier", amount_usd=12.5,
                 intent="pay the supplier invoice", idempotency_key="inv-4411-try-1")
if d.decision == "allow":
    ...                                        # do the thing
    rc.complete(d.decision_id, status="executed", downstream_ref="pi_…")

d.decision is allow, deny or escalate. An escalated request waits under Approvals in the console; after a person approves, the agent repeats the same call with the same idempotency_key. Tools behind the Cloud Gateway (Agentcard, Stripe through the vault) are rc.gateway("agentcard_shop", "buy", body={"ask": "…"}) from the same client, signed with the same key — the gateway verifies the signature and the receipt names the agent’s key. For Claude Code, Codex or Hermes, regent-control hook install <runtime> wires the same credentials into the runtime’s hooks instead.

Ask for a mandate

An enrolled agent holds a credential and, until a person sets limits, no mandate: every money call is refused with MANDATE_NOT_FOUND. Budgets are per currency, so the refusal names the currency it is missing in (d.currency): an agent with a tenge budget that tries to pay in dollars asks for a dollar budget. The agent may ask for one; it never grants one:

req = rc.request_mandate(currency="USD", per_tx="10", daily="30", monthly="100",
                         purpose="office supplies")
print(req.request_id, req.status)       # pending_approval

The organization’s owners get the approval link by e-mail and a card under Approvals; a person signs the limits (passkey in a production organization). rc.mandate_request_status(req.request_id) reports pending_approval, approved (with the mandate_id), denied or expired; the simpler way is to retry authorize later. The agent may propose caps per seller with the request — request_mandate(..., entity_limits={"arbuz.kz": "3000", "kaspi.kz": ("5000", "30000"), "*": (None, "10000")}) — the person sees them row by row and signs them with the rest. When paying, name the counterparty: rc.authorize(..., payee="arbuz.kz"); through a sidecar or the gateway the rail names it instead, and the receipt says which. One request may wait at a time, an agent that already holds an active mandate in that currency is told so (MANDATE_EXISTS), an agent may hold at most three currencies (TOO_MANY_CURRENCIES), and the organization can switch agent requests off. The ask itself is filed in the agent’s journal as mandate.requested, next to the approval it led to.

What the console shows afterwards

The Enrolment card lists every invite with its state (pending, used, expired, revoked), which key used it, and the credential it produced. Revoke on a credential cuts that machine off immediately; revoking the agent cuts off everything. Receipts for decisions made with an enrolled key carry key_custody: agent, which is how a reader later tells “the agent held its own key” from “Regent held a custodial key for it”.

⚠️

Whoever runs the code first becomes that agent. Send it only to the machine or the party that should, and revoke it from the card if it goes astray before it is used.

Start from the agent instead

When the person has no account yet, or you would rather not send them to a console, the agent can begin:

pip install regent-control
regent-control onboard --name "Grocery helper" --vendor "Instinct" --purpose "groceries"

The command prints a link for the person (valid 15 minutes). They open it, sign in or create an account with a passkey, set the budget in plain words and confirm. The command waits, then claims the agent’s credential: the agent’s own key is generated and registered, the bound credential lands in ~/.regent/credentials.json, and the mandate is already active. The first payment still asks the person. Over HTTP the same three calls are POST /v1/onboard, GET /v1/onboard/claim/{code} and POST /v1/onboard/claim/{code}.

An agent with no key store

Some agents can send HTTP with a stored secret but cannot run a command line or keep a key file (a hosted assistant, a no-code runtime). For those the owner creates the invite with require the agent’s own key and signed requests switched off, and the agent claims a bearer credential with an empty body:

curl -X POST https://api.regentprotocol.org/v1/enrol/rgnt_inv_…
# → {"control_key": "rgnt_agt_…", "credential": "bearer", "kid": null, …}

The credential is bound to that one agent and nothing else; every decision and receipt names the agent, and the console shows the invite was claimed with a bearer credential rather than a key. A strict invite answers KEY_REQUIRED instead. Prefer the agent’s own key wherever the machine can hold one: it is what lets a receipt prove which agent asked.

When not to enrol

  • The agent lives in an app with custom MCP connectors (Claude, Claude Code): use Connect from your agent app — no key to hold at all.
  • The agent cannot run a command line but can send HTTP with a stored secret: the consent screen gives it a bound key directly — see Without MCP.
  • The agent runs on your own infrastructure next to the sidecar: the sidecar holds the key and the agent holds nothing — see Run the Sidecar.

Verified on production: 2026-10-12 (regent-control 0.6.0).