All documentation
Explanation Verified 2026-09-06

Authority and access

How ONE decides whether a caller may do a thing — the group-tree walk, the four rungs, and why a 403 and a 503 mean different things.

Reading this as an agent? The same page in raw markdown: /docs/authority.md

Every access question in ONE is the same question: may this caller do this action on this node? There is one function that answers it, one tree it reads, and three answers it can give. This page is the mental model behind every forbidden you will see.

One rule: walk up and take the first answer

Groups form a tree. To answer any question about a node — who controls it, which credit pool it spends from, whose brand it wears, which plan tier it is on — start at the node and walk up until something answers.

        root
          │
      agency-org        ← owner membership lives here
          │
     client-acme        ← the node being asked about
          │
      team-launch

An owner membership on agency-org answers for client-acme and for team-launch, because both are below it.

The walk is written once, in TypeQL, at schema/roles.tql. self-or-ancestors-of is the primitive; controls, can, funding-of, brand-of, plan-of and sensitivity-of are all readings of it. Authority and money share one shape.

Ownership goes down. Lesser roles do not.

The two directions behave differently, and the difference is what makes tenants isolated from each other.

  • Ownership inherits down, ungated. Own a node and you own its whole subtree at any depth. Ownership pierces privacy: an ancestor-owner reaches a node marked private.
  • Lesser roles inherit only through open groups. A group is born private so its creator can set it up. Once opened (visibility: group), the parent’s members inherit their roles into it — flat, never raised. public opens it to anyone. A private node seals its subtree from the roster above it.
  • Siblings are isolated. Neither sibling is an ancestor of the other, so neither controls nor can ever returns for the other. Multitenancy is not a filter bolted onto queries; it is the shape of the tree.

Four rungs, one comparison

Non-owner authority is one number against another number.

RungTierReaches
owner3everything, and every action below
admin2people and settings
member1create and change
viewer0read

The decision is tierOf(role) >= minTier(action). There are 39 named actions, each with exactly one floor: 5 at tier 3 (delete_group, edit_schema, mint_capability, grant_capability, act_as), 15 at tier 2, 10 at tier 1, 9 at tier 0. rungOf() — the parser for an untrusted role string, a D1 column or a webhook body — reads an unknown or absent value as viewer, the floor, never a grant.

agent and auditor are not rungs. agent is an attribute of the actor, a software flag; auditor is a viewer plus a delegation grant for sensitive reads. Both were roles in an earlier model. Where a stale value survives on a membership row the walk has already matched, decide()’s rung step reads it as member — membership is established by then, and only the rung is in question.

Delegation: the one sideways grant

The walk answers ownership (down) and rungs (flat). It does not answer scoped, sideways authority — an agency operator who may act on one named client org and nothing else. That is the delegation(grantee, scope, role-action) relation.

It is bound to a single group and does not nest. can() matches scope: $g exactly, never an ancestor, so a delegation on a parent never leaks into a child. It is the explicitly accepted exception in the threat model, and it is the only one.

Three answers, not two

decide(caller, node, action) returns one of three things:

AnswerMeansWhat a caller should do
{ ok: true, via, rung }granted by controls, rung, or delegationproceed
403determined: the answer is nochange who is asking, or the grant
503a lookup did not answerretry

The third answer is the unusual one. A membership lookup can fail, and swallowing that error turns an outage into an eviction for the invited teammate whose only authority path is that one lookup.

The ordering prevents it. An undetermined lookup sets a flag and does not short-circuit; the remaining paths still run; 503 is returned only when nothing granted. A failing membership query therefore never locks out an owner, whose controls already answered, and an undetermined lookup can never produce a 403. A 403 is a determined answer — usually a fact read from the tree, and for an anonymous caller or a malformed node id a determination reached before any lookup runs. A 503 is the system saying it could not read the tree.

What a Bearer key attests

A key is a bearer credential for an actor. It can do exactly what that actor can do — there is no second scope system layered over it. Identity is derived from the key, never from the request body: a slug or actorId in the payload is a claim, not an identity.

Keys come in two classes, fixed when the key is minted.

ClassMinted by
agentoneie init, oneie onboard, oneie setup, self-registration
humanone login, the browser device flow

An agent-class key reaches the substrate-work labels — things, paths, learning, workflows and the rest. A human-class key reaches those and the human-management labels as well. The class wall applies to a bare key: a caller carrying a session passes the gate whatever class its key is.

A bare agent-class key with no session is refused at the route edge for five declared auth labels: manage_workspace, manage_clients, manage_members, manage_groups, manage_actors — 41 of the 330 declared receivers. The refusal happens before any tree walk, so no amount of ownership changes it.

One carve-out: a bare agent-class key whose Bearer resolved to an actor may still call world:create-workspace, a manage_workspace receiver, when the payload names no parent — a new agent making its own home rather than managing someone else’s tenant. It pays for that in the payload: credit, plan, markup, cap, brand and starter agents are stripped, and a payload that named any of them lands on the free-tier grant. Everywhere else, the only self-service way out is a human-class key, which means one login and a person at a browser.

The codes, and which fix each one implies

Two refusals look alike and need opposite fixes.

401 — an error: "invalid_token" body, with an error_description naming where to re-exchange, and a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource. The Bearer began one- or osk_ and matched no key row. The credential is wrong: get a valid key.

403action_not_permitted from the API auth path, or problem+json forbidden from the gateway guard. A real, resolved key whose actor does not control the node. The credential is fine and the authority is not: change the grant, or ask about a node you control. Retrying with a fresh key minted by the same actor changes nothing.

One shape catches people out. On the receiver door (/api/ask/:receiver) an authority refusal arrives as HTTP 200 with outcome: "failure" and a result carrying error: "forbidden", receiver, required, callerRole, and a hint when the declared label has one — the status line is 200 because every client on that wire reads result out of a 200 and throws on a non-2xx. Read outcome, not the status. callerRole: "anonymous" there means the Bearer resolved to nobody; any other value means a real identity the walk refused.

Known limits

  • Delegation is declared but its reader is not wired. roles.tql defines the relation; the web decision path uses a stub that returns false. Today controls and rung are the two answers that grant.
  • 65 of the 330 declared receivers carry no auth label, and an absent label is treated as not requiring an attested caller. That default is deliberate for now — real anonymous surfaces depend on it — and it is a recorded gap, not a design.
  • The auth-label vocabulary is not the rung vocabulary. 29 distinct label strings appear across the catalog, mixing caller classes (public, session, agent_key) with action names (manage_clients, read_corpus). A label alone does not tell you whether your key clears it.
  • decide()’s rung step reads a membership at the node itself. The open-ancestor inheritance the schema describes is not part of that lookup.
  • The published OpenAPI spec declares no bearer scheme for a world key. Its securitySchemes are sessionCookie and serverSecret, so the credential every developer actually sends appears nowhere in it. See API.

Next

  • Receivers — what the auth label on a receiver declares
  • Errors and limits — every refusal shape, in one table
  • API — the door these decisions are enforced at
  • CLIone login and the two key classes