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.
Revocation
Stopping a token in flight has a cost, and there is no design that avoids it. Onbe makes the choice explicit rather than picking for you and hoping you do not notice.
The two modes
Section titled “The two modes”Local validation. The tool server verifies the signature, iss, aud and exp against
cached keys, and lets the request through. No call to the control plane. Fast, no shared
failure mode, and revocation is eventual: a revoked token keeps working until it expires,
bounded by the agent’s max_token_ttl.
Introspection. The tool server asks the control plane, on every request, whether this token is still active. One round trip per call. Revocation is immediate, because introspection is answered from storage and a revocation shows the moment it is written.
That is the whole tradeoff: max_token_ttl of exposure, or a round trip per call.
Choosing
Section titled “Choosing”The lever that does the work is on the tool server, per route, because that is where validation actually happens:
app.get('/issues', onbeExpress(onbe, { scope: 'jira:read' }), handler);app.post( '/issues/:id/comments', onbeExpress(onbe, { scope: 'jira:comment', highRisk: true }), handler,);A useful rule: if you would be uncomfortable with the action happening five minutes after you hit the kill switch, that route is high-risk. Reads usually are not. Writes, deletions, payments and anything that leaves your systems usually are.
The lever that travels with the token
Section titled “The lever that travels with the token”Marking routes is per route, and it only works if whoever writes the tool server knows what the operator decided. The registration says it once instead:
{ "allowed_audiences": ["https://jira.internal", "https://db.internal"], "high_risk_audiences": ["https://db.internal"]}A task token for an audience in that list carries a claim:
{ "aud": "https://db.internal", "introspect_required": true}@onbe/server and @onbe/mcp introspect a token carrying it whether or not the route was
marked highRisk, so the operator’s decision reaches a tool server nobody remembered to
reconfigure. The claim is written only when it is true, so an ordinary token is unchanged.
The two levers compose, and they are not the same lever:
| Set this | And |
|---|---|
high_risk_audiences on the registration |
every token for that audience is introspected, by any tool server using an SDK |
highRisk on a route |
that route is introspected, even for an audience that is not high-risk as a whole |
A useful rule for the second: a route riskier than its audience deserves its own mark. A tool
server built against the raw HTTP contract rather than an SDK has to read the claim itself —
it is introspect_required in the token body.
Keeping max_token_ttl short narrows the gap for everything else. Five minutes is the default
for a reason.
How to revoke
Section titled “How to revoke”| What | How | Effect |
|---|---|---|
| One token | POST /oauth2/revoke with the token and the agent’s assertion |
That jti is dead |
| One task | DELETE /admin/tasks/{task_id} |
The task, every task delegated from it at any depth, and every grant under them |
| Every task of an agent | DELETE /admin/agents/{agent_id}/tasks |
The same, for all of that agent’s live tasks |
| The agent itself | PATCH /admin/agents/{agent_id} with {"enabled": false} |
Takes effect on the next request; registrations are never cached |
POST /oauth2/revoke is RFC 7009 and only the agent a token was issued to can use it. Anything
else — someone else’s token, a token that does not exist — gets the same empty 200, so the
endpoint cannot be used to probe which tokens are real. Revoking twice is one revocation.
Revoking a task grant revokes its task and everything delegated from it; revoking a task token
kills that jti alone.
The admin kill switches are idempotent too: a repeat answers 200 with revoked_tasks: 0 and
writes nothing.
What revocation does not stop
Section titled “What revocation does not stop”Tokens already issued and validated locally stay valid until they expire. There is no way around that short of introspection, and pretending otherwise is how people end up believing a kill switch did something it did not.
Refreshing, though, fails at once — access_denied — so the longest anything survives a kill
switch is the life of the token already in the agent’s hand.
In the ledger
Section titled “In the ledger”Every one of these writes a record. A task revoked by an operator carries reason
operator_kill_switch; each task revoked underneath it carries parent_revoked. A single token
revoked by jti writes token.revoked. Expiry writes task.expired — and is never written as
a revocation, because reading the ledger back and being able to tell “stopped” from “finished”
is most of what the ledger is for.
© 2026 Onbe