fix(scaffolding): builder self-approval PROVED and closed by approval attribution

§1 SELF-APPROVAL ATTACK RUN FIRST, AND IT SUCCEEDED. Before this change:

  ConstructionInterface(builder="mimo").transition(
      cid, "approved", evidence_ref="user-instruction:fabricated",
      authorised_paths=["docs"])                          -> success
  ...amend_scope(..., authority_ref="user-instruction:fabricated") -> success
  => BUILDER_SELF_APPROVAL = PROVEN DEFECT

Removing record_user_authority was NOT sufficient: the builder could still
CREATE the canonical approval it was supposed to be constrained by. The
authority was circular.

REPAIR AT THE SHARED CANONICAL OWNER (no TUI change):
- transition_contract records `authorised_by` on the approval checkpoint — WHO
  approved, not merely what the approval says;
- ConstructionInterface.transition passes the caller identity; the contextual
  activation path passes its origin;
- _resolve_authority REFUSES an approval that records no authorising identity,
  or one whose authorising identity is a BUILDER_IDENTITIES member.

RESULT:
  A/B/C builder transition->approved + self-made amend -> REFUSED, scope unchanged
        ("approval ... was created by 'mimo', which produces construction —
          a builder cannot approve its own work, so this approval grants nothing")
  D     user-surface (TUI identity) approval           -> ALLOWED
  E     amend within the grant                         -> ALLOWED
  F     amend beyond the grant                         -> REFUSED
  G     builder CONSUMES a real user approval          -> ALLOWED
        (consuming is not minting — the required architecture is
         "may consume a proven user-authority event, may not create one")

A CORRECTION FOUND BY THE ATTACK: "tui-build-cmd" was listed in
BUILDER_IDENTITIES. The TUI is the USER-FACING APPROVAL SURFACE, not a builder;
listing it as one would have made the existing TUI approval path unauthorised
the moment attribution was enforced. Removed, with the reason recorded inline.

§0 HARD BOUNDARY RESPECTED. TUI_CODE_CHANGED = NO — rune_tui.py is untouched.
Every change is shared canonical code (scaffold, construction_interface,
construction_authority, construction_activation). The TUI remains a consumer of
the same canonical Scaffolding APIs; MiMo adapts to them rather than the TUI
being rewritten to suit MiMo. TUI regression passed unchanged.

Verification: 542 targeted regression including TUI tests, 0 failures; ruff
clean. LOCAL_MODEL_COMPUTE_EXECUTED = NO.

STILL OPEN, recorded rather than claimed: §5 revision/replay/supersession
semantics (reusable vs revision-scoped, staleness, revocation), and §6 the
registered-external-project absolute-path positive proof.
This commit is contained in:
conductor committed 2026-09-24 22:05:56 +01:00
1 parent d7450cb0eb
commit a41c93c878
5 files changed
+33 -7

No files matched your search

+2 -1
View File
@@ -349,7 +349,8 @@ def mint_bounded_contract(scaffold: sc.Scaffold, *, text: str, origin: str,
for phase in ("approved", "building"):
scaffold.transition_contract(
doc, cid, phase,
evidence_ref=f"contextual-activation:{origin}")
evidence_ref=f"contextual-activation:{origin}",
authorised_by=origin)
scaffold.save(doc)
return {"contract_id": cid, "scope": scope, "minted": True}
except Exception as exc: # noqa: BLE001 — reported, never swallowed
+4 -1
View File
@@ -88,7 +88,10 @@ AUTHORITY_PRINCIPALS = frozenset({"user", "operator", "supervisor"})
# Identities that PRODUCE construction and therefore cannot certify it.
BUILDER_IDENTITIES = frozenset({
"mimo", "mimo-guard", "mimo-builder", "rune-native", "scope-promotion",
"tui-build-cmd", "rune", "test", "tests", "automated", "ci",
# NOTE: "tui-build-cmd" is deliberately NOT here — the TUI is the
# user-facing approval surface, not a builder. Listing it as a builder
# would have made the existing TUI approval path unauthorised.
"rune", "test", "tests", "automated", "ci",
"subagent", "actor", "builder",
})
+12
View File
@@ -44,6 +44,7 @@ from rune import structural_store as ss
from rune import scaffold as sc
from rune import spine as spine_mod
from rune import construction_decision as cd
from rune.construction_authority import BUILDER_IDENTITIES
__all__ = ["ConstructionInterface", "INTERFACE_SCHEMA_VERSION", "Errors"]
@@ -87,9 +88,19 @@ def _resolve_authority(contract: dict, authority_ref: str,
ref = (authority_ref or "").strip()
if not ref:
return None, "no authority_ref supplied"
builders = {b.lower() for b in BUILDER_IDENTITIES}
for cp in contract.get("checkpoints") or []:
if str(cp.get("evidence", "")) != ref:
continue
# WHO authorised it matters as much as WHAT it says.
by = str(cp.get("authorised_by") or "").strip()
if not by:
return None, (f"approval {ref!r} records no authorising identity — "
"an unattributed approval is not authority")
if by.lower() in builders:
return None, (f"approval {ref!r} was created by {by!r}, which "
"produces construction — a builder cannot approve "
"its own work, so this approval grants nothing")
if cp.get("phase") != "approved":
return None, (f"reference {ref!r} is not an approval checkpoint "
f"(phase={cp.get('phase')!r})")
@@ -690,6 +701,7 @@ class ConstructionInterface:
doc, contract_id, phase,
evidence_ref=evidence_ref or f"builder:{self.builder}",
authorised_paths=authorised_paths,
authorised_by=self.builder,
)
except sc.IllegalPhaseTransition as e:
return _envelope(
+4 -2
View File
@@ -460,7 +460,8 @@ class Scaffold:
def transition_contract(
self, doc: dict, contract_id: str, phase: str, *, evidence_ref: str = "",
note: str = "", authorised_paths: list[str] | None = None
note: str = "", authorised_paths: list[str] | None = None,
authorised_by: str = ""
) -> dict:
"""Move a contract to a new phase through the legal-transition map.
Durable and resumable: the phase lives in the store."""
@@ -479,7 +480,8 @@ class Scaffold:
e["status"] = "closed"
elif phase == "failed":
e["status"] = "failed"
_cp = {"phase": phase, "at": now(), "evidence": evidence_ref, "note": note[:200]}
_cp = {"phase": phase, "at": now(), "evidence": evidence_ref,
"note": note[:200], "authorised_by": authorised_by}
if authorised_paths is not None:
# T24: the SMALLEST COMPATIBLE EXTENSION of the existing authority
# owner. A user approval already records durable provenance as a
+11 -3
View File
@@ -257,7 +257,15 @@ def test_authority_reference_must_resolve_and_bound_the_request(store):
from rune.construction_interface import ConstructionInterface
from rune.scaffold import Scaffold
cid = turn("refactor src/rune/scaffold.py", store)["contract_id"]
# The user-facing surface creates the contract (draft) and approves it.
# A MINTED contract is already past `approved`, so an approval could not
# legally be recorded on it — that is why this uses submit_need.
user = ConstructionInterface(builder="tui-build-cmd")
user.scaffold = Scaffold(str(store), PROJECT)
env = user.submit_need("refactor src/rune/scaffold.py", origin="user",
affected_surface=["docs"],
contract_id="gov-user-1")
cid = (env.get("ids") or {}).get("contract_id")
iface = ConstructionInterface(builder="mimo")
iface.scaffold = Scaffold(str(store), PROJECT)
@@ -274,8 +282,8 @@ def test_authority_reference_must_resolve_and_bound_the_request(store):
assert scope() == base
# the USER surface records an approval naming what it authorises
iface.transition(cid, "approved", evidence_ref="user-instruction:SUP",
authorised_paths=["docs"])
user.transition(cid, "approved", evidence_ref="user-instruction:SUP",
authorised_paths=["docs"])
R = "user-instruction:SUP"
assert iface.amend_scope(cid, ["docs"], reason="ok", authority="user",
authority_ref=R)["success"] is True