#!/usr/bin/env python3 """Run the Nono Slice 2 hermetic suite and explicit security gates.""" from __future__ import annotations import argparse import json import platform import shutil import sys import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from tests.nono.harness.fixtures import isolated_environment # noqa: E402 from tests.nono.harness.nono import HostRequirementError, NonoAdapter # noqa: E402 def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", action="store_true", help="run mandatory Linux/Nono 0.74 checks") parser.add_argument( "--live", action="store_true", help="run separately approved live gate (never implicit)" ) parser.add_argument( "--approve-live", metavar="APPROVER", help="required explicit human approval for --live; absence is an error", ) parser.add_argument("--nono", default=None, help="absolute Nono executable for --host") return parser def run_hermetic() -> int: suite = unittest.defaultTestLoader.discover( str(ROOT / "tests" / "nono"), pattern="test_*.py", top_level_dir=str(ROOT) ) result = unittest.TextTestRunner(verbosity=2).run(suite) return 0 if result.wasSuccessful() else 1 def run_host(executable: str | None) -> int: if platform.system() != "Linux": print("host gate failed: Linux is required", file=sys.stderr) return 2 selected = executable or shutil.which("nono") if selected is None: print("host gate failed: nono executable is missing", file=sys.stderr) return 2 selected_path = Path(selected) if not selected_path.is_absolute(): print("host gate failed: nono executable must resolve to an absolute path", file=sys.stderr) return 2 fixture_root = ROOT / "tests" / "nono" / "fixtures" / "core" minimal_profile = fixture_root / "minimal.json" broad_profile = fixture_root / "broad.json" try: with isolated_environment(prefix="nono-host-") as fixture: session = fixture.session("one") adapter = NonoAdapter( selected_path, cwd=session.repo, timeout=20, environment=session.environment(fixture.env), ) version = adapter.require_host_0_74() validation = adapter.validate_temporary_profile(minimal_profile) if not validation.ok: print( f"host gate failed: strict validation exited {validation.returncode}: " f"{validation.stderr or validation.stdout}", file=sys.stderr, ) return 1 shown = adapter.show_temporary_profile(minimal_profile) if not shown.ok: print("host gate failed: profile show did not succeed", file=sys.stderr) return 1 try: resolved = json.loads(shown.stdout) except json.JSONDecodeError as error: print(f"host gate failed: profile show was not JSON: {error}", file=sys.stderr) return 1 if resolved.get("network", {}).get("block") is not True: print("host gate failed: fixture is not network-blocked", file=sys.stderr) return 1 run = adapter.run_temporary_profile(minimal_profile, ["/usr/bin/true"]) if not run.ok: print( f"host gate failed: isolated execution exited {run.returncode}: " f"{run.stderr or run.stdout}", file=sys.stderr, ) return 1 denied_target = fixture.session("two").tmp / "private.txt" denied_target.write_text("denied fixture\n", encoding="utf-8") probe = [ "/usr/bin/python3", "-c", "from pathlib import Path; Path(__import__('sys').argv[1]).read_text()", str(denied_target), ] denied = adapter.run_temporary_profile(minimal_profile, probe) if denied.ok: print( "host gate failed: minimal profile read another session's temporary file", file=sys.stderr, ) return 1 broad_target = session.repo / "broad-readable.txt" broad_target.write_text("broad fixture\n", encoding="utf-8") broad_probe = [*probe[:-1], str(broad_target)] allowed = adapter.run_temporary_profile( broad_profile, broad_probe, allow_cwd=True, ) if not allowed.ok: print( "host gate failed: broad fixture did not demonstrate the over-grant", file=sys.stderr, ) return 1 print( json.dumps( { "suite": "host", "nonoVersion": version, "status": "passed", "negativeChecks": 1, "overgrantDetectionChecks": 1, } ) ) return 0 except HostRequirementError as error: print(f"host gate failed: {error}", file=sys.stderr) return 2 except (OSError, ValueError) as error: print(f"host gate failed: {error}", file=sys.stderr) return 2 def run_live(approver: str | None) -> int: if not isinstance(approver, str) or not approver.strip(): print("live gate requires --approve-live APPROVER", file=sys.stderr) return 2 # This source harness has no credential broker. A missing manual check is # blocked, never represented as a pass or an implicit unittest skip. print( json.dumps( { "suite": "live", "approver": approver, "status": "blocked", "reason": "manual credentialed evidence is not installed in Slice 2", } ) ) return 1 def main(argv: list[str] | None = None) -> int: args = _parser().parse_args(argv) if args.host and args.live: print("choose exactly one of --host or --live", file=sys.stderr) return 2 if args.approve_live is not None and not args.live: print("--approve-live is valid only with --live", file=sys.stderr) return 2 if args.host: return run_host(args.nono) if args.live: return run_live(args.approve_live) return run_hermetic() if __name__ == "__main__": raise SystemExit(main())