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.

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.

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.

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.

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.

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.

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.

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.

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

© 2026 Onbe