ADR-0008 — Workarea algebra: per-tenant universe tree, node-id grants, deny semantics, edges
Status: accepted design · 2026-10-29
Context
Where memory lives must itself be governed, and by one object, not several. Splitting the job —
say, between a flat operator-created container that bounds grants and review policy, and a path
inside it that an agent opens by writing into it — puts a seam through every write: multiple
checks fed by different folds, grants naming containers by string, and relations between areas
bolted on later. OML is multi-tenant
(ADR-0007), has no single operator, and must answer subtree questions — "grant this crew
everything under experiments", "freeze this whole branch" — that a flat set of named spaces
cannot express.
This record decides one object: the workarea, a governed node in one tree per tenant. A
workarea exists before it is written to — created by a journalled admin act, never conjured by a
typo, so a write naming an unknown workarea is refused — and a claim lives in exactly one
workarea. The rules this record decides are declared in full, with worked examples, on
architecture/workareas.md; this record states the decisions and
what was rejected. Nothing here is built.
Decision
-
d1 — One tree per tenant, rooted at
universe. Every workarea of a tenant is a node in exactly one tree, rooted at that tenant'suniversenode. The universe root is permanentlycommitted, cannot be frozen, deleted or re-parented, and refuses direct claim writes — claims live in crew workareas beneath it. It exists so that paths, subtree grants and subtree denies have a well-defined top, not so that anything is stored at the top. The tree never crosses a tenant boundary; cross-tenant "whole world" mapping is explicitly future (federation, RS-3 in the roadmap). -
d2 — Inclusion is single-parent. Every node except the universe has exactly one parent. Cycles are impossible by construction, every node has one derivable path from the root, and every subtree question — containment, grant coverage, deny masking, the child-status rule of ADR-0009 — has exactly one answer.
-
d3 — Grants bind to node ids, never paths. A grant names a workarea node id; the path (Postgres
ltree) is derived from the tree at check time. Renaming or re-parenting a node never invalidates or rescopes a grant, and a path string is never an authority — anything that compares paths is a derived view as of the current permission epoch, reproducible from journalled state. -
d4 — Effective access is a union of attenuated grants, then deny-wins. Effective access = the union of a key's grants, each grant first attenuated by its own deny entries, minus anything named by a global per-key deny object. A deny entry lives inside a grant, names a subtree by node id, and masks it within that grant only; a global per-key deny is a distinct journalled object with deny-wins semantics over any union. Every effective-access computation is reproducible from journalled grants, and a permission epoch invalidates caches on any change. The formula, its evaluation order and a worked example are declared on architecture/workareas.md.
-
d5 — Relations are labelled edges that confer zero access, governed under R2.
relatedis a separate edge kind between two workarea nodes with a closed label set (relates_to,continues,derived_from,mirrors). An edge is recorded structure for lineage and navigation, never an input to access computation. Edges follow the propose/ratify split: proposing requiresdreamer(propose)effective access on at least one endpoint and reader effective access on both; ratifying requiresdreamer(review)on both endpoints; the ratifier is never the proposer — R2 extends to edges (ADR-0001 d2). -
d6 — Re-parenting is a journalled admin act with a single-transaction path rewrite. The move event, the parent-pointer change and the descendant
ltreerewrite commit in one Postgres transaction, so no read observes a tree in which a path and its parent pointer disagree. Because of d3 no grant is touched; the act bumps the permission epoch because cached subtree resolutions computed against the old paths are now wrong. The cost is a lock proportional to the moved subtree, accepted. Re-parenting into avalidateddestination is refused (ADR-0009). -
d7 — A workarea records its crew; the record is operated by nobody. Crew name, charter URI and external manager are recorded metadata — OML never contacts the manager, schedules the crew or validates the charter beyond storing it. Crew-metadata update is a journalled admin act and is permitted on
validatedareas: freeze covers claim content and promotions, not workarea metadata. The reviewer-independence axis (architecture/admission.md) resolves reviewer crew and manager from this journalled metadata at promotion time, which is why its updates must be journalled acts rather than free edits.
Consequences
- The schema realizes d1–d7 as
workareas(id, tenant, parent_id, derivedltreepath, status, crew jsonb),workarea_edges(pending/ratified, proposer, ratifier),grants(key, role, workarea node id, deny entries, epoch) andkey_denials(global per-key deny objects) — data/postgres-schema.md. RLS by tenant applies to all of them (ADR-0007). - Every access check in the system — write admission, recall scoping, graph reads, edge acts — computes d4's formula and nothing else. There is no second access mechanism to keep consistent, and every answer is reproducible from journalled grants at a named permission epoch, which is what the evidence export relies on (architecture/audit-and-lineage.md).
- d3 makes rename and re-parent cheap for authority and pays for it in d6's path rewrite: the
derived-path column must be maintained transactionally, and the lock cost of moving a large
subtree is real. Lake rows keep
workarea_idas the stable key with path-at-write as history, so re-parenting never rewrites the lake (architecture/storage.md). - d4's per-grant deny masking means a deny does not compose across grants: operators who want an absolute prohibition must use the global per-key deny, and the documentation must say so where grants are administered (cli/reference.md). This is a deliberate trade: grants stay self-contained statements of scope at the cost of one more object kind.
- d1 defers the stated end goal: relating what separate tenants' crews learned requires a design for claims crossing a tenant boundary — review, attribution, erasure — and is tracked as RS-3 in the roadmap. Until that row closes, every page that mentions the whole-world goal marks it future.
Rejected alternatives
- DAG membership (a workarea with multiple parents). Letting one area sit under two parents
would let one crew's space appear in two branches without edges. Rejected because access
computation becomes ambiguous: with two parents, a node's path is not unique, so "the subtree a
grant covers" and "the subtree a deny masks" stop having one answer, and a deny inherited down
one path can collide with a grant inherited down the other with no principled resolution order.
d5's labelled edges give the expressiveness (one area
mirrorsorcontinuesanother) without touching access, which is the actual need the DAG would have served. - Path-bound grants. Binding a grant to a path string is simpler to implement and to read in an audit row. Rejected on the rename-rescoping failure: the moment a node is renamed or re-parented, a path-bound grant either silently detaches from the subtree it was issued for or silently attaches to whatever now occupies the old path — both are access changes nobody journalled. Name-keyed state has a second failure: once rows, keys and links are recorded by name, a name becomes an identity that can never safely be reused. Node ids make the grant follow the object, and the path stays a derived view (d3).
- One global universe across tenants. A single shared tree with tenants as top-level branches
would make the "whole world" goal a present-tense property. Rejected because it contradicts
tenancy as designed: RLS keys every row by
tenant_id(ADR-0007), and a tree whose root spans tenants is a row no tenant's policy can own — every subtree computation at the root would read across boundaries that D6's isolation exists to make uncrossable. The whole-world goal is real and stays stated, as federation between per-tenant universes, explicitly future: RS-3 in the roadmap.