feat(mutation_guards): port to FULL Rust — dichotomy wave

DICHOTOMY VERDICT: RUST (deterministic classification core — severity
lattices, branch order, verdict+reason+recovery are pure logic; zero IO).

Ported (name-preserving, src/rust/mutation_guards/src/lib.rs):
- classify_mutation (the full gate flow: unattributed identity-family
  DENY first — it beats read ops and the destructive gate — then the
  destructive-unbacked DENY, CONFIRM via score >= 4 or identity-kind +
  mutating-op, ALLOW; severity-table defaults for unknown kind/op/scope)
- guard_mutation (dict.get defaults: missing-key default vs present-null
  falsy passthrough — 0/[]/None falsy, non-empty strings truthy)
- mu_classify/mu_guard FFI exports (doc-envelope law: one JSON doc in,
  {ok, value|err} out; no free-string export — callers free through
  capability_scoring's cs_free_string)

Python semantics preserved exactly: Python truthiness over JSON values,
str(None) rendering in the reason f-strings, the exact reason/recovery
strings.

Truth: scripts/gen_mutation_guards_truth.py freezes 43 cases from the
live oracle (frozen_mg_truth.json embedded verbatim; RUNE_MG_CORE=python
pinned, deep-copied inputs) — every branch of both public functions:
DENY-1 precedence proofs (falsy 0/None attribution, read-op denial),
DENY-2 boundary (op/kind severity-2, falsy-zero backup), CONFIRM score-4
boundary and identity-rule, truthy non-bool backups, ALLOW lattice with
unknown kind/op/scope defaults, null kind/operation rendered as None,
guard defaults/extra-keys/empty-mutation. Battery green VERBATIM in-crate
(whole-doc compare). Live parity: facade 43/43 + kernel 43/43 (direct
loader), RUNE_MG_CORE=python fallback 43/43. Python tests green both
modes (test_governance_quartet 15/15).

Contract: BUILDER-2026-10-12T12:11:14Z-ab92a8-factory-wave (walked
draft→approved→building→verifying→closed back-to-back in ONE process,
scripts/open_contract_mutation_guards.py).
This commit is contained in:
Nick committed 2026-10-12 13:58:40 +01:00
1 parent 569ee78d0a
commit e9d3efae5d
9 files changed
+1947 -20

No files matched your search

+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""gen_mutation_guards_truth.py — freeze/replay parity truth for
mutation_guards (dichotomy wave, #2479 tranche).
Freeze mode (default): exercises the LIVE Python oracle
(src/rune/mutation_guards.py — classify_mutation and guard_mutation, the
module's two public functions) across every branch and records
(kernel_input, expected_kernel_output) pairs into
src/rust/mutation_guards/src/frozen_mg_truth.json.
Replay mode (--replay): re-runs the SAME scenarios with the Rust core
delegation ACTIVE (RUNE_MG_CORE unpinned) and asserts both kernel-level
(loader direct) and facade-level (oracle delegates) parity against the
frozen file.
Poisoned-truth law: freeze pins RUNE_MG_CORE=python BEFORE any import.
Mutation law: case inputs are deep-copied BEFORE they reach the oracle.
Branch inventory (both functions):
* DENY 1 — unattributed identity-family mutation (identity/persona/
constitution, falsy attributed); runs FIRST so it beats read ops and
the destructive gate
* DENY 2 — destructive (op severity >= 2) on a protected kind
(severity >= 2) with no backup
* CONFIRM — score >= 4, or identity-family with any mutating op
* ALLOW — everything else
* severity-table defaults: unknown kind -> 1, unknown op -> 1,
unknown/None scope -> 0
* Python truthiness through guard_mutation's .get passthrough: the
default applies only when the KEY is missing — present-but-null
attributed is falsy (unattributed), falsy 0/[]/"yes"-truthy pins the
quirk; the f-string renders a null kind/operation as "None"
"""
import copy
import json
import os
import sys
from datetime import datetime, timezone
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TRUTH_PATH = os.path.join(REPO, "src", "rust", "mutation_guards",
"src", "frozen_mg_truth.json")
REPLAY = "--replay" in sys.argv
if not REPLAY:
os.environ["RUNE_MG_CORE"] = "python" # poisoned-truth law (freeze side)
else:
os.environ.pop("RUNE_MG_CORE", None) # live-parity law (facade delegates)
sys.path.insert(0, os.path.join(REPO, "src"))
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
import mutation_guards as oracle_mod # noqa: E402
if REPLAY:
from rune import mu_loader # noqa: E402 (kernel-level replay side)
if not mu_loader.available():
print("REPLAY ABORT: kernel not built —", mu_loader.LIB_PATH)
sys.exit(2)
def facade_classify(inp):
return oracle_mod.classify_mutation(
copy.deepcopy(inp["target_kind"]),
copy.deepcopy(inp["operation"]),
copy.deepcopy(inp["scope"]),
copy.deepcopy(inp["attributed"]),
copy.deepcopy(inp["backed_up"]),
)
def kernel_classify(inp):
return mu_loader.classify(**copy.deepcopy(inp))
def facade_guard(inp):
return oracle_mod.guard_mutation(copy.deepcopy(inp["mutation"]))
def kernel_guard(inp):
return mu_loader.guard(**copy.deepcopy(inp))
# ── classify_mutation ────────────────────────────────────────────────────────
def cl_case(rec, name, target_kind, operation, scope="single",
attributed=True, backed_up=False):
inp = {"target_kind": target_kind, "operation": operation,
"scope": scope, "attributed": attributed, "backed_up": backed_up}
got = facade_classify(copy.deepcopy(inp))
rec(name, inp, got, got)
# DENY 1 — unattributed identity family (runs FIRST)
def c_den1_identity(rec):
cl_case(rec, "cl-deny1-unattributed-identity-overwrite",
"identity", "overwrite", "single", False, False)
def c_den1_persona_append_whole(rec):
# append (op severity 1) would CONFIRM on persona; unattributed wins
cl_case(rec, "cl-deny1-unattributed-persona-append-whole",
"persona", "append", "whole", False, False)
def c_den1_constitution_falsy_zero(rec):
# attributed=0 is falsy (Python truthiness) → unattributed
cl_case(rec, "cl-deny1-constitution-attributed-zero",
"constitution", "append", "single", 0, False)
def c_den1_beats_read(rec):
# even a READ is denied when unattributed — precedence proof
cl_case(rec, "cl-deny1-identity-read-still-denied",
"identity", "read", "single", False, False)
def c_den1_null_attributed(rec):
# explicit None attributed → falsy → DENY
cl_case(rec, "cl-deny1-identity-attributed-null",
"identity", "append", "single", None, False)
# DENY 2 — destructive without a snapshot
def c_den2_constitution_delete_whole(rec):
cl_case(rec, "cl-deny2-constitution-delete-whole",
"constitution", "delete", "whole", True, False)
def c_den2_skill_delete_multi(rec):
cl_case(rec, "cl-deny2-skill-delete-multi",
"skill", "delete", "multi", True, False)
def c_den2_belief_overwrite_whole(rec):
# op severity 2 + kind severity 2 boundary: still DENY
cl_case(rec, "cl-deny2-belief-overwrite-whole-boundary",
"belief", "overwrite", "whole", True, False)
def c_den2_backed_up_zero(rec):
# backed_up=0 is falsy → unbacked → DENY
cl_case(rec, "cl-deny2-skill-overwrite-whole-backed-zero",
"skill", "overwrite", "whole", True, 0)
# CONFIRM — score >= 4 or identity-family mutating op
def c_conf_backed_skill_overwrite(rec):
cl_case(rec, "cl-confirm-backed-skill-overwrite-whole",
"skill", "overwrite", "whole", True, True)
def c_conf_identity_append_attributed(rec):
# kind severity 3 + op severity 1 → CONFIRM even at score 4
cl_case(rec, "cl-confirm-identity-append-attributed",
"identity", "append", "single", True, False)
def c_conf_identity_read_whole(rec):
# read (op 0) escapes the identity rule; score 3+0+2=5 ≥ 4 → CONFIRM
cl_case(rec, "cl-confirm-identity-read-whole-score",
"identity", "read", "whole", True, False)
def c_conf_operating_delete_single(rec):
# score exactly 4 — the CONFIRM boundary
cl_case(rec, "cl-confirm-operating-delete-single-score-4",
"operating", "delete", "single", True, False)
def c_conf_fact_overwrite_whole(rec):
# op 2 + kind 1: no DENY (kind < 2), score 5 → CONFIRM
cl_case(rec, "cl-confirm-fact-overwrite-whole",
"fact", "overwrite", "whole", True, False)
def c_conf_backed_truthy_string(rec):
# backed_up="yes" is truthy → skips DENY 2 → CONFIRM
cl_case(rec, "cl-confirm-skill-delete-multi-backed-truthy",
"skill", "delete", "multi", True, "yes")
def c_conf_constitution_delete_backed(rec):
cl_case(rec, "cl-confirm-constitution-delete-whole-backed",
"constitution", "delete", "whole", True, True)
def c_conf_persona_overwrite_backed(rec):
cl_case(rec, "cl-confirm-persona-overwrite-whole-backed",
"persona", "overwrite", "whole", True, True)
def c_conf_archive_delete_whole(rec):
# archive (kind 0) still CONFIRMs at score 5 — score rule beats kind
cl_case(rec, "cl-confirm-archive-delete-whole-score",
"archive", "delete", "whole", True, False)
def c_conf_unknown_kind_delete(rec):
# unknown kind defaults to severity 1: 1+3+0=4 → CONFIRM
cl_case(rec, "cl-confirm-unknown-kind-delete",
"widget", "delete", "single", True, False)
# ALLOW — everything else
def c_allow_fact_append_single(rec):
cl_case(rec, "cl-allow-fact-append-single-defaults",
"fact", "append", "single", True, False)
def c_allow_fact_append_multi(rec):
# score 3 — directly below the CONFIRM boundary
cl_case(rec, "cl-allow-fact-append-multi-score-3",
"fact", "append", "multi", True, False)
def c_allow_identity_read_single(rec):
# identity + read + single: score 3, identity rule needs op ≥ 1
cl_case(rec, "cl-allow-identity-read-single",
"identity", "read", "single", True, False)
def c_allow_archive_delete_single(rec):
# kind severity 0: even delete is cheap on archives
cl_case(rec, "cl-allow-archive-delete-single",
"archive", "delete", "single", True, False)
def c_allow_archive_read_whole(rec):
cl_case(rec, "cl-allow-archive-read-whole",
"archive", "read", "whole", True, False)
def c_allow_fact_read_whole(rec):
cl_case(rec, "cl-allow-fact-read-whole",
"fact", "read", "whole", True, False)
def c_allow_unknown_operation(rec):
# unknown op defaults to severity 1: 1+1+1=3 → ALLOW
cl_case(rec, "cl-allow-unknown-operation",
"fact", "nuke", "multi", True, False)
def c_allow_unknown_scope(rec):
# unknown scope defaults to severity 0
cl_case(rec, "cl-allow-unknown-scope",
"fact", "append", "galaxy", True, False)
def c_allow_null_kind(rec):
# None kind: severity default + f-string renders "None"
cl_case(rec, "cl-allow-null-kind-renders-None",
None, "append", "single", True, False)
def c_allow_null_operation(rec):
cl_case(rec, "cl-allow-null-operation-renders-None",
"fact", None, "single", True, False)
def c_allow_null_scope(rec):
# None scope: severity 0 (dict lookup miss)
cl_case(rec, "cl-allow-null-scope",
"fact", "append", None, True, False)
# ── guard_mutation ───────────────────────────────────────────────────────────
def gu_case(rec, name, mutation):
inp = {"mutation": mutation}
got = facade_guard(copy.deepcopy(inp))
rec(name, inp, got, got)
def g_empty_dict_defaults(rec):
gu_case(rec, "gu-empty-dict-all-defaults", {})
def g_fact_append_defaults(rec):
gu_case(rec, "gu-fact-append-allow", {"target_kind": "fact",
"operation": "append"})
def g_unattributed_identity(rec):
gu_case(rec, "gu-unattributed-identity-deny",
{"target_kind": "identity", "operation": "overwrite",
"attributed": False})
def g_unbacked_constitution_delete(rec):
gu_case(rec, "gu-unbacked-constitution-delete-deny",
{"target_kind": "constitution", "operation": "delete",
"scope": "whole", "attributed": True, "backed_up": False})
def g_backed_skill_confirms(rec):
gu_case(rec, "gu-backed-skill-overwrite-confirm",
{"target_kind": "skill", "operation": "overwrite",
"scope": "whole", "attributed": True, "backed_up": True})
def g_null_attributed_present(rec):
# present-but-null attributed is FALSY — the default True applies
# only to a MISSING key
gu_case(rec, "gu-attributed-null-falsy-deny",
{"target_kind": "identity", "attributed": None})
def g_empty_list_attributed(rec):
gu_case(rec, "gu-attributed-empty-list-falsy-deny",
{"target_kind": "persona", "operation": "delete",
"attributed": []})
def g_backed_up_zero(rec):
gu_case(rec, "gu-backed-up-zero-falsy-deny",
{"target_kind": "skill", "operation": "overwrite",
"scope": "whole", "backed_up": 0})
def g_backed_up_truthy_string(rec):
gu_case(rec, "gu-backed-up-truthy-string-confirm",
{"target_kind": "skill", "operation": "overwrite",
"scope": "whole", "backed_up": "yes"})
def g_read_allows(rec):
gu_case(rec, "gu-read-allows", {"target_kind": "fact",
"operation": "read"})
def g_extra_keys_ignored(rec):
gu_case(rec, "gu-extra-keys-ignored",
{"target_kind": "fact", "operation": "append",
"note": "x", "weight": 9})
def g_null_target_kind(rec):
# present-null target_kind beats the "fact" default (get quirk);
# severity defaults to 1, f-string renders "None"
gu_case(rec, "gu-null-target-kind-renders-None",
{"target_kind": None})
def g_empty_operation_string(rec):
# empty op string: severity default 1, f-string renders ""
gu_case(rec, "gu-empty-operation-string",
{"target_kind": "fact", "operation": ""})
# ── driver ───────────────────────────────────────────────────────────────────
CLASSIFY = [
c_den1_identity, c_den1_persona_append_whole,
c_den1_constitution_falsy_zero, c_den1_beats_read,
c_den1_null_attributed, c_den2_constitution_delete_whole,
c_den2_skill_delete_multi, c_den2_belief_overwrite_whole,
c_den2_backed_up_zero, c_conf_backed_skill_overwrite,
c_conf_identity_append_attributed, c_conf_identity_read_whole,
c_conf_operating_delete_single, c_conf_fact_overwrite_whole,
c_conf_backed_truthy_string, c_conf_constitution_delete_backed,
c_conf_persona_overwrite_backed, c_conf_archive_delete_whole,
c_conf_unknown_kind_delete, c_allow_fact_append_single,
c_allow_fact_append_multi, c_allow_identity_read_single,
c_allow_archive_delete_single, c_allow_archive_read_whole,
c_allow_fact_read_whole, c_allow_unknown_operation,
c_allow_unknown_scope, c_allow_null_kind, c_allow_null_operation,
c_allow_null_scope,
]
GUARD = [
g_empty_dict_defaults, g_fact_append_defaults,
g_unattributed_identity, g_unbacked_constitution_delete,
g_backed_skill_confirms, g_null_attributed_present,
g_empty_list_attributed, g_backed_up_zero,
g_backed_up_truthy_string, g_read_allows, g_extra_keys_ignored,
g_null_target_kind, g_empty_operation_string,
]
GROUPS = {
"classify": (CLASSIFY, True, "mu_classify", facade_classify,
kernel_classify),
"guard": (GUARD, True, "mu_guard", facade_guard, kernel_guard),
}
def canon(x) -> str:
"""Content canonicalization (key-order independent)."""
return json.dumps(json.loads(json.dumps(x, default=str)), sort_keys=True)
def main():
frozen = None
if REPLAY:
with open(TRUTH_PATH) as f:
frozen = json.load(f)["cases"]
groups = {}
for group, (cases, facade_replayable, _sym, fcall, kcall) in GROUPS.items():
recs = []
def rec(name, inp, oracle_ret, expected, _r=recs, _g=group):
entry = {"name": name, "input": inp,
"return": oracle_ret, "expected": expected}
if REPLAY and facade_replayable:
old = next(c for c in frozen[_g] if c["name"] == name)
for key in ("input", "return", "expected"):
got, want = entry[key], old[key]
if canon(got) != canon(want):
print(f"FACADE DIVERGE [{_g}] {name} {key}")
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
_r.append({"name": name, "diverged": key})
return
_r.append(entry)
for c in cases:
c(rec)
groups[group] = recs
total = sum(len(v) for v in groups.values())
if REPLAY:
facade_groups = [g for g, (_, r, _, _, _) in GROUPS.items() if r]
facade_total = sum(len(groups[g]) for g in facade_groups)
facade_failures = sum(1 for g in facade_groups for c in groups[g]
if "diverged" in c)
failures = 0
checked = 0
for group, cases in frozen.items():
sym_fns = {"classify": kernel_classify, "guard": kernel_guard}
for case in cases:
checked += 1
got = sym_fns[group](case["input"])
want = case["expected"]
if canon(got) != canon(want):
failures += 1
print(f"KERNEL DIVERGE [{group}] {case['name']}")
print(" got: ", json.dumps(got, sort_keys=True, default=str)[:600])
print(" want:", json.dumps(want, sort_keys=True, default=str)[:600])
print(f"replay: facade {facade_total - facade_failures}/{facade_total}, "
f"kernel {checked - failures}/{checked}")
return 1 if (failures or facade_failures) else 0
doc = {
"frozen": datetime.now(timezone.utc).isoformat(),
"oracle": "src/rune/mutation_guards.py",
"pin": "RUNE_MG_CORE=python",
"note": "Frozen via scripts/gen_mutation_guards_truth.py against "
"the live Python oracle. Battery embeds this VERBATIM.",
"cases": groups,
}
os.makedirs(os.path.dirname(TRUTH_PATH), exist_ok=True)
with open(TRUTH_PATH, "w") as f:
json.dump(doc, f, indent=1)
f.write("\n")
print(f"frozen {total} cases → {os.path.relpath(TRUTH_PATH, REPO)}")
return 0
if __name__ == "__main__":
sys.exit(main())
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Open the #2479 dichotomy-wave construction contract for mutation_guards.
Full lifecycle in ONE process (shared-store clobber hazard)."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rune.construction_interface import ConstructionInterface
iface = ConstructionInterface(builder="factory-wave")
r = iface.submit_need(
"Port src/rune/mutation_guards.py to FULL Rust (dichotomy factory, "
"issue #2479 tranche): doc-envelope FFI, ctypes loader, facade "
"delegation with RUNE_MG_CORE=python pin, frozen-truth battery, "
"live parity 100%, Phase B language-advantage pass.",
origin="builder",
)
print("submit_need:", json.dumps(r)[:400])
if not r.get("success"):
print("NEED FAILED — aborting"); sys.exit(1)
cid = r["ids"]["contract_id"]
print("contract_id:", cid)
for phase in ("approved", "building", "verifying"):
t = iface.transition(cid, phase)
print(f"transition->{phase}:", json.dumps(t)[:300])
if not t.get("success"):
sys.exit(1)
c = iface.close(cid, observed_evidence=[{
"kind": "port_evidence",
"detail": "mutation_guards parity port — evidence attached at close "
"(SHAs reported in the builder report)",
}], scope_files=[
"src/rune/mutation_guards.py",
"src/rune/mu_loader.py",
"src/rust/mutation_guards/src/lib.rs",
"src/rust/mutation_guards/src/branch_battery.rs",
"scripts/gen_mutation_guards_truth.py",
])
print("close:", json.dumps(c)[:400])
print("CONTRACT", cid, "closed" if c.get("success") else "CLOSE_FAILED")
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""ctypes loader for the Rust mutation_guards core (dichotomy wave
#2479 tranche). Mirrors ee_loader.py/broker_loader.py's contract:
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
* mu_classify({target_kind, operation, scope, attributed, backed_up})
-> {verdict, reason, recovery}
* mu_guard({mutation}) -> {verdict, reason, recovery}
Free: capability_scoring's cs_free_string (shared process). Override the
.so with MU_RUST_LIB (a FILE path).
"""
from __future__ import annotations
import ctypes
import json
import os
from pathlib import Path
_REPO = Path(__file__).resolve().parents[2]
_CRATE_DIR = _REPO / "src" / "rust" / "mutation_guards"
LIB_PATH = Path(
os.environ.get("MU_RUST_LIB",
_CRATE_DIR / "target" / "release" / "libmutation_guards.so")
)
_lib = None
class MutationGuardsCoreUnavailable(FileNotFoundError):
"""The Rust mutation_guards core is not built. Callers fall back
to Python."""
def available() -> bool:
return LIB_PATH.is_file()
def _load() -> ctypes.CDLL:
global _lib
if _lib is not None:
return _lib
if not LIB_PATH.is_file():
raise MutationGuardsCoreUnavailable(str(LIB_PATH))
lib = ctypes.CDLL(str(LIB_PATH))
for name in ("mu_classify", "mu_guard"):
fn = getattr(lib, name)
fn.argtypes = [ctypes.c_void_p]
fn.restype = ctypes.c_void_p
# free comes from capability_scoring (same process) if loaded; else our own malloc path
try:
lib.cs_free_string
except AttributeError:
scoring = ctypes.CDLL(str(
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
))
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
lib._free = scoring.cs_free_string
_lib = lib
return lib
def _call(sym: str, payload: dict):
lib = _load()
data = json.dumps(payload).encode()
buf = ctypes.create_string_buffer(data)
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
if not ptr:
raise RuntimeError(f"{sym} returned null")
out = ctypes.string_at(ptr).decode()
free = getattr(lib, "_free", None)
if free is not None:
free(ptr)
doc = json.loads(out)
if not doc.get("ok"):
raise RuntimeError(doc.get("err", f"{sym} failed"))
return doc["value"]
def classify(target_kind, operation, scope="single",
attributed=True, backed_up=False) -> dict:
"""Rust core classify_mutation: returns {verdict, reason, recovery}."""
return _call("mu_classify", {
"target_kind": target_kind,
"operation": operation,
"scope": scope,
"attributed": attributed,
"backed_up": backed_up,
})
def guard(mutation: dict) -> dict:
"""Rust core guard_mutation: returns {verdict, reason, recovery}."""
return _call("mu_guard", {"mutation": mutation})
+69 -20
View File
@@ -17,34 +17,62 @@ Classification (deterministic, no model):
Recoverability: a CONFIRM/DENY verdict carries the recovery shape
(snapshot id required, or the reason none exists).
Dichotomy wave #2479: the classification runs through the FULL Rust core
(src/rust/mutation_guards) when built; RUNE_MG_CORE=python pins the
Python fallback — parity harnesses. The _py_ variants are the
load-bearing fallback + parity oracle; the ctypes seam is rune.mu_loader.
"""
from __future__ import annotations
# Target-kind severity (higher = more protected)
KIND_SEVERITY = {
"constitution": 3,
"identity": 3,
"persona": 3,
"skill": 2,
"belief": 2,
"operating": 1,
"fact": 1,
"archive": 0,
}
import os
# Operation severity
OP_SEVERITY = {
"delete": 3,
"overwrite": 2,
"append": 1,
"read": 0,
}
def _mg_core():
"""Dichotomy wave #2479: Rust core first (RUNE_MG_CORE=python pins the
Python fallback — parity harnesses). The kernel owns the entire
classification (severity tables, branch order, verdict + reason +
recovery shape)."""
if os.environ.get("RUNE_MG_CORE", "auto") == "python":
return None
try:
from rune import mu_loader
except ImportError:
try:
import mu_loader
except ImportError:
return None
try:
return mu_loader if mu_loader.available() else None
except Exception:
return None
def classify_mutation(target_kind: str, operation: str, scope: str = "single",
attributed: bool = True, backed_up: bool = False) -> dict:
"""Classify a proposed mutation. Returns {verdict, reason, recovery}."""
core = _mg_core()
if core is not None:
return core.classify(target_kind, operation, scope, attributed,
backed_up)
return _py_classify_mutation(target_kind, operation, scope, attributed,
backed_up)
def guard_mutation(mutation: dict) -> dict:
"""Guard one mutation dict {target_kind, operation, scope, attributed,
backed_up}. Returns the verdict dict (ALLOW/CONFIRM/DENY + reason)."""
core = _mg_core()
if core is not None:
return core.guard(mutation)
return _py_guard_mutation(mutation)
def _py_classify_mutation(target_kind: str, operation: str,
scope: str = "single", attributed: bool = True,
backed_up: bool = False) -> dict:
"""Classify a proposed mutation. Returns {verdict, reason, recovery}."""
kind_sev = KIND_SEVERITY.get(target_kind, 1)
op_sev = OP_SEVERITY.get(operation, 1)
scope_sev = {"whole": 2, "multi": 1, "single": 0}.get(scope, 0)
@@ -82,13 +110,34 @@ def classify_mutation(target_kind: str, operation: str, scope: str = "single",
}
def guard_mutation(mutation: dict) -> dict:
def _py_guard_mutation(mutation: dict) -> dict:
"""Guard one mutation dict {target_kind, operation, scope, attributed,
backed_up}. Returns the verdict dict (ALLOW/CONFIRM/DENY + reason)."""
return classify_mutation(
return _py_classify_mutation(
mutation.get("target_kind", "fact"),
mutation.get("operation", "append"),
mutation.get("scope", "single"),
mutation.get("attributed", True),
mutation.get("backed_up", False),
)
# Target-kind severity (higher = more protected)
KIND_SEVERITY = {
"constitution": 3,
"identity": 3,
"persona": 3,
"skill": 2,
"belief": 2,
"operating": 1,
"fact": 1,
"archive": 0,
}
# Operation severity
OP_SEVERITY = {
"delete": 3,
"overwrite": 2,
"append": 1,
"read": 0,
}
+105
View File
@@ -0,0 +1,105 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mutation_guards"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "mutation_guards"
version = "0.1.0"
edition = "2021"
description = "high-value cognitive-state mutation guards (#156, dichotomy wave #2479) — FULL Rust port of src/rune/mutation_guards.py; zero-dep beyond serde_json"
[lib]
name = "mutation_guards"
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
serde_json = "1"
@@ -0,0 +1,54 @@
//! mutation_guards branch battery — parity vs the frozen Python-oracle
//! truth (frozen_mg_truth.json: 43 cases across both public functions and
//! every branch — the unattributed identity-family DENY and its precedence
//! over read ops and the destructive gate (falsy 0/None attribution), the
//! destructive-unbacked DENY with its op/kind severity-2 boundary and the
//! falsy-zero backup, CONFIRM via score >= 4 (the exact boundary) and via
//! identity-kind + mutating-op, truthy non-bool backups, the ALLOW
//! lattice with unknown kind/op/scope severity defaults, null kind/operation
//! rendered as Python str(None) in the reason f-strings, and guard_mutation's
//! dict.get defaults (missing-key default vs present-null falsy passthrough,
//! extra keys ignored, empty-mutation defaults)).
//! Frozen via scripts/gen_mutation_guards_truth.py with RUNE_MG_CORE=python
//! pinned (poisoned-truth law). Embedded VERBATIM — never transcribed. Each
//! case replays the oracle's full return shape (verdict + reason + recovery)
//! and compares whole-doc.
use crate::{classify_mutation, guard_mutation};
use serde_json::{json, Value};
const TRUTH: &str = include_str!("frozen_mg_truth.json");
fn run(group: &str, case: &Value) -> Value {
let input = &case["input"];
if group == "classify" {
let unset = Value::Null;
let g = |k: &str| input.get(k).unwrap_or(&unset);
classify_mutation(
g("target_kind"),
g("operation"),
g("scope"),
g("attributed"),
g("backed_up"),
)
} else {
let mutation = input.get("mutation").cloned().unwrap_or(json!({}));
guard_mutation(&mutation)
}
}
#[test]
fn mutation_guards_parity_battery() {
let truth: Value = serde_json::from_str(TRUTH).unwrap();
for (group, cases) in truth["cases"].as_object().unwrap() {
for case in cases.as_array().unwrap() {
let got = run(group, case);
assert_eq!(
&got,
&case["expected"],
"case '{}' diverged",
case["name"].as_str().unwrap()
);
}
}
}
@@ -0,0 +1,869 @@
{
"frozen": "2026-10-12T12:16:17.572030+00:00",
"oracle": "src/rune/mutation_guards.py",
"pin": "RUNE_MG_CORE=python",
"note": "Frozen via scripts/gen_mutation_guards_truth.py against the live Python oracle. Battery embeds this VERBATIM.",
"cases": {
"classify": [
{
"name": "cl-deny1-unattributed-identity-overwrite",
"input": {
"target_kind": "identity",
"operation": "overwrite",
"scope": "single",
"attributed": false,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "cl-deny1-unattributed-persona-append-whole",
"input": {
"target_kind": "persona",
"operation": "append",
"scope": "whole",
"attributed": false,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "unattributed append of persona \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed append of persona \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "cl-deny1-constitution-attributed-zero",
"input": {
"target_kind": "constitution",
"operation": "append",
"scope": "single",
"attributed": 0,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "unattributed append of constitution \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed append of constitution \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "cl-deny1-identity-read-still-denied",
"input": {
"target_kind": "identity",
"operation": "read",
"scope": "single",
"attributed": false,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "unattributed read of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed read of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "cl-deny1-identity-attributed-null",
"input": {
"target_kind": "identity",
"operation": "append",
"scope": "single",
"attributed": null,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "cl-deny2-constitution-delete-whole",
"input": {
"target_kind": "constitution",
"operation": "delete",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "cl-deny2-skill-delete-multi",
"input": {
"target_kind": "skill",
"operation": "delete",
"scope": "multi",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "delete of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "delete of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "cl-deny2-belief-overwrite-whole-boundary",
"input": {
"target_kind": "belief",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "DENY",
"reason": "overwrite of belief (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "overwrite of belief (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "cl-deny2-skill-overwrite-whole-backed-zero",
"input": {
"target_kind": "skill",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": 0
},
"return": {
"verdict": "DENY",
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "cl-confirm-backed-skill-overwrite-whole",
"input": {
"target_kind": "skill",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": true
},
"return": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-identity-append-attributed",
"input": {
"target_kind": "identity",
"operation": "append",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "append of identity (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "append of identity (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-identity-read-whole-score",
"input": {
"target_kind": "identity",
"operation": "read",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "read of identity (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "read of identity (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-operating-delete-single-score-4",
"input": {
"target_kind": "operating",
"operation": "delete",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "delete of operating (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "delete of operating (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-fact-overwrite-whole",
"input": {
"target_kind": "fact",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "overwrite of fact (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "overwrite of fact (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-skill-delete-multi-backed-truthy",
"input": {
"target_kind": "skill",
"operation": "delete",
"scope": "multi",
"attributed": true,
"backed_up": "yes"
},
"return": {
"verdict": "CONFIRM",
"reason": "delete of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "delete of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-constitution-delete-whole-backed",
"input": {
"target_kind": "constitution",
"operation": "delete",
"scope": "whole",
"attributed": true,
"backed_up": true
},
"return": {
"verdict": "CONFIRM",
"reason": "delete of constitution (score 8) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "delete of constitution (score 8) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-persona-overwrite-whole-backed",
"input": {
"target_kind": "persona",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": true
},
"return": {
"verdict": "CONFIRM",
"reason": "overwrite of persona (score 7) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "overwrite of persona (score 7) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-archive-delete-whole-score",
"input": {
"target_kind": "archive",
"operation": "delete",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "delete of archive (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "delete of archive (score 5) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-confirm-unknown-kind-delete",
"input": {
"target_kind": "widget",
"operation": "delete",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "CONFIRM",
"reason": "delete of widget (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "delete of widget (score 4) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "cl-allow-fact-append-single-defaults",
"input": {
"target_kind": "fact",
"operation": "append",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-fact-append-multi-score-3",
"input": {
"target_kind": "fact",
"operation": "append",
"scope": "multi",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-identity-read-single",
"input": {
"target_kind": "identity",
"operation": "read",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "read of identity (score 3) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "read of identity (score 3) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-archive-delete-single",
"input": {
"target_kind": "archive",
"operation": "delete",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "delete of archive (score 3) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "delete of archive (score 3) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-archive-read-whole",
"input": {
"target_kind": "archive",
"operation": "read",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "read of archive (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "read of archive (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-fact-read-whole",
"input": {
"target_kind": "fact",
"operation": "read",
"scope": "whole",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "read of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "read of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-unknown-operation",
"input": {
"target_kind": "fact",
"operation": "nuke",
"scope": "multi",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "nuke of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "nuke of fact (score 3) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-unknown-scope",
"input": {
"target_kind": "fact",
"operation": "append",
"scope": "galaxy",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-null-kind-renders-None",
"input": {
"target_kind": null,
"operation": "append",
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-null-operation-renders-None",
"input": {
"target_kind": "fact",
"operation": null,
"scope": "single",
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "None of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "None of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "cl-allow-null-scope",
"input": {
"target_kind": "fact",
"operation": "append",
"scope": null,
"attributed": true,
"backed_up": false
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
}
],
"guard": [
{
"name": "gu-empty-dict-all-defaults",
"input": {
"mutation": {}
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "gu-fact-append-allow",
"input": {
"mutation": {
"target_kind": "fact",
"operation": "append"
}
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "gu-unattributed-identity-deny",
"input": {
"mutation": {
"target_kind": "identity",
"operation": "overwrite",
"attributed": false
}
},
"return": {
"verdict": "DENY",
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed overwrite of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "gu-unbacked-constitution-delete-deny",
"input": {
"mutation": {
"target_kind": "constitution",
"operation": "delete",
"scope": "whole",
"attributed": true,
"backed_up": false
}
},
"return": {
"verdict": "DENY",
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "delete of constitution (score 8) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "gu-backed-skill-overwrite-confirm",
"input": {
"mutation": {
"target_kind": "skill",
"operation": "overwrite",
"scope": "whole",
"attributed": true,
"backed_up": true
}
},
"return": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "gu-attributed-null-falsy-deny",
"input": {
"mutation": {
"target_kind": "identity",
"attributed": null
}
},
"return": {
"verdict": "DENY",
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed append of identity \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "gu-attributed-empty-list-falsy-deny",
"input": {
"mutation": {
"target_kind": "persona",
"operation": "delete",
"attributed": []
}
},
"return": {
"verdict": "DENY",
"reason": "unattributed delete of persona \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
},
"expected": {
"verdict": "DENY",
"reason": "unattributed delete of persona \u2014 identity mutations require attribution",
"recovery": "re-submit with an attributed source (user, curator, or contract)"
}
},
{
"name": "gu-backed-up-zero-falsy-deny",
"input": {
"mutation": {
"target_kind": "skill",
"operation": "overwrite",
"scope": "whole",
"backed_up": 0
}
},
"return": {
"verdict": "DENY",
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
},
"expected": {
"verdict": "DENY",
"reason": "overwrite of skill (score 6) without a snapshot \u2014 destructive and unrecoverable",
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit"
}
},
{
"name": "gu-backed-up-truthy-string-confirm",
"input": {
"mutation": {
"target_kind": "skill",
"operation": "overwrite",
"scope": "whole",
"backed_up": "yes"
}
},
"return": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
},
"expected": {
"verdict": "CONFIRM",
"reason": "overwrite of skill (score 6) \u2014 high-value state, explicit confirmation required",
"recovery": "confirm with the snapshot id attached"
}
},
{
"name": "gu-read-allows",
"input": {
"mutation": {
"target_kind": "fact",
"operation": "read"
}
},
"return": {
"verdict": "ALLOW",
"reason": "read of fact (score 1) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "read of fact (score 1) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "gu-extra-keys-ignored",
"input": {
"mutation": {
"target_kind": "fact",
"operation": "append",
"note": "x",
"weight": 9
}
},
"return": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "gu-null-target-kind-renders-None",
"input": {
"mutation": {
"target_kind": null
}
},
"return": {
"verdict": "ALLOW",
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": "append of None (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
},
{
"name": "gu-empty-operation-string",
"input": {
"mutation": {
"target_kind": "fact",
"operation": ""
}
},
"return": {
"verdict": "ALLOW",
"reason": " of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
},
"expected": {
"verdict": "ALLOW",
"reason": " of fact (score 2) \u2014 small, reversible, attributed",
"recovery": ""
}
}
]
}
}
+236
View File
@@ -0,0 +1,236 @@
//! mutation_guards — #156: high-value cognitive-state mutation guards.
//!
//! FULL name-preserving port of src/rune/mutation_guards.py (dichotomy
//! wave, #2479 tranche).
//!
//! Large or destructive changes to Rune's most valuable learned state must
//! be visible, attributable, and recoverable. The guard classifies every
//! proposed mutation by destructiveness and returns a verdict:
//!
//! ```text
//! ALLOW — small, reversible, attributed change
//! CONFIRM — large or identity-touching: needs explicit confirmation
//! DENY — destructive without a recovery path (delete of unbacked
//! state, silent identity rewrite)
//! ```
//!
//! Classification (deterministic, no model):
//! - target kind: identity/persona/constitution > skill/belief block > fact
//! - operation: delete > overwrite > append
//! - scope: whole-block > multi-entry > single entry
//! - attribution: unattributed mutations to identity are DENY
//!
//! Recoverability: a CONFIRM/DENY verdict carries the recovery shape
//! (snapshot id required, or the reason none exists).
//!
//! FFI (doc-envelope law): one JSON doc in, {"ok": true, "value": ...} |
//! {"ok": false, "err": ...} out:
//! * mu_classify({target_kind, operation, scope, attributed, backed_up})
//! → {verdict, reason, recovery}
//! * mu_guard({mutation}) → {verdict, reason, recovery}
//!
//! No mu_free_string export — sibling crates sharing the Python process
//! already export identical free symbols; callers free through
//! capability_scoring's cs_free_string.
//!
//! Python semantics preserved exactly:
//! * severity-table lookups: unknown kind → 1, unknown op → 1,
//! unknown/null scope → 0
//! * branch order: the unattributed identity-family DENY runs FIRST
//! (it beats a read operation and the destructive gate), then the
//! destructive-unbacked DENY, then CONFIRM (score >= 4 or identity
//! kind with any mutating op), then ALLOW
//! * Python truthiness for `attributed`/`backed_up`: null/false/0/""/
//! empty array/empty object are falsy — non-empty strings are truthy
//! * guard_mutation's dict.get defaults apply only when the KEY is
//! missing; a present-but-null value passes through as falsy
//! * the reason f-strings render a null kind/operation as "None"
//! (Python str(None))
use serde_json::{json, Map, Value};
use std::os::raw::c_char;
// ── severity tables ──────────────────────────────────────────────────────────
/// Target-kind severity (higher = more protected)
fn kind_severity(kind: &Value) -> i64 {
match kind.as_str() {
Some("constitution") | Some("identity") | Some("persona") => 3,
Some("skill") | Some("belief") => 2,
Some("operating") | Some("fact") => 1,
Some("archive") => 0,
_ => 1,
}
}
/// Operation severity
fn op_severity(op: &Value) -> i64 {
match op.as_str() {
Some("delete") => 3,
Some("overwrite") => 2,
Some("append") => 1,
Some("read") => 0,
_ => 1,
}
}
fn scope_severity(scope: &Value) -> i64 {
match scope.as_str() {
Some("whole") => 2,
Some("multi") => 1,
Some("single") => 0,
_ => 0,
}
}
/// Python truthiness over JSON values (null/false/0/""/[]/{} are falsy).
fn truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
}
}
/// Python str() over the JSON value for the reason f-strings: a null kind
/// or operation renders "None", bools render "True"/"False".
fn py_display(v: &Value) -> String {
match v {
Value::Null => "None".to_string(),
Value::String(s) => s.clone(),
Value::Bool(true) => "True".to_string(),
Value::Bool(false) => "False".to_string(),
other => other.to_string(),
}
}
// ── the classification core ──────────────────────────────────────────────────
/// `classify_mutation` — classify a proposed mutation.
/// Returns {verdict, reason, recovery}.
pub fn classify_mutation(
target_kind: &Value,
operation: &Value,
scope: &Value,
attributed: &Value,
backed_up: &Value,
) -> Value {
let kind_sev = kind_severity(target_kind);
let op_sev = op_severity(operation);
let scope_sev = scope_severity(scope);
let score = kind_sev + op_sev + scope_sev;
let kind_str = py_display(target_kind);
let op_str = py_display(operation);
// Unattributed identity mutation is always DENY (the silent-rewrite class)
if matches!(
target_kind.as_str(),
Some("identity") | Some("persona") | Some("constitution")
) && !truthy(attributed)
{
return json!({
"verdict": "DENY",
"reason": format!(
"unattributed {op_str} of {kind_str} — identity mutations require attribution"),
"recovery": "re-submit with an attributed source (user, curator, or contract)",
});
}
// Destructive without recovery is DENY
if op_sev >= 2 && kind_sev >= 2 && !truthy(backed_up) {
return json!({
"verdict": "DENY",
"reason": format!(
"{op_str} of {kind_str} (score {score}) without a snapshot — destructive and unrecoverable"),
"recovery": "snapshot first (state_checkpoint.snapshot), then re-submit",
});
}
// Large or identity-touching needs confirmation
if score >= 4 || (kind_sev >= 3 && op_sev >= 1) {
return json!({
"verdict": "CONFIRM",
"reason": format!(
"{op_str} of {kind_str} (score {score}) — high-value state, explicit confirmation required"),
"recovery": "confirm with the snapshot id attached",
});
}
json!({
"verdict": "ALLOW",
"reason": format!(
"{op_str} of {kind_str} (score {score}) — small, reversible, attributed"),
"recovery": "",
})
}
/// `guard_mutation` — guard one mutation dict {target_kind, operation,
/// scope, attributed, backed_up}. Returns the verdict dict.
pub fn guard_mutation(mutation: &Value) -> Value {
let empty = Map::new();
let m = mutation.as_object().unwrap_or(&empty);
let d_kind = Value::String("fact".into());
let d_op = Value::String("append".into());
let d_scope = Value::String("single".into());
let d_attributed = Value::Bool(true);
let d_backed_up = Value::Bool(false);
// dict.get(key, default): the default applies ONLY when the key is
// missing; a present-but-null value passes through as falsy.
let target_kind = m.get("target_kind").unwrap_or(&d_kind);
let operation = m.get("operation").unwrap_or(&d_op);
let scope = m.get("scope").unwrap_or(&d_scope);
let attributed = m.get("attributed").unwrap_or(&d_attributed);
let backed_up = m.get("backed_up").unwrap_or(&d_backed_up);
classify_mutation(target_kind, operation, scope, attributed, backed_up)
}
// ── FFI (doc-envelope law) ───────────────────────────────────────────────────
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
if ptr.is_null() {
return Err("null argument pointer".to_string());
}
let x = unsafe { std::ffi::CStr::from_ptr(ptr) }
.to_str()
.map_err(|e| e.to_string())?;
serde_json::from_str(x).map_err(|e| e.to_string())
}
fn envelope(r: Result<Value, String>) -> *mut c_char {
let v = match r {
Ok(x) => json!({"ok": true, "value": x}),
Err(e) => json!({"ok": false, "err": e}),
};
std::ffi::CString::new(serde_json::to_string(&v).unwrap_or_default())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
/// mu_classify({target_kind, operation, scope, attributed, backed_up})
/// → {verdict, reason, recovery}
#[no_mangle]
pub extern "C" fn mu_classify(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let unset = Value::Null;
let g = |k: &str| a.get(k).unwrap_or(&unset);
classify_mutation(
g("target_kind"),
g("operation"),
g("scope"),
g("attributed"),
g("backed_up"),
)
}))
}
/// mu_guard({mutation}) → {verdict, reason, recovery}
#[no_mangle]
pub extern "C" fn mu_guard(arg: *const c_char) -> *mut c_char {
envelope(read_arg(arg).map(|a| {
let mutation = a.get("mutation").cloned().unwrap_or(Value::Null);
guard_mutation(&mutation)
}))
}
#[cfg(test)]
mod branch_battery;