Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 75 additions & 40 deletions internal/kernel/liveness.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"moos/kernel/internal/operad"
)

// §M11 liveness gate and §M12 admin-capability hook for kernel rewrites.
// §M11 liveness gate and §M12 admin-capability gate for kernel rewrites.
//
// Doctrine:
//
Expand All @@ -18,49 +18,64 @@ import (
// - §M12 says admin-scope rewrites additionally require the actor (after
// has-occupant resolution) to hold the superadmin role via WF02 governs.
//
// This file integrates both checks into Runtime.Apply / Runtime.ApplyProgram
// as small helpers so the main Apply body stays readable. §M12 is plumbed
// here via operad.AdminScopeRewrite + operad.CheckAdminCapability and ships
// as dormant in PR 3 (AdminScopeRewrite returns false until PR 4 fills it in).

// checkLiveness is the §M11 gate. Called from Apply / ApplyProgram BEFORE
// operad validation so we fail fast on missing session context without
// paying structural-validation cost. Read-only over state; no locking
// required because Apply/ApplyProgram are already serialised by rt.mu in
// the caller (or read-lock during validation).
// Evaluation surface:
//
// - §M11 evaluates emitter-context: the session this envelope runs under.
// That session must pre-exist. For ApplyProgram, §M11 runs in preflight
// against the batch-initial state — emitter references cannot depend on
// prior envelopes in the same batch (see the initial-state-check
// doctrine note in kb/research/kernel/20260421-t171-m11-m12-implementation-plan.md §2.4).
//
// Order of checks:
// 1. System-internal allowlist (sweep, kernel-actor, infrastructure ADD).
// These are below the governance line and always pass.
// 2. Session-context resolution via operad.ResolveSessionForEnvelope.
// Explicit env.SessionURN wins; inferred from Actor when unambiguous;
// reject when ambiguous or absent.
// 3. (PR 4 hook) admin-scope classification + capability check. Dormant
// in PR 3.
// - §M12 evaluates target-operation: what the envelope does. That operation
// can depend on nodes created earlier in the same batch. For ApplyProgram,
// §M12 runs per-envelope inside the working-state loop — a MUTATE on a
// node ADDed in envelope N-1 sees that node's type when classifying at
// envelope N. Closes the Gemini PR 31 security-HIGH where §M12 was
// evaluated against initial state and ADD-then-MUTATE could bypass the
// admin gate.

// checkLiveness runs both §M11 and §M12 against rt.state (no working state
// evolves). Used by Apply. The combined form is safe here because Apply
// handles a single envelope — the target either exists at call time or it
// doesn't, and ApplyProgram's mid-batch scenario cannot arise.
//
// Returns nil on pass, a fmt.Errorf wrapping the failure mode on reject.
// Error messages name the doctrine section so log readers can trace back.
// Caller must hold rt.mu at least for read. Returns nil on pass; a
// fmt.Errorf naming the doctrine section on reject.
func (rt *Runtime) checkLiveness(env graph.Envelope) error {
// Registry-less mode (no --ontology): liveness is a no-op. The kernel
// still replays and applies rewrites; it just does not enforce the
// occupancy invariant. Matches the existing pattern where validators
// short-circuit when registry is nil.
if err := rt.checkLivenessM11(env, rt.state); err != nil {
return err
}
return rt.checkLivenessM12(env, rt.state)
}

// checkLivenessM11 is the §M11 emitter-context gate. Single-phase: the
// session URN the envelope runs under must exist in the passed state and
// must have the actor as has-occupant (or the actor must itself be an
// occupied session node).
//
// Parameter `state` is explicit so callers can choose whether to evaluate
// against rt.state (Apply, single-envelope) or some other snapshot. For
// ApplyProgram we always pass the batch-initial state because §M11 is an
// emitter-pre-existence rule — emitter references cannot depend on prior
// envelopes in the batch.
//
// Returns nil on pass; a fmt.Errorf for each resolver failure kind.
func (rt *Runtime) checkLivenessM11(env graph.Envelope, state graph.GraphState) error {
if rt.registry == nil {
return nil
}

// Step 1 — system-internal allowlist. Sweep, kernel actors, and
// bootstrap-infrastructure ADDs are below the liveness line.
// System-internal allowlist precedes all M11/M12 enforcement. Sweep,
// kernel actors, and bootstrap-infrastructure ADDs are below the
// governance line and bypass both gates.
if operad.SystemInternalEnvelope(env) {
return nil
}

// Step 2 — resolve session context. Pass the live state so reverse
// has-occupant walks see the most recent seat assignments.
res := operad.ResolveSessionForEnvelope(rt.state, env)
res := operad.ResolveSessionForEnvelope(state, env)
switch res.Kind {
case operad.ResolveSessionExplicit, operad.ResolveSessionInferred, operad.ResolveSessionActorIsSession:
// Pass: a single, verified session context was resolved.
return nil
case operad.ResolveSessionExplicitMismatch:
return fmt.Errorf("kernel(§M11): envelope names session_urn=%s but that session is missing, not of type session, or does not have has-occupant -> actor=%s",
res.SessionURN, env.Actor)
Expand All @@ -71,17 +86,37 @@ func (rt *Runtime) checkLiveness(env graph.Envelope) error {
return fmt.Errorf("kernel(§M11): no session context for actor=%s (no has-occupant relation in state and no session_urn on envelope)",
env.Actor)
}
return nil
}

// Step 3 — admin-scope gate (§M12 hook). Dormant in PR 3 — the
// classifier currently returns false for every envelope. PR 4 tightens
// this to check capability via operad.CheckAdminCapability.
if operad.AdminScopeRewrite(env, rt.state) {
if !operad.CheckAdminCapability(rt.state, env.Actor) {
return fmt.Errorf("kernel(§M12): actor=%s lacks WF02 superadmin capability for admin-scope rewrite",
env.Actor)
}
// checkLivenessM12 is the §M12 admin-capability gate. Classifies the
// envelope via the registry's AdminScopeRewrite method against the passed
// state — if classified admin-scope, the actor must hold WF02 superadmin
// capability. The state parameter matters: for ApplyProgram, the caller
// passes the working state AFTER earlier envelopes in the batch have been
// folded so a MUTATE on a mid-batch-ADDed node sees the node's type
// correctly.
//
// The §M11 allowlist (SystemInternalEnvelope) is re-checked here to keep
// checkLivenessM12 safe to call in isolation — if a future caller uses it
// without first running checkLivenessM11, kernel-actor and infrastructure
// envelopes still bypass correctly.
//
// Returns nil on pass; a fmt.Errorf naming §M12 on reject.
func (rt *Runtime) checkLivenessM12(env graph.Envelope, state graph.GraphState) error {
if rt.registry == nil {
return nil
}
if operad.SystemInternalEnvelope(env) {
return nil
}
if !rt.registry.AdminScopeRewrite(env, state) {
return nil
}
if !operad.CheckAdminCapability(state, env.Actor) {
return fmt.Errorf("kernel(§M12): actor=%s lacks WF02 superadmin capability for admin-scope rewrite",
env.Actor)
}

return nil
}

Expand Down
Loading