Skip to content

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.

A registration belongs in a repository, next to everything else that decides what runs in production. Two commands do that:

Terminal window
Onbe.Server agent init jira-triage --out agents/
Onbe.Server agent apply agents/*.yaml --server https://onbe.internal.example.com

init 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:

Terminal window
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"
}'
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.

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 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, as agent init writes 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.

Terminal window
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.

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."]
}
}

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.

OnbePre-release. v0.1 is not out yet.

© 2026 Onbe