---
title: "Classification and deny rules"
---

# Classification and deny rules

How classification levels are inherited across the organisation, which DLP action each level applies, who may read a space, and how organisation deny rules refuse Context actions.

## Classification levels

Every space and source has an effective classification, from least to most sensitive: `public`, `internal`, `confidential`, `restricted`.

- **Own values.** A project's classification is set on the project; other nodes (organisation, department, team, group) have a value on their Context space. The organisation root always has one (default `internal`) and cannot be cleared.
- **Inheritance.** A node's inherited value is the strictest of its own value and **all** its ancestors': project, owning team, department, organisation; a group continues at its parent groups and team.
- **A source's value** is the strictest of the inherited value, the source's own value and the `.keystone.yaml` declaration in its repository.
- **Tighten only.** A space or source can only raise its level. A value below what it inherits, or a weaker DLP action, is refused with `context.classification.loosening`.

## DLP action per level

| Effective classification | DLP action |
|---|---|
| `public`, `internal` | `redact`: findings of the organisation's DLP patterns are replaced by markers |
| `confidential` | `drop`: chunks with a finding are left out |
| `restricted` | `block_source`: one finding withholds every chunk of that source from the response |

A source of a **regulated project** (a project with at least one compliance framework) gets at least `drop`. A source's own `dlpAction` (`redact`, `drop` or `block_source`) can only make it stricter.

Fail closed: a missing organisation root, an unknown stored value, a source with an unsupported `dlp_policy_id` or any lookup error drops the source's chunks.

### Set a classification

Both routes need `context:space:admin`, enforced:

```http
PUT /api/v1/context/spaces/{spaceId}/classification
{"classification": "confidential"}

PUT /api/v1/context/spaces/{spaceId}/sources/{sourceId}/policy
{"classification": "restricted", "dlpAction": "block_source"}
```

`null` clears an own value. A project space is classified on the project, not here.

Raising a level takes effect in the same request: vectors the new level no longer permits are removed, the affected sources are marked stale (full-text only) and re-indexed. A level raised outside the Context API (a project's classification or compliance frameworks, a hierarchy move) is applied by the 15-minute maintenance scan.

Frozen lenses and snapshots are re-scanned with the stricter of their captured level and the source's current level: a raised level applies to them, a lowered one never loosens frozen content.

## Who may read a space

Besides the role grant, reading or searching a non-personal space needs a membership path to its node, by the node's inherited classification:

| Classification | Who may read |
|---|---|
| `public`, `internal` | Members of the node; for a project space also members of its owning team; administrators of the node or an ancestor; super admins and holders of an explicit `context:space:admin` grant on the space (own organisation only) |
| `confidential` | Members of the node; for a project space also members of its owning team. Administrator roles and permissions do not grant read |
| `restricted` | Members of the node itself only: no inherited read, not even the owning team, and no administrator override, super admins included |

- Membership: organisation membership for the organisation space; a team of a department for the department; a Directory team membership (direct or through groups, with nesting) for a team; group membership for a group; project membership for a project.
- Administrators of a node are people with a role binding that grants `context:space:admin` on that node or above it. Members of a department do not read its team spaces, and teams do not see each other's spaces.
- Personal spaces are owner-only in every case.
- A space whose node no longer exists, or whose classification cannot be resolved, is not readable. A database error answers `503 context.authz.unavailable`.

The model applies everywhere a space is read: retrieval (lenses, frozen lenses, pair sessions, the MCP endpoint and the agent routes included), the space and source API, project context, snapshots and handoffs, and other people's attribution records.

## Organisation deny rules

Deny rules refuse Context actions on chosen nodes for everyone in the organisation, super admins included. They are checked before every retrieval and in every rollout mode. They are written as `context_platform_deny` on an organisation-level policy:

```json
PUT /api/v1/hierarchy/policies
{
  "level": "org",
  "entity_id": "<org id>",
  "policy": {
    "context_platform_deny": [
      {"actions": ["context:space:search"], "node_kinds": ["team"], "node_ids": ["<team id>"], "reason": "legal hold"}
    ]
  }
}
```

| Key | Type | Meaning |
|---|---|---|
| `actions` | string array, required | `context:space:read`, `context:space:search`, `context:space:write`, `context:space:admin`, `context:space:*`, `context:*` or `*` |
| `node_kinds` | string array | `org`, `department`, `team`, `group`, `project`, `user`; empty or absent means every kind |
| `node_ids` | string array | Empty or absent means every node |
| `reason` | string | Shown in denial logs |

Any other key, a value that is not an array, or a non-string entry makes the rule invalid. Rules are validated when they are proposed (through `PUT /api/v1/hierarchy/policies` or `POST`/`PUT /api/v1/policies`), and an invalid rule is refused with `400 validation_error` naming the rule; nothing is stored.

A stored document that does not parse (written before validation existed) makes every Context authorisation fail closed with `503 context.authz.unavailable` until it is fixed. `GET /api/v1/context/deny-rules` (organisation administrators) shows the effective rules as authorisation parses them, and `valid: false` with the parser's message when they do not parse.
