Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,4 +571,9 @@ environment. Verification machinery is consumed from the `receipt`
package with this repository's trust pins committed in
`scripts/receipt_pins.py`; see `releases/README.md` (immutable
post-genesis, like everything under `releases/`) for the schema, offline
verification procedure, and security limits.
verification procedure, and security limits. The gate surface in that file
lists every base-checkout file that can decide a verdict: all of `scripts/`,
the append workflow, the anchors, and the uv inputs that build the gate's
environment (`pyproject.toml`, `uv.lock`, `uv.toml`, `.python-version`,
`.venv/`). The gate refuses a pull request that changes any of them together
with the ledger, and names them when a pull request changes only them.
22 changes: 18 additions & 4 deletions scripts/receipt_pins.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,27 @@
(f"releases/anchors/{LEDGER_SPEC.producer_public_key_filename}"),
}
),
# Every file in the judging (base) checkout that can decide a verdict. A
# proposal that changes one of them together with the ledger is refused as
# mixed, and a proposal that changes only these is reported by name.
gate_surface=frozenset(
{
"scripts/check_thesis_facts_append.py",
"scripts/verify_release_chain.py",
"scripts/canonical_json.py",
"scripts/cut_release_manifest.py",
# The judge runs scripts/check_thesis_facts_append.py, so scripts/
# is sys.path[0], and both pull request jobs also put it on
# PYTHONPATH. Any file there can decide the verdict: these pins,
# a module that shadows an import (the standard library's
# included), or a sitecustomize.py, which runs at startup.
"scripts/**",
".github/workflows/thesis-facts-append.yml",
# `uv sync --locked --no-dev --project <base>` builds the judge's
# environment from these, including the receipt version that
# implements the gate. uv also reads the project's uv.toml and
# .python-version, and reuses a .venv it finds there.
"pyproject.toml",
"uv.lock",
"uv.toml",
".python-version",
".venv/**",
"releases/anchors/**",
}
),
Expand Down
48 changes: 48 additions & 0 deletions tests/test_receipt_shim_transparency.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,54 @@ def test_original_oracle_fixtures_are_authenticated(name: str) -> None:
assert _sha256(ORIGINAL_FIXTURES / name) == ORIGINAL_HASHES[name]


def _original_surface(name: str) -> frozenset[str]:
"""Read one surface literal from the authenticated original gate."""

import ast

module = ast.parse(
(ORIGINAL_FIXTURES / "check_thesis_facts_append.py").read_text(encoding="utf-8")
)
for node in module.body:
if (
isinstance(node, ast.Assign)
and [getattr(target, "id", None) for target in node.targets] == [name]
and isinstance(node.value, ast.Call)
):
return frozenset(ast.literal_eval(node.value.args[0]))
raise AssertionError(f"{name} not found in the original gate")


def test_the_shim_gate_surface_deliberately_widens_the_originals() -> None:
"""The one place the pair is meant to disagree about classification.

The originals' GATE_SURFACE predates scripts/receipt_pins.py, where the
shim's trust pins now live, and never listed the uv inputs that provision
the judge. The shim's surface covers every original entry and adds those,
so a proposal that changes one of them is refused as mixed (with rows) or
named as gate-only (alone), where the original judged it as plain data.
No case in this file changes such a file, so the byte-identity asserted
here is unaffected; tests/test_thesis_append_shim_isolation.py holds the
shim to the wider surface.
"""

from receipt.append_gate import _matches_surface

from scripts.receipt_pins import APPEND_GATE_SPEC

original_gate = _original_surface("GATE_SURFACE")
shim_gate = APPEND_GATE_SPEC.gate_surface
for entry in original_gate:
if entry.endswith("/**"):
assert entry in shim_gate, entry
else:
assert _matches_surface(entry, shim_gate), entry
assert _original_surface("DATA_SURFACE") == APPEND_GATE_SPEC.data_surface
for widened in ("scripts/receipt_pins.py", "pyproject.toml", "uv.lock"):
assert not _matches_surface(widened, original_gate), widened
assert _matches_surface(widened, shim_gate), widened


