RBAC System Design
Version: 2.0 (redesign) Status: Design Supersedes: spec.md RBAC section, task 002/003 RBAC designs
1. Overview
The RBAC system controls authorization for all events in an enclave. Every event — content events (message), AC events (Move, Grant, Revoke), KV events (Shared, Own), and lifecycle events (Pause, Terminate) — is authorized through the manifest. See Section 5 for manifest format and Section 6 for validation rules.
1.1 Terminology
The old unified "role" is retired. Three column types replace it:
| Concept | Convention | Examples | Encoding | Changed by | Semantics |
|---|---|---|---|---|---|
| State | UPPER_CASE | PENDING, MEMBER, BLOCKED | 8-bit enum | Move | WHERE you are. Base permissions. Mutually exclusive. |
| trait | lower_case | owner, admin, muted | Flag bits | Grant / Revoke / Transfer / init | WHAT modifies you. Additive or deny ops. Ranked. |
| Context | PascalCase | Self, Sender, Public | System-evaluated | (implicit) | System condition. Determined at authorization time. |
1.2 Operations
Six operations apply to all event types:
| Op | Meaning | Deny |
|---|---|---|
| C | Create | _C |
| R | Read | _R |
| U | Update | _U |
| D | Delete | _D |
| N | Notify (lightweight, human clients) | _N |
| P | Push (full delivery, service endpoints) | _P |
Positive ops grant capabilities. Negative (deny) ops revoke capabilities. The sign comes from the schema entry, not the bitmask.
2. Bitmask Encoding
All RBAC state for an identity is stored in a single bitmask value in the SMT (namespace 0x00), keyed by the identity's public key.
bits 0-7: State enum
bits 8+: trait flags2.1 State Enum (bits 0-7)
8-bit integer field. All 8 bits together encode a single value. Mutually exclusive by structure — an identity is in exactly one State.
| Value | Name | Description |
|---|---|---|
| 0 | OUTSIDER | Not in the enclave. No SMT leaf exists. Protocol-reserved. |
| 1-255 | (app-defined) | Custom States defined in manifest. E.g., 1=PENDING, 2=MEMBER, 3=BLOCKED. |
OUTSIDER (0) is the only protocol-reserved State. All other States — including blocking/banning — are app-defined.
State values 1-255 are assigned sequentially from the manifest states array. The first State in the array gets value 1, the second gets value 2, etc.
2.2 trait Flags (bits 8+)
Independent flag bits. Each bit represents one trait. Multiple traits can be set simultaneously.
Bit positions are assigned sequentially from the manifest traits array: the first trait gets bit 8, the second gets bit 9, etc.
Each trait declares a rank using the syntax name(N) where N is a non-negative integer. Lower number = higher rank. Multiple traits may share the same rank (peers). Rank determines targeting authority: an operator can only target identities ranked strictly below them (see §7 Rank Rule).
All traits are managed uniformly:
- Assigned via
init,Grant, orTransfer. - Removed via
Revoke,Transfer, orMove(clears all trait flags unlesspreserve: true). - Rank determines targeting authority (lower number = higher authority).
The bitmask does not distinguish positive from negative traits. A "muted" trait flag at bit 10 is stored identically to an "admin" trait flag at bit 9. The sign (positive or negative ops) comes from the schema entry, not the bitmask.
2.3 SMT Leaf Lifecycle
- Join: Move(OUTSIDER, X) creates a new SMT leaf with State=X and no trait flags.
- State change: Move(X, Y) updates the State enum and clears all trait flags (unless
preserve: true). - Leave: Move(X, OUTSIDER) sets State=0 and clears all traits. If bitmask becomes 0, leaf is deleted.
- Service account: Granting a trait to an OUTSIDER creates a leaf with State=0 and trait bit set.
- Leaf deletion: SMT leaf is deleted when the entire bitmask == 0 (no State, no traits).
- No leaf: An identity with no SMT leaf is implicitly OUTSIDER with no traits.
Historical membership is provable via the CT log.
3. Contexts
Contexts are system-evaluated conditions used as columns in the schema. They are not stored — they are determined at authorization time.
3.1 Self
- Matches when: The actor is targeting their own identity (
event.from == content.target). - Use case: AC events — leaving a group (Move Self), stepping down from a trait (Revoke Self).
3.2 Sender
- Matches when: The actor authored the referenced event (
event.from == original_event.from). - Use case: Content events — editing/deleting own messages (message Sender), removing own reactions (reaction Sender), updating own KV slot (Own Sender).
3.3 Public
- Matches when: Always. Any identity, any State, including OUTSIDER.
- Use case: Public read access to group messages, public enclave manifests.
4. Authorization Algorithm
The authorization algorithm determines whether an identity can perform a specific operation on a specific event type.
isAuthorized(identity_bitmask, event_type, operation, is_self, is_sender) → bool
1. Extract State from bitmask (bits 0-7).
2. Collect allowed ops:
a. State ops (from State column for identity's current State)
b. trait positive ops (OR across all held trait flags)
c. Self ops (if is_self == true: event.from == content.target)
d. Sender ops (if is_sender == true: event.from == original_event.from)
e. Public ops (always applies)
3. Collect denied ops:
negative ops from all sources (State _X, trait _X, Self _X, Sender _X, Public _X)
4. Compute effective:
effective = allowed − denied
5. Return: operation ∈ effectiveNo State gate. traits can extend any State with new capabilities. Safety against invalid State+trait combinations comes from operational constraints:
- Move clears all traits
- Grant validates target State (schema-defined)
4.1 Deny Override
Negative ops always win over positive ops regardless of source. If the effective set has both C (from State) and _C (from a trait, State, or Context), the result is: C is removed.
5. Manifest Format
5.1 Manifest Sections
A manifest declares all RBAC rules for an enclave. Ten sections:
| Section | Purpose | Entry format |
|---|---|---|
states | App-defined States | Array of UPPER_CASE names. Assigned enum values 1+ sequentially. |
traits | App-defined traits | Array of name(rank) strings. Assigned bit positions 8+ sequentially. Lower rank = higher authority. |
readers | Read access | [{ type, reads }]. Each entry declares R ops for one column. |
init | Initial identities | { identity, state, traits[] }. Bootstrap SMT at enclave creation. |
moves | State transitions | { event, from, to, operator, ops }. Optional alias and gate. |
grants | trait assignment rules | { event (Grant/Revoke), operator[], scope[], trait[] } |
transfers | Atomic trait movement | { trait, scope[] }. Operator must hold the trait. |
slots | KV state (Shared/Own) | { event (Shared/Own), operator, ops, key } |
lifecycle | Enclave lifecycle | { event (Pause/Resume/Migrate/Terminate), operator, ops } |
customs | App-defined content events | { event, operator, ops } |
5.2 Section Details
readers — Column-oriented read declarations. Each entry is { type, reads }:
type: A declared State, trait, or Context.reads: Array of event type names this column gets R on, or"*"for all events.
moves — Each entry authorizes one State transition for one operator.
grants — Each entry authorizes Grant or Revoke of specific traits, scoped to specific States.
operator: Who can grant/revoke (array — Contexts, traits, or States).scope: Which States the target must be in (array).trait: Which traits can be granted/revoked (array).
transfers — Each entry authorizes atomic movement of a trait from one identity to another.
trait: Which trait can be transferred.scope: Which States the target must be in (array).- The operator is implicit: must hold the trait being transferred.
slots — KV state entries. Each entry requires a key field (lowercase, app-defined).
Shared: Enclave-wide singleton per key. Last write wins.Own: Per-identity slot per key. Node enforces per-identity isolation.- Protocol-reserved keys:
gate:*,lifecycle. Apps cannot write to reserved keys.
lifecycle — Protocol-defined enclave lifecycle events. State stored in Shared("lifecycle").
customs — App-defined content events (lowercase names). Standard { event, operator, ops } entries.
5.2.1 Naming Convention
- Protocol events: PascalCase (Move, Grant, Shared, Pause, etc.)
- App events: lowercase (message, request, sent, etc.)
- States: UPPER_CASE (MEMBER, PENDING, BLOCKED, etc.)
- traits: lower_case (admin, muted, etc.)
- Contexts: PascalCase (Self, Sender, Public)
- KV keys: lowercase (topic, profile, etc.)
5.2.2 Common Entry Properties
Any entry in any section may include:
alias: Optional. Names the entry for referencing (Gate keys, UI, logs, API).gate: Optional.{ operator[] }— declares who can toggle the gate. Requiresalias. Gate state stored inShared("gate:<alias>"). When gate is closed, the event is rejected before RBAC runs.preserve: Optional (movesonly). Iftrue, trait flags are kept after the Move. Defaultfalse(clear all traits).
6. Validation Rules
- In and Out — Every State in
statesmust appear astoin at least onemovesentry or be assigned in at least oneinitentry (in). Any State with no ops must also appear asfromin at least onemovesentry (out). - No Stuck Traits — Every trait in
traitsmust have at least one assign path (Grant entry or Transfer entry) and at least one remove path (Revoke entry or Transfer entry). Traits only assigned ininitare exempt from the assign path requirement. - Valid Operators — Every
operatormust be a declared State, declared trait, or a Context (Self, Sender, Public). - Read/Write Completeness — Every event must have at least one operator with C ops and at least one operator with R ops.
- Reserved Keys — App-defined
slotskeys must not use reserved prefixes (gate:,lifecycle). - Gate Requires Alias — If an entry has
gate, it must havealias. - Valid Ranks — Every trait must declare a rank via
name(N)syntax. All rank values must be non-negative integers. - Complete States — Every State name referenced in the manifest (
movesfrom/to,grantsscope,transfersscope,initstate) must be either declared instatesor be OUTSIDER.
7. Processing Pipeline
The full event processing pipeline, from submission to commitment:
1. Signature verification (is the event signed by the claimed identity?)
2. Duplicate check (has this event already been committed?)
3. Lifecycle check (is the enclave active, not paused/terminated?)
4. Gate check (is this event type/transition currently enabled?)
5. RBAC authorization (Section 4 algorithm)
6. Rank check (for AC events targeting another identity — see Rank Rule below)
7. Content validation (event-type-specific format checks)
8. State mutation (apply bitmask changes for AC events)
9. SMT update (write new bitmask, compute new root)
10. CT append (add event to the certificate transparency log)
11. Commit (finalize sequence number, return receipt)Rank Rule. For any AC event (Move, Grant, Revoke) targeting another identity: if the operator holds any trait, and the target holds any trait, the operator's best rank (lowest rank number among held traits) must be strictly less than the target's best rank. If either party holds no traits, or the operator is Self-targeting, the rank check is skipped. Violation → reject (RANK_INSUFFICIENT).
8. Event Processing
When reading a target's bitmask from SMT, if no leaf exists, treat the bitmask as 0x00 (OUTSIDER, no traits).
8.1 Move
Changes an identity's State. Clears all traits. The core lifecycle operation.
Event content:
{ "target": "<target_pub>", "from": "PENDING", "to": "MEMBER", "preserve": true }The preserve field is a manifest entry selector, not a behavioral directive. The node matches the content's from, to, and preserve against the moves entries to find the authorizing entry. Two entries with the same from→to but different preserve values are distinct Moves.
Processing:
1. Resolve from/to State names to enum values.
2. Match against moves entries: find the entry where from, to, and preserve
all match. If no match → reject (UNAUTHORIZED).
3. Read target's current bitmask from SMT.
4. Verify: current State enum == from value.
If mismatch → reject (STATE_MISMATCH).
5. Compute new bitmask:
a. Set State enum (bits 0-7) to the "to" value.
b. If the matched moves entry has preserve: true → keep all trait flags.
Otherwise (default): clear all trait flags.
6. If entire bitmask == 0: delete SMT leaf.
Else: write new bitmask to SMT.Authorization: Move events are authorized via the moves section of the manifest. Each entry specifies from, to, operator, ops, and optionally preserve. The content's from, to, and preserve fields together select the matching entry.
Move matching: State is an enum. The check is exact: current_state == from. trait bits are ignored. No CAS on the full bitmask — only the State enum is compared.
Application payload: Move content may carry additional application-defined fields alongside the required target, from, and to fields. The node does not interpret these fields — they are opaque payload for the application layer. Example: DM enclaves carry epoch key-establishment data in Move events (see app/dm.md).
8.2 Grant
Adds a trait flag to an identity's bitmask.
Event content:
{ "target": "<target_pub>", "trait": "admin" }If the trait has P (Push) ops in the schema, the event includes an endpoint:
{ "target": "<target_pub>", "trait": "dataview", "endpoint": "https://..." }Processing:
1. Resolve trait name to bit position (from manifest traits array).
2. Read target's current bitmask from SMT.
3. Verify: target's current State is in the allowed scope.
If not → reject (INVALID_STATE_FOR_GRANT).
4. Rank check (see §7 Rank Rule).
5. Set the trait bit: new_bitmask = current | (1 << trait_bit).
6. If trait has P ops in schema AND endpoint is provided: register push endpoint (operational state, not in SMT).
7. Write new bitmask to SMT (create leaf if it doesn't exist).Authorization: Grant events are authorized via Grant schema entries. Each entry specifies operator (who can grant), scope (what States the target must be in), and trait (what traits can be granted).
No separate Grant_Push event type. The node detects P ops from the schema and handles endpoint registration as part of regular Grant.
8.3 Revoke
Removes a trait flag from an identity's bitmask.
Event content:
{ "target": "<target_pub>", "trait": "admin" }Processing:
1. Resolve trait name to bit position.
2. Read target's current bitmask from SMT.
3. Rank check (see §7 Rank Rule).
4. Clear the trait bit: new_bitmask = current & ~(1 << trait_bit).
5. Write new bitmask to SMT.Revoking a trait the target doesn't have is a no-op (bit was already 0).
Self is allowed as operator for Revoke (voluntarily drop own trait).
8.4 Transfer
Atomically moves a trait from one identity to another. The operator must hold the trait being transferred.
Event content:
{ "target": "<target_pub>", "trait": "owner" }Processing:
1. Verify operator holds the trait being transferred.
2. Verify target is not the operator (no self-transfer).
If same → reject (INVALID_TRANSFER_TARGET).
3. Read target's current bitmask from SMT.
4. Verify target does not already hold the trait.
If already held → reject (TRAIT_ALREADY_HELD).
5. Verify: target's current State is in the allowed scope.
If not → reject (INVALID_STATE_FOR_TRANSFER).
6. Clear trait bit on operator's bitmask.
7. Set trait bit on target's bitmask.
8. Write both bitmasks to SMT.Authorization: Transfer events are authorized via the transfers section. Each entry specifies trait (which trait can be transferred) and scope (which States the target must be in).
8.5 Gate
Pre-RBAC check that enables/disables specific events at the enclave level. Gates are open by default.
Event content:
{ "gate": "applications", "open": false }Processing:
1. Verify submitter is an allowed operator for this gate (from manifest).
2. Update gate state in KV Shared: Shared("gate:<id>") = open value.Gate state is checked BEFORE the RBAC authorization algorithm. If a gated event type/transition is closed, the event is rejected before RBAC runs.
8.6 AC_Bundle
Atomic multi-event bundle. All events succeed or none apply.
Event content:
{
"events": [
{ "event": "Move", "target": "<pub>", "from": "PENDING", "to": "MEMBER" },
{ "event": "Grant", "target": "<pub>", "trait": "admin" },
{ "event": "Grant", "target": "<pub>", "trait": "moderator" }
]
}Processing:
1. For each event in order:
a. Validate against simulated intermediate state.
b. Apply to simulated state.
If any event fails → reject entire bundle.
2. If all pass: apply all changes atomically to SMT.8.7 Lifecycle Events
Lifecycle events control enclave-level state. They store their state in KV Shared (SMT namespace 0x02) with the reserved lifecycle key.
Pause
Stops the enclave from accepting new events (except Resume).
Event content:{ "event": "Pause" }1. Verify current lifecycle state is "active".
If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "paused".Resume
Lifts a pause, restoring normal operation.
Event content:{ "event": "Resume" }1. Verify current lifecycle state is "paused".
If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "active".Migrate
Initiates migration of the enclave to a new node.
Event content:{ "event": "Migrate", "target_node": "<node_pub>" }1. Verify current lifecycle state is "active".
If not → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "migrating".Terminate
Permanently shuts down the enclave. Irreversible.
Event content:{ "event": "Terminate" }1. Verify current lifecycle state is not "terminated".
If terminated → reject (INVALID_LIFECYCLE_STATE).
2. Update KV Shared: Shared("lifecycle") = "terminated".Lifecycle Check
Lifecycle state is checked at step 3 of the processing pipeline (Section 7), before Gate and RBAC checks:
Lifecycle check → Gate check → RBAC authorization| State | Accepts events? |
|---|---|
| active | All events |
| paused | Only Resume (from owner) |
| migrating | Node-managed (implementation-defined) |
| terminated | No events |
8.8 KV Events (Shared / Own)
Mutable key-value state stored in SMT namespace 0x02. KV events are NOT content events — they form their own category. Authorization uses the same schema matrix as all other events.
8.8.1 Problem
Content events are append-only (the CT is a log). To represent "current topic" or "my display name", apps would need to scan the CT for the latest event. KV events provide first-class mutable state with SMT inclusion proofs for the current value.
8.8.2 Shared
Enclave-wide singleton. One slot per key per enclave. Last write wins.
Event content:{ "key": "topic", "value": "General Discussion" }SMT key: H(0x02 || key_name)
SMT value: H(content)
Any actor with C on Shared (for matching key) can overwrite. History preserved in CT.
8.8.3 Own
Per-identity slot. One slot per key per identity. Each identity can only write to their own slot.
Event content:{ "key": "profile", "value": { "display_name": "Alice", "status": "Available" } }SMT key: H(0x02 || key_name || commit.from)
SMT value: H(content)
Per-identity isolation enforced by the node — the from in the SMT key is always commit.from. No identity field in content needed.
8.8.4 Schema Representation
KV events use the standard schema format with an additional key field:
{ "event": "Shared", "operator": "admin", "ops": ["C"], "key": "topic" }
{ "event": "Shared", "operator": "MEMBER", "ops": ["R"], "key": "topic" }
{ "event": "Own", "operator": "MEMBER", "ops": ["C"], "key": "profile" }
{ "event": "Own", "operator": "Sender", "ops": ["U"], "key": "profile" }The key field constrains which KV slot the operator can access.
| Op | Meaning |
|---|---|
| C | Create or overwrite the value |
| R | Read the current value |
| U | Update an existing value |
| D | Clear the value (remove SMT leaf) |
| P | Push delivery on value change |
| N | Notify on value change |
8.8.5 Delete (D)
- Shared: Remove SMT leaf at
H(0x02 || key). Value cleared. - Own: Remove SMT leaf at
H(0x02 || key || commit.from). Only your own slot.
8.8.6 Protocol-Reserved KV Keys
Gate state (Section 8.5) is stored as KV Shared with the gate: key prefix:
Shared("gate:applications") = true/falseGate and lifecycle KV slots are protocol-managed — written by Gate and lifecycle events, not arbitrary Shared writes. Reserved keys: gate:* prefix and lifecycle.
8.8.7 Bounding
No dynamic keys. Only keys declared in schema entries can be written:
- Shared: bounded by manifest (only declared keys)
- Own: bounded by RBAC (one slot per key per identity, identities controlled by Move)
8.8.8 KV vs Content Events
| Aspect | Content Event | KV Event |
|---|---|---|
| History | Full history matters | Only current value matters |
| SMT | No SMT entry | SMT leaf tracks current hash |
| Proofs | CT inclusion only | SMT inclusion proof for current state |
| Use case | Messages, reactions | Topic, settings, profiles, status |
Both live in the same CT. KV events maintain a "current state" view in SMT 0x02.
8.8.9 Read API
- Shared:
GET /enclave/{id}/kv/{key} - Own:
GET /enclave/{id}/kv/{key}/{identity}
The SMT provides inclusion proofs for the current value hash.
9. Example Manifest
This is the Group Chat enclave manifest from group.md. For other examples, see personal.md and dm.md.
{
"states": ["PENDING", "MEMBER", "BLOCKED"],
"traits": ["owner(0)", "admin(1)", "muted(2)", "dataview(3)"],
"readers": [
{ "type": "MEMBER", "reads": "*" }
],
"moves": [
{ "event": "Move", "from": "OUTSIDER", "to": "PENDING", "operator": "Self", "ops": ["C"],
"alias": "applications", "gate": { "operator": ["owner", "admin"] } },
{ "event": "Move", "from": "OUTSIDER", "to": "MEMBER", "operator": "Self", "ops": ["C"],
"alias": "auto_join", "gate": { "operator": ["owner"] } },
{ "event": "Move", "from": "OUTSIDER", "to": "MEMBER", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "OUTSIDER", "to": "BLOCKED", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "PENDING", "to": "MEMBER", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "PENDING", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "MEMBER", "to": "OUTSIDER", "operator": "Self", "ops": ["C"] },
{ "event": "Move", "from": "MEMBER", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "MEMBER", "to": "BLOCKED", "operator": "admin", "ops": ["C"] },
{ "event": "Move", "from": "BLOCKED", "to": "OUTSIDER", "operator": "admin", "ops": ["C"] }
],
"grants": [
{ "event": "Grant", "operator": ["admin"], "scope": ["MEMBER"], "trait": ["muted"] },
{ "event": "Grant", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] },
{ "event": "Grant", "operator": ["owner"], "scope": ["OUTSIDER", "MEMBER"], "trait": ["dataview"] },
{ "event": "Revoke", "operator": ["admin"], "scope": ["MEMBER"], "trait": ["muted"] },
{ "event": "Revoke", "operator": ["owner"], "scope": ["MEMBER"], "trait": ["admin"] },
{ "event": "Revoke", "operator": ["owner"], "scope": ["OUTSIDER", "MEMBER"], "trait": ["dataview"] },
{ "event": "Revoke", "operator": ["Self"], "scope": ["MEMBER"], "trait": ["admin"] }
],
"transfers": [
{ "trait": "owner", "scope": ["MEMBER"] }
],
"slots": [
{ "event": "Shared", "operator": "admin", "ops": ["C", "U"], "key": "topic" },
{ "event": "Shared", "operator": "dataview", "ops": ["P"], "key": "topic" },
{ "event": "Own", "operator": "MEMBER", "ops": ["C"], "key": "profile" },
{ "event": "Own", "operator": "Sender", "ops": ["U"], "key": "profile" }
],
"lifecycle": [
{ "event": "Pause", "operator": "owner", "ops": ["C"] },
{ "event": "Resume", "operator": "owner", "ops": ["C"] },
{ "event": "Migrate", "operator": "owner", "ops": ["C"] },
{ "event": "Terminate", "operator": "owner", "ops": ["C"] }
],
"customs": [
{ "event": "message", "operator": "MEMBER", "ops": ["C"] },
{ "event": "message", "operator": "admin", "ops": ["D"] },
{ "event": "message", "operator": "muted", "ops": ["_C", "_U"] },
{ "event": "message", "operator": "dataview", "ops": ["P"] },
{ "event": "message", "operator": "Sender", "ops": ["U", "D"] },
{ "event": "message", "operator": "BLOCKED", "ops": ["_U", "_D"] },
{ "event": "reaction", "operator": "MEMBER", "ops": ["C"] },
{ "event": "reaction", "operator": "Sender", "ops": ["D"] },
{ "event": "reaction", "operator": "muted", "ops": ["_C"] },
{ "event": "reaction", "operator": "BLOCKED", "ops": ["_D"] },
{ "event": "notice", "operator": "admin", "ops": ["C", "D"] },
{ "event": "rotate", "operator": "admin", "ops": ["C"] }
],
"init": [
{ "identity": "<owner_pub>", "state": "MEMBER", "traits": ["owner", "admin"] }
]
}Event-Operator Matrix
| Event | MEMBER | OUTSIDER | PENDING | BLOCKED | owner(0) | admin(1) | muted(2) | dataview(3) | Self | Sender |
|---|---|---|---|---|---|---|---|---|---|---|
| message | CR | _U_D | D | _C_U | P | UD | ||||
| reaction | CR | _D | _C | D | ||||||
| notice | R | CD | ||||||||
| rotate | R | C | ||||||||
| Shared(topic) | R | CU | P | |||||||
| Own(profile) | CR | U | ||||||||
| Move(OUTSIDER, PENDING) | R | C | ||||||||
| Gate(applications) | R | C | C | |||||||
| Move(OUTSIDER, MEMBER) | R | C | C | |||||||
| Gate(auto_join) | R | C | ||||||||
| Move(OUTSIDER, BLOCKED) | R | C | ||||||||
| Move(PENDING, MEMBER) | R | C | ||||||||
| Move(PENDING, OUTSIDER) | R | C | ||||||||
| Move(MEMBER, OUTSIDER) | R | C | C | |||||||
| Move(MEMBER, BLOCKED) | R | C | ||||||||
| Move(BLOCKED, OUTSIDER) | R | C | ||||||||
| Grant(muted) | R | C | ||||||||
| Grant(admin) | R | C | ||||||||
| Grant(dataview) | R | C | ||||||||
| Revoke(muted) | R | C | ||||||||
| Revoke(admin) | R | C | C | |||||||
| Revoke(dataview) | R | C | ||||||||
| Transfer(owner) | R | C | ||||||||
| Pause | R | C | ||||||||
| Resume | R | C | ||||||||
| Migrate | R | C | ||||||||
| Terminate | R | C |
Columns: States (MEMBER, OUTSIDER, PENDING, BLOCKED) → traits (owner, admin, muted, dataview) → Contexts (Self, Sender).
10. SMT Namespace Table
| Namespace | Purpose | Key | Value |
|---|---|---|---|
| 0x00 | RBAC | H(identity) | Bitmask (State + traits) |
| 0x01 | EventStatus | H(event_hash) | Status flags (Update/Delete) |
| 0x02 | KV State | H(key) or H(key || identity) | H(content) |
Gate state lives in 0x02 with the gate: key prefix. Lifecycle state lives in 0x02 with the lifecycle key.
11. Appendix: Removed Events
Four event types from the v1 spec are removed. Each is replaced by a more general mechanism already in the registry.
| Removed | Replacement | Rationale |
|---|---|---|
| Move_Self | Move with operator: "Self" | Self is a Context, not an event type. The node enforces event.from == content.target when the moves entry has operator: "Self". No expressiveness lost. |
| Revoke_Self | Revoke with operator: "Self" | Same pattern. Self-targeting is an operator constraint, not a separate event type. |
| Grant_Push | Grant | The node detects P (Push) ops from the schema for the granted trait. If the trait has P ops and the Grant content includes an endpoint field, the node registers the push endpoint. No separate event type needed. |
| Force_Move | (removed, no replacement) | Owner can define all necessary transitions in moves. State is an 8-bit enum (not a combinatorial bitmask), so Owner can always move anyone to any State by having the right moves entries. Force_Move added no capability that moves entries cannot express. |