Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.
Register an agent
The registration is the policy. Nothing an agent does at runtime can exceed it, so it is worth five minutes of thought per agent.
Register one
Section titled “Register one”A registration belongs in a repository, next to everything else that decides what runs in production. Two commands do that:
Onbe.Server agent init jira-triage --out agents/Onbe.Server agent apply agents/*.yaml --server https://onbe.internal.example.cominit writes the registration, the agent’s key pair and its public key set, with defaults tight
enough to be useless until somebody fills in the two empty lists. apply reconciles the files
against a running control plane: it creates what is missing, patches what differs, and does
nothing when the file already matches. It goes through the admin API like everything else, so
every change lands in the ledger. The admin key comes from ONBE_ADMIN_KEY or standard input,
never a flag; --dry-run needs no key, which is what runs on a pull request. apply never
deletes a registration and never changes enabled: both are decisions for the admin API.
The same registration over HTTP, which is what apply sends:
curl -s -X POST https://onbe.internal.example.com/admin/agents \ -H "Authorization: Bearer $ONBE_ADMIN_KEY" \ -H 'Content-Type: application/json' \ -d '{ "agent_id": "jira-triage", "display_name": "Jira triage agent", "sponsor_required": true, "allowed_scopes": ["jira:read", "jira:comment"], "allowed_audiences": ["https://jira.internal"], "max_task_ttl": "PT30M", "max_token_ttl": "PT5M", "max_delegation_depth": 1, "jwks_uri": "https://agents.internal.example.com/jira-triage/jwks.json" }'The fields
Section titled “The fields”| Field | What it does |
|---|---|
agent_id |
The agent’s name everywhere: in its assertion, in act.sub as agent:<id>, in the ledger |
display_name |
For humans reading the registry. Required |
sponsor_required |
Must be true in v0.1. A registration asking for false is rejected |
allowed_scopes |
The operator’s ceiling. The token’s scope is the intersection of this, the user’s scopes and the request |
allowed_audiences |
Where this agent may go. Anything else is invalid_target |
max_task_ttl |
How long one piece of work may last. Optional; Onbe:Tokens:DefaultTaskTtl when omitted |
max_token_ttl |
How long one credential may last, never past the task. Optional; Onbe:Tokens:DefaultTokenTtl when omitted, cut to the task if that is shorter |
max_delegation_depth |
How long an act chain this agent may produce. Required; 1 to 5 accepted, and v0.1 issues depth 1 whatever it says |
high_risk_audiences |
Audiences whose tokens carry introspect_required, so every call is checked with the control plane |
jwks |
The agent’s public keys, held and served by the control plane. Either this or jwks_uri |
jwks_uri |
Where the control plane fetches this agent’s public keys instead. HTTPS only |
Durations are ISO 8601: PT5M is five minutes, PT30M thirty, PT6H six hours.
Choosing the values
Section titled “Choosing the values”sponsor_required. true, and in v0.1 that is the only value accepted: a registration
asking for false is refused with sponsor_required: must be true. Every exchange requires a
human subject token, so false would describe a grant the server does not have. Scheduled agents
with an explicit service sponsor in the ledger — never an anonymous one — come later.
allowed_scopes. Narrow. This is the one that stops a compromised agent being an
interesting compromise. Write down what the agent does, translate it into scopes, and register
exactly those.
max_token_ttl. Five minutes is the default and is usually right. It is the width of your
revocation window for everything not in high_risk_audiences — see
Revocation. Longer buys you slightly fewer refreshes and a
proportionally longer window in which a leaked token is worth something.
max_task_ttl. As long as the work genuinely takes. This is not a security boundary in the
same way — the credential is bounded by max_token_ttl regardless — but a task that outlives
its usefulness is a grant sitting around waiting to be used.
max_delegation_depth. 1 unless this agent really does call other agents.
Whatever you pick is held inside the server’s Onbe:Agents:MinTaskTtl/MaxTaskTtl (a minute
to a day) and MinTokenTtl/MaxTokenTtl (thirty seconds to an hour) by default; an operator
lowers those to hold every agent shorter.
high_risk_audiences. Anything you would not want reached five minutes after you hit the
kill switch. A token for one of these audiences carries introspect_required, and both server
SDKs act on it without the tool server being configured to — so the decision is made once, here,
rather than kept in step by hand in every service that answers for the audience.
The agent’s keys
Section titled “The agent’s keys”The control plane authenticates an agent by verifying a short-lived assertion against that agent’s registered public keys. There are two ways to register them, and exactly one of the two is set:
jwks— the key set inline, asagent initwrites it. The control plane holds it and serves it at/agents/{agent_id}/jwks.json, so the agent needs no public endpoint of its own and the control plane needs no egress to reach one. Rotation is a change to a file under review.jwks_uri— a URL the control plane fetches instead. It must be absolute HTTPS; the registration is rejected otherwise, because an agent’s identity is not something to fetch over a cleartext connection.
The assertion itself is a compact JWS:
| Claim | Value |
|---|---|
iss and sub |
The agent_id |
aud |
The control plane’s issuer URL, or that URL plus /oauth2/token |
jti |
Never reused. At most 256 characters |
exp |
At most five minutes ahead; 60 seconds of clock skew is tolerated |
alg must be RS256, PS256 or ES256. Each jti is accepted exactly once — a replayed
assertion is invalid_client, and a conformance test exists to keep it that way.
@onbe/client builds and signs all of this; see Build an agent.
Changing a registration
Section titled “Changing a registration”curl -s -X PATCH https://onbe.internal.example.com/admin/agents/jira-triage \ -H "Authorization: Bearer $ONBE_ADMIN_KEY" \ -H 'Content-Type: application/json' \ -d '{"allowed_scopes": ["jira:read"]}'PATCH takes any subset of the fields plus enabled, and the merged registration is validated
as a whole. Registrations are read from storage per request and never cached, so a narrowed
allowed_scopes takes effect on the next refresh of every live task, and {"enabled": false}
stops the agent on its next request.
Every mutation writes agent.registered, agent.updated or agent.deleted to the ledger in
the same transaction, so the registry and the ledger cannot disagree about what was registered
when.
The rest of the admin API
Section titled “The rest of the admin API”| Method | Path | Result |
|---|---|---|
GET |
/admin/agents |
Every agent |
GET |
/admin/agents/{agent_id} |
One, or 404 |
DELETE |
/admin/agents/{agent_id} |
204, or 409 while the agent still has tasks |
DELETE |
/admin/agents/{agent_id}/tasks |
{"revoked_tasks": n} |
DELETE |
/admin/tasks/{task_id} |
{"revoked_tasks": n} |
A validation failure answers 400 with the fields named:
{ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "The request is invalid.", "status": 400, "errors": { "display_name": ["is required."], "max_delegation_depth": ["is required."] }}One key, for now
Section titled “One key, for now”v0.1 has one admin API key. It grants every admin operation, it must be at least 32 characters,
and rotating it means changing Onbe:Admin:ApiKey and restarting. Admin identities and
role-based access control are out of scope for v0.1; this is stated here rather than discovered
later.
When the key is not set at all, the admin API is disabled and every /admin request answers
503. A wrong key answers 401 and writes admin.denied to the ledger.
© 2026 Onbe