feat(lease): verified lease-remediation stack (rebased onto next) — promotion trigger + promote CLI + carve-out + TTL #1109

Open
mos-dt-0 wants to merge 24 commits from feat/lease-promotion-and-harness-isolation into next
3 changed files with 241 additions and 10 deletions
Showing only changes of commit 79e18343ca - Show all commits
@@ -31,13 +31,28 @@ while proving nothing — a gate-disabler indistinguishable from a working fix
unless someone looks for it. **This module never posts a receipt.** Emitting it
belongs to the runtime adapter, where a real model turn happens.
The construction binds the exact normative source bytes, so a VERIFIED lease
means "this agent is running THIS law", not merely "this session id is known".
``h_source`` / ``h_payload`` are derived by the framework's own
The construction binds the exact normative source bytes. ``h_source`` /
``h_payload`` are derived by the framework's own
``normative_fragments.build_payload`` rather than reimplemented: the broker
derives them the same way and any divergence yields ``PAYLOAD_BINDING_MISMATCH``.
There must be exactly one implementation.
WHAT THE BINDING DOES *NOT* PROVE
---------------------------------
It is tempting to read a VERIFIED lease as "this agent is running THIS law".
**It does not mean that**, and writing it down that way is how the belief spread.
The broker holds no reference copy of any normative source and never opens one;
it recomputes ``h_source`` / ``h_payload`` from the fragment bytes THIS CLIENT
sent and compares them to the binding THIS CLIENT sent (``daemon.py:602-616``).
Both sides of that comparison originate here, so it detects corruption in
transit and nothing else. What the binding actually asserts is "the client
claims these bytes, self-consistently".
Making it mean the stronger thing requires the broker to re-read the on-disk
sources itself, against a manifest the agent cannot rewrite — i.e. broker code
attestation under its own uid. Until then, do not cite a VERIFIED lease as
evidence of law integrity.
Usage
-----
lease_promote.py --begin # prints the receipt the MODEL must emit
@@ -48,6 +63,7 @@ from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
import socket
@@ -70,8 +86,9 @@ MANIFEST_VERSION: Final = 1
GENERATOR_VERSION: Final = "mosaic/lease_promote@1"
DEFAULT_TTL_SECONDS: Final = 300
# Normative sources whose exact bytes bind the lease. Sources absent on a given
# deployment are simply not part of the binding — never fabricated.
# Normative sources whose exact bytes bind the lease, in binding order. Order is
# load-bearing: ``h_source`` frames the resolved sequence, so reordering changes
# the derivation. Never fabricate a source that is not on disk.
FRAGMENT_SOURCES: Final = (
"CONSTITUTION.md",
"AGENTS.md",
@@ -81,6 +98,30 @@ FRAGMENT_SOURCES: Final = (
"TOOLS.md",
)
# Framework-owned sources, reconciled on every upgrade — `install.sh:76`
# FRAMEWORK_OWNED and `config/file-adapter.ts` FRAMEWORK_OWNED_FILES — plus the
# per-runtime contract shipped under `framework/runtime/<runtime>/`. A deployment
# missing one of these is broken, not minimal, so their absence is refused rather
# than silently dropped from the binding.
#
# SOUL.md and USER.md are deliberately excluded: install.sh does not seed them
# ("intentionally NOT seeded here — they are generated by `mosaic init`"), so a
# fresh install legitimately lacks both. TOOLS.md is user-seeded on first install
# only. Absence of those three is reported, not fatal.
REQUIRED_SOURCES: Final = frozenset({"CONSTITUTION.md", "AGENTS.md", "STANDARDS.md"})
class IncompleteBinding(RuntimeError):
"""A source that must bind this lease could not be read.
**Never downgrade this to a skip.** The broker recomputes the hashes from the
fragments it is sent, so an omitted fragment is internally consistent and
``PAYLOAD_BINDING_MISMATCH`` cannot fire — a partial law promotes exactly like
a complete one, and nothing downstream can tell the difference. Dropping an
unreadable source therefore does not degrade the binding, it forges a smaller
one. Fail here, where the omission is still visible.
"""
def mosaic_home() -> Path:
return Path(os.environ.get("MOSAIC_HOME") or Path.home() / ".config" / "mosaic")
@@ -116,16 +157,33 @@ def session_identity() -> tuple[str, int, str]:
def build_construction(runtime: str) -> tuple[dict[str, object], object]:
"""Assemble the wire construction and derive its hashes with the sole builder."""
sources = list(FRAGMENT_SOURCES) + [f"runtime/{runtime}/RUNTIME.md"]
runtime_contract = f"runtime/{runtime}/RUNTIME.md"
sources = list(FRAGMENT_SOURCES) + [runtime_contract]
required = REQUIRED_SOURCES | {runtime_contract}
wire_fragments: list[dict[str, str]] = []
objects: list[NormativeFragment] = []
absent: list[str] = []
for source_id in sources:
try:
content = (mosaic_home() / source_id).read_bytes()
except OSError:
except FileNotFoundError:
# Genuinely not on disk. Legitimate only for operator-owned sources.
if source_id in required:
raise IncompleteBinding(
f"required normative source is absent: {source_id}"
) from None
absent.append(source_id)
continue
import hashlib
except OSError as exc:
# The path resolves but will not read — EACCES, EIO, EISDIR, ELOOP.
# That is an anomaly for EVERY source, optional ones included: an
# unreadable file is not an un-configured one, and treating it as
# absent is what lets a permission change quietly shrink the law.
raise IncompleteBinding(
f"normative source is present but unreadable: {source_id} "
f"({type(exc).__name__})"
) from exc
digest = hashlib.sha256(content).hexdigest()
wire_fragments.append(
@@ -138,7 +196,17 @@ def build_construction(runtime: str) -> tuple[dict[str, object], object]:
objects.append(NormativeFragment(source_id, content, digest))
if not wire_fragments:
raise RuntimeError("no normative sources found — refusing to build an empty binding")
raise IncompleteBinding("no normative sources found — refusing an empty binding")
# Absence is legitimate here but never invisible. The omission is already
# baked into h_source (the framed source sequence differs), but nothing
# compares h_source to an expected value, so this line is the only place a
# human learns the binding was narrower than the full set.
if absent:
print(
f"lease_promote: binding omits absent operator sources: {', '.join(absent)}",
file=sys.stderr,
)
result = build_payload(
manifest_version=MANIFEST_VERSION,
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""The promotion client must never build a binding narrower than it claims.
RED-first against a real defect: ``build_construction`` skipped any normative
source it could not read (``except OSError: continue``) and promoted whatever
remained. That is not a degraded binding, it is a forged smaller one — the
broker recomputes ``h_source`` / ``h_payload`` from the fragments it is *sent*
(``daemon.py:602-616``), so an omitted fragment is internally consistent and
``PAYLOAD_BINDING_MISMATCH`` cannot fire. Measured before the fix: with only
``USER.md`` readable (964 bytes on the live host), the client produced a
one-fragment construction with ``promotion=True``.
The classification under test mirrors the framework's own file ownership, and
must keep mirroring it:
* framework-owned, reconciled every upgrade (``install.sh`` FRAMEWORK_OWNED /
``config/file-adapter.ts`` FRAMEWORK_OWNED_FILES) plus the per-runtime
contract — absence is a broken deployment, so it is REFUSED;
* ``SOUL.md`` / ``USER.md`` — install.sh deliberately does not seed them
("generated by `mosaic init`"), so absence is legitimate and ALLOWED.
Unreadable is treated separately from absent for *every* source, optional ones
included: a file that will not open is not a file that was never configured, and
collapsing the two is what let a permission change quietly shrink the law.
"""
from __future__ import annotations
import os
import sys
import tempfile
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
sys.path.insert(0, str(TOOLS))
import lease_promote # noqa: E402
RUNTIME = "pi"
RUNTIME_CONTRACT = f"runtime/{RUNTIME}/RUNTIME.md"
ALL_SOURCES = (*lease_promote.FRAGMENT_SOURCES, RUNTIME_CONTRACT)
REQUIRED = frozenset(lease_promote.REQUIRED_SOURCES) | {RUNTIME_CONTRACT}
# Derived, never listed: a hand-kept second copy is exactly the drift this file
# exists to catch.
OPTIONAL = tuple(s for s in ALL_SOURCES if s not in REQUIRED)
class PromotionBindingTest(unittest.TestCase):
def setUp(self) -> None:
self._previous_home = os.environ.get("MOSAIC_HOME")
self._temporary = tempfile.TemporaryDirectory()
self.root = Path(self._temporary.name)
for source_id in ALL_SOURCES:
path = self.root / source_id
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(f"# {source_id}\nnormative bytes\n".encode())
os.environ["MOSAIC_HOME"] = str(self.root)
def tearDown(self) -> None:
for path in self.root.rglob("*"):
if path.is_file():
path.chmod(0o644)
self._temporary.cleanup()
if self._previous_home is None:
os.environ.pop("MOSAIC_HOME", None)
else:
os.environ["MOSAIC_HOME"] = self._previous_home
def reset_home(self) -> None:
"""Discard the current home and seed a fresh complete one.
Each subTest mutates the tree destructively, so it needs a clean start —
and the old one must be released, not orphaned.
"""
self.tearDown()
self.setUp()
def build(self):
return lease_promote.build_construction(RUNTIME)
def source_ids(self) -> list[str]:
construction, _ = self.build()
return [f["source_id"] for f in construction["fragments"]]
# --- the binding is complete when the deployment is complete -------------
def test_complete_deployment_binds_every_source(self) -> None:
construction, result = self.build()
self.assertEqual([f["source_id"] for f in construction["fragments"]], list(ALL_SOURCES))
self.assertTrue(result.promotion)
# --- absence: refused for framework-owned, allowed for operator-owned ----
def test_absent_required_source_is_refused(self) -> None:
for source_id in sorted(REQUIRED):
with self.subTest(source=source_id):
self.reset_home()
(self.root / source_id).unlink()
with self.assertRaises(lease_promote.IncompleteBinding) as caught:
self.build()
self.assertIn(source_id, str(caught.exception))
def test_absent_operator_source_still_binds_the_rest(self) -> None:
for source_id in OPTIONAL:
with self.subTest(source=source_id):
self.reset_home()
(self.root / source_id).unlink()
bound = self.source_ids()
self.assertNotIn(source_id, bound)
for required in lease_promote.REQUIRED_SOURCES:
self.assertIn(required, bound)
# --- unreadable is never the same as absent -----------------------------
def test_unreadable_source_is_refused_even_when_optional(self) -> None:
for source_id in ALL_SOURCES:
with self.subTest(source=source_id):
self.reset_home()
(self.root / source_id).chmod(0o000)
with self.assertRaises(lease_promote.IncompleteBinding) as caught:
self.build()
self.assertIn(source_id, str(caught.exception))
# --- the exact measured regression --------------------------------------
def test_single_readable_source_cannot_promote(self) -> None:
"""The observed failure: only USER.md readable produced a valid binding."""
for source_id in ALL_SOURCES:
if source_id != "USER.md":
(self.root / source_id).chmod(0o000)
with self.assertRaises(lease_promote.IncompleteBinding):
self.build()
def test_no_source_readable_cannot_promote(self) -> None:
for source_id in ALL_SOURCES:
(self.root / source_id).chmod(0o000)
with self.assertRaises(lease_promote.IncompleteBinding):
self.build()
# --- the classification must not drift from the framework's -------------
def test_required_set_excludes_only_the_unseeded_sources(self) -> None:
"""`install.sh` decides which files exist; this list must follow it.
If a source moves between framework-owned and operator-generated
upstream, this fails and forces the classification to be re-read rather
than silently inherited.
"""
self.assertEqual(
set(lease_promote.REQUIRED_SOURCES),
{"CONSTITUTION.md", "AGENTS.md", "STANDARDS.md"},
"REQUIRED_SOURCES changed — re-read install.sh FRAMEWORK_OWNED and "
"config/file-adapter.ts FRAMEWORK_OWNED_FILES before accepting it",
)
self.assertTrue(
set(lease_promote.REQUIRED_SOURCES) <= set(lease_promote.FRAGMENT_SOURCES),
"a required source is not in the binding order",
)
if __name__ == "__main__":
unittest.main()