@pytest.fixture(scope="session")
def original_oracle(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path:
"""Copy the authenticated original scripts into one executable tree."""
Expand Down
237 changes: 237 additions & 0 deletions tests/test_thesis_append_shim_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _subject_line(clone: pathlib.Path, candidate: str, base: str | None) -> str:
line += f" base commit {base} tree {_git(clone, 'rev-parse', base + '^{tree}')}"
return line


# A commit id of the right shape that no repository holds.
ABSENT_OBJECT_ID = "0" * 40

Expand Down Expand Up @@ -942,6 +943,242 @@ def test_a_true_append_is_accepted_end_to_end(tmp_path):
_assert_nothing_left_behind(clone, tmp_path / "tmp")


# Files that decide a verdict when they sit in the judging checkout, one per
# way in: the pins the shim imports, the uv inputs the workflow provisions the
# judge from, and files that are not there today but would be read if added.
# Until 2026-09-29 the first three were off the gate surface, so a resolver-
# shaped append that also edited them was judged as plain data and never named
# them (PolicyEngine/chronicle#299 review).
GATE_DECIDERS = (
"scripts/receipt_pins.py",
"pyproject.toml",
"uv.lock",
"uv.toml",
".python-version",
"scripts/sitecustomize.py",
".venv/lib/python3.14/site-packages/zz_injected.pth",
)


def _change_gate_decider(relative: str):
"""Return a mutation that changes one decider, or adds it if absent."""

def mutate(root: pathlib.Path) -> None:
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
handle.write("# a change riding the proposal\n")

return mutate


def _copy_existing_gate_deciders(root: pathlib.Path) -> None:
"""Put the repository's own copy of every decider it has into the base."""

for relative in GATE_DECIDERS:
source = ROOT / relative
if source.is_file():
destination = root / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination)


@pytest.mark.parametrize("relative", GATE_DECIDERS)
def test_an_append_that_also_changes_a_gate_decider_is_refused_as_mixed(
tmp_path, relative
):
"""The witnessed append plus one decider change cannot pass as data.

The refusal comes before any ledger check and names the decider, so a trust
change can no longer ride an append that the resolver merges on its own.
"""

clone, base, candidate = _replay_latest_release(
tmp_path,
prepare=_copy_existing_gate_deciders,
mutate=_change_gate_decider(relative),
)

completed = _run_shim(
clone, commit=candidate, base_ref=base, temporary_root=tmp_path / "tmp"
)

assert completed.returncode == 1, completed.stdout + completed.stderr
assert completed.stdout == ""
assert f"{FAILED}mixed data/gate proposal is forbidden: " in completed.stderr
assert (
f"; GATE_SURFACE changes={[relative]!r}; split them into separate pull requests"
) in completed.stderr
_assert_nothing_left_behind(clone, tmp_path / "tmp")


@pytest.mark.parametrize("relative", GATE_DECIDERS)
def test_a_gate_decider_change_alone_is_a_named_gate_only_proposal(tmp_path, relative):
clone, base = _replay_current_state(tmp_path)
_copy_existing_gate_deciders(clone)
base = _commit(clone, "deciders in the base")
_change_gate_decider(relative)(clone)
candidate = _commit(clone, f"change {relative}")

completed = _run_shim(
clone, commit=candidate, base_ref=base, temporary_root=tmp_path / "tmp"
)

assert completed.returncode == 0, completed.stdout + completed.stderr
assert completed.stdout.splitlines() == [
"thesis-facts append check OK: gate-only proposal; DATA_SURFACE "
f"unchanged; GATE_SURFACE changes={[relative]!r}",
_subject_line(clone, candidate, base),
]
_assert_nothing_left_behind(clone, tmp_path / "tmp")


def test_an_append_beside_unchanged_gate_deciders_still_passes(tmp_path):
"""The resolver's shape: ledger and release files only, deciders untouched."""

clone, base, candidate = _replay_latest_release(
tmp_path, prepare=_copy_existing_gate_deciders
)
changed = set(_git(clone, "diff", "--name-only", base, candidate).splitlines())
assert changed and all(
path.startswith(("ledger/", "releases/manifests/")) for path in changed
), changed

