feat(lease): verified lease-remediation stack (rebased onto next) — promotion trigger + promote CLI + carve-out + TTL #1109
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user