The independent W-0R review of 3592b92e passed but left two PLAUSIBLE
findings: the stderr notice for a legitimately-omitted operator source was
claimed and never asserted (a silent omission is the original defect in
miniature), and the chmod 0o000 unreadable simulations fail spuriously when
euid==0 (CAP_DAC_OVERRIDE). Falsifier for the new assertion: deleting the
notice block turns the suite red (failures=3); restoring returns green.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EHYXhcCQsL3J1Lnm7EraGq
180 lines
7.3 KiB
Python
180 lines
7.3 KiB
Python
#!/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 contextlib
|
|
import io
|
|
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)
|
|
|
|
# chmod 0o000 does not deny root (CAP_DAC_OVERRIDE), so the unreadable
|
|
# simulations would fail spuriously in a root container.
|
|
runs_unprivileged = unittest.skipIf(
|
|
os.geteuid() == 0, "chmod 0o000 cannot make a file unreadable to root"
|
|
)
|
|
|
|
|
|
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()
|
|
notice = io.StringIO()
|
|
with contextlib.redirect_stderr(notice):
|
|
bound = self.source_ids()
|
|
self.assertNotIn(source_id, bound)
|
|
for required in lease_promote.REQUIRED_SOURCES:
|
|
self.assertIn(required, bound)
|
|
# A silent omission is the original defect in miniature: the
|
|
# narrower binding must announce itself.
|
|
self.assertIn(source_id, notice.getvalue())
|
|
|
|
# --- unreadable is never the same as absent -----------------------------
|
|
|
|
@runs_unprivileged
|
|
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 --------------------------------------
|
|
|
|
@runs_unprivileged
|
|
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()
|
|
|
|
@runs_unprivileged
|
|
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()
|