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:
1 parent
569ee78d0a
commit
e9d3efae5d
9 files changed
+1947
-20
No files matched your search
@@ -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())
|
||||
@@ -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")
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
Generated
+105
@@ -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"
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user