completed = _run_shim(
clone, commit=candidate, base_ref=base, temporary_root=tmp_path / "tmp"
)

assert completed.returncode == 0, completed.stdout + completed.stderr
assert completed.stdout == (
f"{APPEND_GATE_OK}\n{_subject_line(clone, candidate, base)}\n"
)
_assert_nothing_left_behind(clone, tmp_path / "tmp")


OPENED_FILES_DRIVER = """\
import json
import os
import pathlib
import sys

opened = set()


def _record(event, arguments):
if event == "open" and isinstance(arguments[0], (str, bytes, os.PathLike)):
opened.add(os.fsdecode(arguments[0]))


sys.addaudithook(_record)
sys.path.insert(0, {scripts!r})

import check_thesis_facts_append as shim # noqa: E402

try:
code = shim.main()
finally:
opened.update(
getattr(module, "__file__", None) or "" for module in list(sys.modules.values())
)
pathlib.Path({report!r}).write_text(json.dumps(sorted(opened)), encoding="utf-8")
raise SystemExit(code)
"""


def test_every_file_the_gate_reads_from_its_own_checkout_is_on_the_gate_surface(
tmp_path,
):
"""The invariant behind GATE_SURFACE, measured on an accepting run.

Every file the gate process opens or imports from inside the judging
checkout is on the surface. That is the pins, the gate's scripts, the
trust anchors, and the installed receipt wheel, which lives in the
checkout's own .venv just as it does in the workflow's base-gate clone.
"""

from receipt.append_gate import _matches_surface

from scripts.receipt_pins import APPEND_GATE_SPEC

clone, base, candidate = _replay_latest_release(tmp_path)
report = tmp_path / "opened.json"
driver = tmp_path / "driver" / "driver.py"
driver.parent.mkdir()
driver.write_text(
OPENED_FILES_DRIVER.format(scripts=str(SHIM_SCRIPTS), report=str(report)),
encoding="utf-8",
)

completed = subprocess.run(
[
sys.executable,
str(driver),
"--root",
str(clone),
"--commit",
candidate,
"--base-ref",
base,
],
cwd=clone,
capture_output=True,
text=True,
check=False,
)
assert completed.returncode == 0, completed.stdout + completed.stderr

root = ROOT.resolve()
inside = set()
for name in json.loads(report.read_text(encoding="utf-8")):
if not name:
continue
path = pathlib.Path(name)
if not path.is_absolute():
path = clone / path
resolved = path.resolve()
if resolved.is_relative_to(root):
inside.add(resolved.relative_to(root).as_posix())
assert "scripts/receipt_pins.py" in inside
assert any(path.startswith("releases/anchors/") for path in inside), inside
off_surface = sorted(
path
for path in inside
if not _matches_surface(path, APPEND_GATE_SPEC.gate_surface)
)
assert off_surface == [], off_surface


def test_the_gate_surface_covers_the_judges_import_path_and_environment():
"""What a run cannot show: files that would decide a verdict if added.

The judge runs a script in scripts/, so scripts/ is sys.path[0], and both
pull request jobs also put it on PYTHONPATH, where a sitecustomize.py runs
at startup. The whole directory is therefore on the surface, not just the
files imported today. The judge's environment comes from the base's own
uv project, so its inputs are on the surface too.
"""

from scripts.receipt_pins import APPEND_GATE_SPEC

workflow = (ROOT / ".github" / "workflows" / "thesis-facts-append.yml").read_text(
encoding="utf-8"
)
assert 'PYTHONPATH="$base_gate/scripts"' in workflow
assert 'uv sync --locked --no-dev --project "$base_gate"' in workflow
assert SHIM.parent.relative_to(ROOT).as_posix() == "scripts"
for required in (
"scripts/**",
".github/workflows/thesis-facts-append.yml",
"pyproject.toml",
"uv.lock",
"uv.toml",
".python-version",
".venv/**",
"releases/anchors/**",
):
assert required in APPEND_GATE_SPEC.gate_surface, required


def test_the_scratch_directory_is_private_to_the_run(tmp_path):
"""The private directory is created 0700, and the shim asserts that it was.

Expand Down
Loading