#!/usr/bin/env python3 # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc from __future__ import annotations import argparse import ast import hashlib import json import subprocess import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] OUTPUT = REPO_ROOT / "solstone/think/generated/access_rejections.py" ORACLE_COMMIT = "dd04f55c8" ORACLE_PATH = "solstone/think/sol_cli.py" ORACLE_BLOB = "a20570fc0994f6215a013e8c89ce7776ddec7d17" EXPECTED_COUNT = 5 SENTINELS = {"import", "call"} def git_bytes(*args: str) -> bytes: return subprocess.check_output(["git", *args], cwd=REPO_ROOT) def git_hash_object(data: bytes) -> str: header = f"blob {len(data)}\0".encode() return hashlib.sha1(header + data, usedforsecurity=False).hexdigest() def oracle_text() -> str: blob = git_bytes("rev-parse", f"{ORACLE_COMMIT}:{ORACLE_PATH}").decode().strip() if blob != ORACLE_BLOB: raise RuntimeError( f"{ORACLE_COMMIT}:{ORACLE_PATH} is {blob}, expected {ORACLE_BLOB}" ) data = git_bytes("show", f"{ORACLE_COMMIT}:{ORACLE_PATH}") digest = git_hash_object(data) if digest != ORACLE_BLOB: raise RuntimeError(f"extracted oracle blob is {digest}, expected {ORACLE_BLOB}") return data.decode() def call_surface(node: ast.AST) -> str | None: if not isinstance(node, ast.Call) or len(node.args) < 2: return None surface = node.args[1] if isinstance(surface, ast.Constant) and isinstance(surface.value, str): return surface.value return None def extract() -> list[str]: tree = ast.parse(oracle_text()) commands: list[str] = [] for node in tree.body: if isinstance(node, ast.Assign) and len(node.targets) == 1: target = node.targets[0] value = node.value elif isinstance(node, ast.AnnAssign): target = node.target value = node.value else: continue if not isinstance(target, ast.Name) or target.id != "COMMANDS": continue if not isinstance(value, ast.Dict): raise RuntimeError("oracle COMMANDS must be a dict") for key, item in zip(value.keys, value.values, strict=True): if not isinstance(key, ast.Constant) or not isinstance(key.value, str): continue if call_surface(item) == "access": commands.append(key.value) result = sorted(commands) if not result: raise RuntimeError("access rejection extraction is empty") missing = sorted(SENTINELS - set(result)) if missing: raise RuntimeError(f"missing access-rejection sentinels: {missing}") if len(result) != EXPECTED_COUNT: raise RuntimeError( f"access rejection count {len(result)} != {EXPECTED_COUNT}: {result!r}" ) return result def render(commands: list[str]) -> str: lines = [ "# SPDX-License-Identifier: AGPL-3.0-only", "# Copyright (c) 2026 sol pbc", "# Generated by scripts/build_journal_access_rejection_inventory.py.", "", "JOURNAL_ACCESS_ONLY_COMMANDS: tuple[str, ...] = (", ] lines.extend(f" {json.dumps(command)}," for command in commands) lines.extend([")", ""]) return "\n".join(lines) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Build journal access-only rejection inventory." ) parser.add_argument("--output", type=Path, default=OUTPUT) parser.add_argument("--check", action="store_true") return parser.parse_args() def main() -> int: args = parse_args() output = args.output.resolve() rendered = render(extract()) if args.check: if not output.is_file(): print(f"{output} is missing") return 1 if output.read_text() != rendered: print( f"{output} is stale; run make build-journal-access-rejection-inventory" ) return 1 print(f"{output} is current") return 0 output.parent.mkdir(parents=True, exist_ok=True) output.write_text(rendered) print(f"wrote {output}") return 0 if __name__ == "__main__": sys.exit(main())