Local Group Governance
Signed gossip operations, causal DAG, capability-based authorization
Architecture
Core Concept: Groups Own Contexts
Member Roles
Admin
Full control. Can perform all governance operations, manage members, configure the group, and write state in all contexts.
Member
Standard participant. Can read and write state in joined contexts. Specific governance operations require capability bits (e.g. CAN_INVITE_MEMBERS, MANAGE_MEMBERS).
ReadOnly
Observer. Can join contexts and read state, but state mutations are rejected by the platform. Enforced at both the local node (execute handler discards changes) and remote nodes (state deltas from ReadOnly authors are rejected on ingestion).
Subgroups
Groups can form a tree hierarchy. A child group links to a parent via GroupParentRef. Membership and admin authority inherit downward:
Membership Inheritance
Membership checks walk up the ancestor chain (max depth 16). A member of the parent group is automatically a member of all descendant groups. Direct membership in the child takes priority over inherited membership.
Admin Authority
Parent admins inherit structural governance over descendant groups: add/remove members, detach contexts, delete subgroups, set target application.
Restricting Access via Subgroups
To restrict access to specific contexts, create a subgroup and register those contexts under it. Only members of that subgroup (direct or inherited from its parent) can join its contexts. This replaces the former per-context visibility/allowlist model with a simpler group-membership-based approach.
Create a subgroup with --parent-group-id on meroctl group create or via the parentGroupId field in the API. The SubgroupCreated governance op is published on the parent group's gossip topic.
When joining a parent group with auto_join (default: true), the node subscribes to gossip topics and contexts in all descendant subgroups. MemberRemoved cascades to descendant subgroup contexts (skipping child groups where the member has direct membership).
Governance DAG Structure
Each group has a DAG of SignedGroupOp operations. Operations reference their parents by content hash, forming a causal chain that supports offline ops, concurrent admins, and merge.
SignedGroupOp Schema (v3)
Signable Payload
GroupOp Variants
Operation Flows
Create Context Flow
Admin calls create_context
API receives group_id (or auto-creates group). Uses the node's namespace identity keypair (from the datastore) if no explicit secret provided.
Handler signs GroupOps
Emits a sequence: ContextRegistered → optional ContextAliasSet. Each is a SignedGroupOp applied locally and published via gossip.
Local apply + OpLog
group_store::apply_local_signed_group_op verifies signature, checks state_hash, updates KV rows, appends to op log, recomputes DAG heads.
Gossip propagation
Published on group/<hex> topic. Remote nodes receive via SignedGroupOpV1 broadcast, verify, and apply the same ops.
Join Group via Invitation
Admin creates invitation
create_group_invitation generates a SignedGroupOpenInvitation with optional expiration timestamp. Shared out-of-band to the joiner.
Joiner submits invitation
join_group builds GroupRevealPayloadData, signs it, submits a JoinWithInvitationClaim GroupOp.
Op applied + member added
Group store validates the claim, adds the new member, updates op log. Gossip ensures all peers converge.
Join Context (within Group)
Group membership implies context access
Context membership is implicit from group membership — no separate governance op or explicit join is required. When a member joins a group with auto_join: true (default), they automatically subscribe to all contexts in that group and its descendant subgroups.
Context identity resolved
The node uses its namespace identity as the context member identity. No per-context keypair generation needed.
Member Removal Cascade
Sync & Catch-up Protocol
Startup Recovery
reload_group_dags + recover_in_progress_upgrades
On node start, for each group in the store, reads the full OpLog and rebuilds the in-memory DagStore via restore_applied_delta. Reconstructs pending parent resolution state. Re-spawns propagators for any in-progress group upgrades (crash recovery).
start_namespace_heartbeat
Starts a 30-second periodic timer. Each tick collects unique namespaces from all groups and publishes NamespaceStateHeartbeat with current namespace DAG dag_heads. Also publishes GroupStateHeartbeat per group for backward compat.
Peers respond with deltas
Peers compare heads against their own. For any ops the restarted node is missing, they serve them via NamespaceBackfillRequest/Response streams (or legacy GroupDeltaRequest/Response).
Namespace Governance
Since the namespace identity model, governance has moved to a single DAG per namespace (root group). All groups within a namespace share one governance DAG, stored in NamespaceGovOp / NamespaceGovHead storage keys. Operations are either cleartext RootOps (visible to all namespace members) or encrypted GroupOps (only readable by group members).
RootOps (Cleartext)
Visible to all namespace members. Used for structural changes and key distribution:
parent_id (strict-tree invariant — no orphan creation path)cascade_group_ids + cascade_context_ids which every peer re-enumerates and verifies for deterministic application.GroupNested + GroupUnnested pair; orphan state is structurally impossible.GroupOps (Encrypted)
Encrypted with the group’s key. Non-members store an opaque skeleton (causal structure preserved, content unknown):
Inner GroupOp variants (after decryption) are the same as the per-group ops: MemberAdded, MemberRemoved, ContextRegistered, etc.
Key Delivery Mechanism
1. Joiner publishes RootOp::MemberJoined (signed by joiner’s namespace identity)
2. Existing member sees MemberJoined on the namespace DAG
3. Member wraps group key via ECDH: shared = ECDH(sender_sk, joiner_pk)
4. Member publishes RootOp::KeyDelivery { envelope }
5. Joiner unwraps using ECDH(joiner_sk, sender_pk)
6. Joiner can now decrypt GroupOp payloads and participate fully
Signing Key Hierarchy (Ancestor Walk)
Signing keys are stored at the namespace root when a namespace is created. Child groups (created via create_group_in_namespace) do not get their own copy. Instead, governance_preflight resolves signing authority by walking the parent chain:
1. Check current group for a signing key matching the requester identity
2. If not found, walk to parent via get_parent_group()
3. Repeat up to MAX_NAMESPACE_DEPTH (16) levels + final check at root
4. Return the first key found, or error if none exists
Revocation: Revoking a signing key at the namespace root automatically prevents all descendant groups from signing — no stale copies to clean up. Unnesting: breaks the parent link, so the child can no longer walk to the ancestor’s key.
Recursive Member Removal
When a member is removed from a group, recursive_remove_member() cascades the removal to all descendant groups and their contexts. Removal from a child group does not affect the parent (upward isolation).
Namespace Governance Epoch
Context state deltas carry a governance_epoch field. This is now computed from the namespace DAG heads (not per-group heads), ensuring a consistent governance state reference across all groups in a namespace. Computed via compute_namespace_governance_epoch(store, context_id).
Known Limitations
Multi-admin convergence
Concurrent ops from multiple admins with the same state_hash: first applied wins per node. Different nodes may pick different winners for conflicting ops. Merge Noop resolves heads but op ordering may differ. Single-admin groups have no ambiguity.
Governance epoch
State deltas carry a governance_epoch field but it's not yet used to reject stale application-level deltas from removed members.
Context-level capability enforcement
Context-level capabilities (ManageApplication, ManageMembers) are stored and can be granted/revoked via governance ops, but are not yet checked during WASM execution. ReadOnly role enforcement is platform-level (execute handler + delta ingestion), not capability-based.
Gossip publish failures
If gossip publish fails, it's currently silent. Should surface errors to callers or retry.