# Part 15 — who is allowed to do what

Signatures answer _who wrote this?_ Until now, nothing answered _were they
allowed to?_ Inside a shared graph, every member was therefore effectively an
owner: anyone holding the key could rotate it out from under everybody or
overwrite anyone's work, and the only thing stopping them was your UI not
offering the button.

This part is about what can honestly be enforced, which is narrower than a
role matrix and worth understanding before you design around it.

## The line, stated plainly

A member who holds a graph key **can read it**. That is arithmetic, not
policy, and no permission system changes it. Anyone who tells you otherwise is
selling you a UI.

What a member cannot do is reach another device except through the relay. So
the relay is the only place a permission is real, and the operations it
mediates are the ones that can be _prevented_ rather than disapproved of
afterwards:

```mermaid
flowchart LR
  subgraph P["preventable — the relay mediates it"]
    W["push a record"]
    B["upload or delete a blob"]
    R["mint a new key epoch<br/>(rotation, and therefore removal)"]
  end
  subgraph N["not preventable — no chokepoint exists"]
    RD["reading what they hold the key for"]
    OW["overwriting a record<br/><i>the relay cannot see whose it is</i>"]
    KD["which KIND a record is<br/><i>the relay cannot read it</i>"]
  end
  P --> Y["refused: 403 not-permitted"]
  N --> Z["your app's convention, and nothing more"]
```

Two capabilities, because those are the two that map onto operations the relay
can actually see:

| capability | what it gates                                     | who needs it        |
| ---------- | ------------------------------------------------- | ------------------- |
| `write`    | pushing records, uploading and deleting blobs     | contributors and up |
| `rotate`   | minting a new key epoch — how somebody is removed | admins              |

Which gives the three seats an organisation actually asks for: **read-only**
(neither), **contributor** (`write`), **admin** (both).

## Turning it on

argala does not own your roles, just as it does not own your users. Roles
arrive through the same `Directory` seam as membership:

```ts
import { SqliteDirectory, type Capability } from '@argala/relay';

// Using argala's own directory:
directory.setCapabilities('acme', 'sam', ['write', 'rotate']); // admin
directory.setCapabilities('acme', 'jo', ['write']); // contributor
directory.setCapabilities('acme', 'auditor', []); // read-only

// Or against the roles you already have — an IdP, a permissions table:
class MyDirectory implements Directory {
  async capabilitiesOf(groupId: string, memberId: string): Promise<Capability[] | null> {
    const role = await myRoles.for(groupId, memberId);
    if (!role) return null; // unmanaged: everything, as before
    return role === 'admin' ? ['write', 'rotate'] : role === 'member' ? ['write'] : [];
  }
}
```

**A directory that does not implement `capabilitiesOf` grants everything**, and
so does returning `null`. That is deliberate: a permission model that silently
locks out every existing user on upgrade day is not a feature. `null` also
lets you govern one group and ignore the rest.

**Personal and pairwise graphs are never asked about.** `u` has one member and
`d:` has two peers, so a capability check there could only lock somebody out of
their own data.

## What your app sees

A refused push does **not** throw and does not stop the sync:

```ts
engine.on('error', (e) => {
  if (e.scope === 'policy.readonly') showReadOnlyBanner(e.graphId);
});
```

The queue is held rather than dropped, and **the pull still runs**. That detail
is central to the design: a client that threw here would abandon the sync run
before reading, so a read-only seat would slowly stop receiving other people's
records, turning a permission feature into a disconnection. If the seat is
widened later, the held writes are pushed.

Do not rely on the banner alone. A read-only member's app should not offer the
button; the relay's refusal is the backstop for the stale tab, optimistic UI,
or forgotten background job.

## What this does not do

- **It does not stop a member reading.** They hold the key. If somebody must
  not read a graph, they must not be in it — remove them and rotate.
- **It does not give per-record ownership.** A member with `write` can
  overwrite or delete another member's records. Deciding otherwise means
  knowing whose record it is, and the relay cannot read one to find out.
- **It does not restrict by kind or shard.** Same reason.
- **It is not a defence against a malicious relay.** The relay asks the
  question and applies the answer, so a hostile one grants itself whatever it
  likes. This stops a _member_, which is the adversary an organisation
  actually has — and it composes with the checks that do watch the relay
  ([Part 13](https://docs.thinkingmachinelabs.dev/argala/guide/catching-a-dishonest-relay.md)).
- **It is not retroactive.** Demoting somebody to read-only stops their next
  write. Everything they already wrote stays exactly where it is.

---
