> ## Documentation Index
> Fetch the complete documentation index at: https://docs.impellabs.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Permissions

> One primitive for enrolment, roles, ownership and asking about someone.

Access is decided by **grants**. There is one grant table and one shape, and
every case your product has reduces to it:

> *(this holder)* may *(act)* on *(that resource)*.

| What you need                        | Holder          | Resource              |
| ------------------------------------ | --------------- | --------------------- |
| A learner enrolled in two programmes | principal       | `program:phy-101` ×2  |
| A whole cohort sees week three       | role `cohort_b` | `week:phy-101/w3`     |
| A tutor sees their own students      | role `tutor`    | `principal:…`         |
| A parent may ask about their child   | principal       | `principal:stu_88120` |
| A rep owns two accounts              | principal       | `account:acme` ×2     |

<Note>
  The fourth row is the one that earns the design. **A grant whose resource is
  another person** makes "ask about someone else" an ordinary entitlement,
  checked by the same code as everything else — instead of a relationship table
  with its own rules that has to be audited separately.
</Note>

## Resources are a tree

You name the kinds. A syllabus is a programme with weeks beneath it and days
beneath those; a CRM is accounts; a school is departments and cohorts.

```
program:phy-101
 └── week:phy-101/w3
      └── day:phy-101/w3/d2
```

A grant with `inherit` set covers the whole subtree, so enrolling somebody in a
programme reaches every week and day inside it without enumerating them. A node
optionally carries a body of knowledge; a week that carries none simply costs
nothing.

Cycles are **refused** when the tree is rebuilt, not tolerated. A loop in a
parent chain does not return a wrong answer — it hangs the request that walks
it.

## Roles include, they do not rank

A role reaches the grants of the roles it `includes`:

```
head_teacher  includes  tutor
```

A grant made to `tutor` is reachable by `head_teacher`. A grant made to
`head_teacher` is **not** reachable by `tutor`.

<Note>
  A numeric level was the obvious alternative and is worse: it is a second
  ordering that can disagree with the first, and "may a head of year read a
  bursar's ledger?" is not a question about magnitude. A lattice says exactly
  what it means, and it expresses a CRM's `manager ⊃ rep` with the same
  construct as a school's four levels.
</Note>

### Scoping a role to one subtree

One `tutor` role serves every tutor. Narrow the *assignment*, not the role:

```
assign tutor to ravi, scope = cohort:b12
```

The role's grants are intersected with that subtree, so Ravi reaches his own
cohort and nobody else's, and adding a tutor needs no new grants at all.

## Deny always wins

A grant can be an **allow** or a **deny**, and deny is subtracted last. That
lets you exclude one person from one week without unpicking the role that
everybody else depends on.

## How a request is resolved

1. The roles the principal holds, unexpired.
2. Everything those roles reach through `includes`.
3. Every grant held directly or by one of those roles.
4. Subtrees expanded for inheriting grants.
5. Role-scoped assignments intersected with their scope.
6. Deny subtracted.
7. What remains becomes the knowledge and the people this request may reach.

Resolution is cached for five minutes against a version that **every**
entitlement write bumps — so a revoked grant takes effect immediately rather
than when a cache expires.

## What retrieval does with it

<CardGroup cols={2}>
  <Card title="No end user" icon="globe">
    Nothing changes. The dashboard and the public widget behave exactly as they
    did before permissions existed.
  </Card>

  <Card title="An end user" icon="user-lock">
    Search is narrowed to what a grant names, in SQL, before ranking.
  </Card>
</CardGroup>

Filtering happens **before** ranking, not after. Filtering afterwards would mean
paying to embed and rank rows the asker may not see — and, worse, would make
"is it in the results" depend on how well it happened to match.

### Knowledge that belongs to no group

A workspace's profile, its FAQs and its hand-typed cards belong to no group, and
by default they answer for everyone. That is right for an assistant talking to
the public and wrong for a learner.

So it is a per-workspace policy:

| Setting              | Behaviour                                               |
| -------------------- | ------------------------------------------------------- |
| `tenant` *(default)* | Ungrouped knowledge answers for anyone in the workspace |
| `strict`             | A principal sees only what a grant names                |

<Warning>
  The default is `tenant` because that is what every existing workspace already
  does, and flipping it silently would take live assistants quiet. If your
  product has end users who must not see each other's material, set `strict`
  deliberately — and expect some answers that used to work to stop.
</Warning>

## It fails closed

An entitlement lookup that cannot be resolved yields an **empty** scope, not an
open one. A request that the platform knows belongs to an end user but cannot
identify reaches nothing.

<Note>
  This is the opposite of how workspace scoping behaves, and the difference is
  intentional. A background job with no workspace pinned is trusted code and
  reads across the platform. An end user is by definition untrusted, and the
  cost of guessing wrong is showing one person another person's records.
</Note>

## Tools are stricter than knowledge

A role grants tool access explicitly, and **an empty list means no tools**.

Knowledge keeps working when nobody has configured anything, because the cost of
being wrong is a thinner answer. A tool can move money or change somebody's
record, so the default there is silence.
