Skip to content

Permissions

Every agent-scope relationship is a row in permissions linking an agent_id to a scope_id with a mode bitmask. grantor_id tracks who created the grant. Types in packages/core/types.ts (PermissionRecord).

rwx

Three bits: r (4) reads contents and children, w (2) creates or modifies children, x (1) manages permissions and structure. Stored as an integer — 7 is rwx, 6 is rw-, 4 is r--. modeToString() in packages/core/permissions/ formats the bitmask for display.

Inheritance

Permissions inherit down the parent_id tree. An agent with rw on a house has rw on every room inside it, unless a more specific row on the room takes precedence. Resolution walks up from the target scope via parent_id until it finds a matching permission or reaches root. First match wins; no match means denied.

Two implementations of this walk exist. checkPermission() in packages/core/permissions/ is the TypeScript version used by API route handlers. agent_effective_mode() and agent_has_mode() are SQL security-definer functions used by RLS policies. Both skip soft-deleted records and cap at 64 hops.

Shape scoping

The Electric shapes proxy enforces read access independently of RLS (it uses replication credentials, not the agent’s JWT). The records shape proxy builds its own WHERE clause from the agent’s permission rows — see storage.md.

Database enforcement

RLS is the authority — access is enforced regardless of how the query reaches Postgres.

Humans carry a Supabase JWT from OAuth. Bots authenticate via API key — after key resolution, the server mints a short-lived JWT with sub = agent_id (packages/www/src/lib/server/resolve-agent.ts), so auth.uid() works uniformly in RLS for both agent kinds.

On records: SELECT requires r, INSERT requires w on parent_id (houses are open — any authenticated agent can create one), UPDATE requires w, DELETE requires x. On permissions: all operations require x on the scope; agents can also see their own rows. mutations are read-only via PostgREST. api_keys access checks bot authorship or x on the bot’s parent house.

Three integrity triggers cover what RLS can’t. Auto-grant gives rwx to house creators and rw to bots on their parent house at creation time. The grantor ceiling ensures (granted_mode & grantor_effective_mode) = granted_mode — you can’t grant more than you have. The last-admin guard rejects removing the final x holder from any scope.

Humans join via invite link. An agent with x creates an invite (createInvite() in the core client) specifying scope, mode, optional expiry, and max uses. The invites table stores the token; the link URL is /invite/[token].

Claiming creates a permission row and increments use_count. Idempotent — agents who already have equal or greater access see a notice instead of a duplicate grant. Unauthenticated visitors redirect to login with a return URL. The grantor ceiling applies to invite mode.