4"""Guard hook-to-Just parity and the immutable pre-commit owner transport."""
6from __future__
import annotations
16from contextlib
import suppress
17from pathlib
import Path
19sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
21from scripts.checks
import hook_parity_mutations
as mutations
22from scripts.checks
import hook_transport_support
as transport
23from scripts.checks.hook_git_policy_selftest
import run_hostile_owner_cases
as _hostile
24from scripts.checks.hook_runtime_selftest
import (
25 default_signal_test_command,
26 run_runtime_selftests,
28from scripts.dev.git_environment
import sanitized_git_environment, trusted_git_executable
30REPO_ROOT = Path(__file__).resolve().parents[2]
31POLICY_ROOT = Path(os.environ.get(
"RA8_HOOK_POLICY_ROOT", REPO_ROOT)).resolve()
32HOOKS_JUST = POLICY_ROOT /
"just" /
"hooks.just"
33PRE_COMMIT = POLICY_ROOT /
"scripts" /
"git" /
"pre-commit"
34PRE_PUSH = POLICY_ROOT /
"scripts" /
"git" /
"pre-push"
35HOOK_LAUNCHER_FILE = POLICY_ROOT /
"scripts" /
"git" /
"hook-launcher"
36HOOK_INSTALLER = POLICY_ROOT /
"scripts" /
"git" /
"install-hooks.sh"
37PROOF_WRITER = POLICY_ROOT /
"scripts" /
"git" /
"write-proof.py"
38CI_SCRIPT = POLICY_ROOT /
"scripts" /
"ci.sh"
39CI_GATES_DIR = POLICY_ROOT /
"scripts" /
"ci" /
"gates"
40ROOT_JUSTFILE = POLICY_ROOT /
"justfile"
41ROOT_CMAKE = POLICY_ROOT /
"CMakeLists.txt"
42RUN_JUST = POLICY_ROOT /
"scripts" /
"dev" /
"run_just.sh"
43CANDIDATE_CHECKER = POLICY_ROOT /
"scripts" /
"checks" /
"check_hook_parity.py"
44MUTATION_HELPER = POLICY_ROOT /
"scripts" /
"checks" /
"hook_parity_mutations.py"
45TRUSTED_CHECKER = Path(__file__).resolve()
46TRUSTED_RUNTIME = REPO_ROOT /
"scripts" /
"checks" /
"hook_runtime_selftest.py"
47TRUSTED_MUTATIONS = REPO_ROOT /
"scripts" /
"checks" /
"hook_parity_mutations.py"
58CANDIDATE_BOUNDARY_MODULES = (
59 "scripts/checks/hook_transport_support.py",
60 "scripts/checks/hook_git_policy_selftest.py",
63EXPECTED_POLICY_FAILURE = 42
86 "check_mcdc_block.py --staged",
87 "check_new_compound_has_mcdc.py --staged",
88 "check_obsolete_standards.py --staged",
91JUST_EXECUTABLE =
'"{{ just_executable() }}"'
92HOOK_LAUNCHER =
"scripts/dev/run_just.sh"
93PRE_COMMIT_SHA256 =
"d5cba09dfbdb9b03f3d94cd3fea59e4ca98c626c6edd85d499171c8b812222d5"
94INSTALLED_LAUNCHER_SHA256 =
"1ad13a9da6b76e6f8449ace4df6a535d2972d1062654899b353ccd1d4a863b08"
95HOOK_INSTALLER_SHA256 =
"18850cb6b3c06c2c1794b6f60103cd9acb584bf7f8745f1c877ff4f838119e86"
96PROOF_WRITER_SHA256 =
"09ec423b2f922c03f83504f92786fe018255ccefc31c0ef7c30bb53bb5ff5406"
100 "resolve_owner_tools",
101 "prepare_private_repository",
102 "write_candidate_tree",
103 "checkout_candidate_tree",
104 "verify_candidate_tree",
105 "prepare_head_control_plane",
107 "run_head_validator",
108 "run_snapshot_policy",
110 "verify_completion_proof",
111 "verify_source_unchanged",
115class ParityError(RuntimeError):
116 """A hook-parity self-test found a safety regression."""
119def _fail(message: str) ->
None:
120 """Raise one self-test failure without embedding messages in exceptions."""
121 raise ParityError(message)
124def _recipe(text: str, name: str) -> str:
125 """Return one top-level Just recipe, including its indented body."""
126 lines = text.splitlines()
127 start = next((i
for i, line
in enumerate(lines)
if line.startswith(f
"{name}")), -1)
128 if start < 0
or not lines[start].endswith(
":"):
131 while end < len(lines):
133 if line
and not line.startswith((
" ",
"\t"))
and not line.startswith(
"#"):
136 return "\n".join(lines[start:end])
139def _active_lines(recipe: str) -> tuple[str, ...]:
140 """Return non-comment shell lines from a recipe."""
143 for line
in recipe.splitlines()[1:]
144 if line.strip()
and not line.lstrip().startswith(
"#")
148def _digest(path: Path) -> str:
149 """Return the SHA-256 digest of one exact policy surface."""
150 return hashlib.sha256(path.read_bytes()).hexdigest()
153def _check_candidate_control_plane(
154 checker: bytes, runtime: bytes, mutation_helper: bytes, run_just: str
156 """Reject candidate attempts to replace the immutable validator boundary."""
157 failures: list[str] = []
158 if checker != TRUSTED_CHECKER.read_bytes():
159 failures.append(
"candidate hook validator differs from immutable HEAD")
160 if runtime != TRUSTED_RUNTIME.read_bytes():
161 failures.append(
"candidate hook runtime validator differs from immutable HEAD")
162 if mutation_helper != TRUSTED_MUTATIONS.read_bytes():
163 failures.append(
"candidate hook mutation helper differs from immutable HEAD")
164 for relative
in CANDIDATE_BOUNDARY_MODULES:
165 candidate = POLICY_ROOT / relative
166 if not candidate.is_file():
167 failures.append(f
"candidate {relative} is absent from the validator boundary")
168 elif candidate.read_bytes() != (REPO_ROOT / relative).read_bytes():
169 failures.append(f
"candidate {relative} differs from immutable HEAD")
170 proof_names = (
"RA8_STAGED_HOOK_PROOF",
"RA8_STAGED_GATE_PROOF")
171 if any(name
in run_just
for name
in proof_names):
172 failures.append(
"candidate run_just.sh branches on an owner proof capability")
176def _check_installation_surfaces() -> list[str]:
177 """Pin the stable common-dir installer, launcher, and proof helper."""
178 failures: list[str] = []
180 _check_candidate_control_plane(
181 CANDIDATE_CHECKER.read_bytes(),
182 (POLICY_ROOT /
"scripts/checks/hook_runtime_selftest.py").read_bytes(),
183 MUTATION_HELPER.read_bytes(),
184 RUN_JUST.read_text(encoding=
"utf-8"),
188 (HOOK_LAUNCHER_FILE, INSTALLED_LAUNCHER_SHA256,
"installed launcher"),
189 (HOOK_INSTALLER, HOOK_INSTALLER_SHA256,
"hook installer"),
190 (PROOF_WRITER, PROOF_WRITER_SHA256,
"atomic proof writer"),
193 f
"{label} differs from its exact audited digest"
194 for path, expected, label
in exact
195 if _digest(path) != expected
197 launcher = HOOK_LAUNCHER_FILE.read_text(encoding=
"utf-8")
198 installer = HOOK_INSTALLER.read_text(encoding=
"utf-8")
199 root_just = ROOT_JUSTFILE.read_text(encoding=
"utf-8")
200 cmake = ROOT_CMAKE.read_text(encoding=
"utf-8")
201 required_launcher = (
203 "PATH=/usr/bin:/bin:/usr/sbin:/sbin",
204 "system_git=/usr/bin/git",
206 '"$system_git" -C "$root" cat-file blob "$blob"',
207 '"$system_git" -C "$root" hash-object --no-filters "$owner"',
210 "exec env -u BASH_ENV -u ENV -u PYTHONHOME -u PYTHONPATH",
211 '"$bash_bin" -p "$owner" "${hook_args[@]}"',
213 if any(token
not in launcher
for token
in required_launcher):
214 failures.append(
"installed launcher lost HEAD, argv, or signal ownership")
215 required_installer = (
216 "PATH=/usr/bin:/bin:/usr/sbin:/sbin",
217 "TRUSTED_GIT=/usr/bin/git",
219 'cat-file blob "$blob"',
220 "refusing unmanaged",
222 if any(token
not in installer
for token
in required_installer):
223 failures.append(
"hook installer lost common-dir or unmanaged-path safety")
224 if root_just.count(
"/bin/bash -p scripts/git/install-hooks.sh") != 1:
225 failures.append(
"root Justfile must expose exactly one explicit hook installer")
226 if "core.hooksPath" in cmake
or "install-hooks.sh" in cmake:
227 failures.append(
"CMake must not mutate Git hook configuration")
231def _check_wrappers(pre_commit: str, pre_push: str) -> list[str]:
232 """Check that executable hook files remain transport-only wrappers."""
233 failures: list[str] = []
234 push_command = pre_push.replace(
"\\\n",
" ")
235 digest = hashlib.sha256(pre_commit.encode(
"utf-8")).hexdigest()
236 if digest != PRE_COMMIT_SHA256:
237 failures.append(
"pre-commit owner hook differs from its exact audited digest")
238 if '"$root/scripts/ci.sh" --staged-hook' in pre_commit:
239 failures.append(
"pre-commit still enters live ci.sh before snapshotting")
240 main_start = pre_commit.rfind(
"main() {")
241 main_body = pre_commit[main_start:]
if main_start >= 0
else ""
242 positions = tuple(main_body.find(name)
for name
in BOOTSTRAP_ORDER)
243 if -1
in positions
or positions != tuple(sorted(positions)):
244 failures.append(
"pre-commit owner hook lost snapshot-first execution order")
245 active = _active_lines(
"owner:\n" + pre_commit)
246 if any(line.startswith((
"source ",
". "))
for line
in active)
or "scripts/ci/lib" in pre_commit:
247 failures.append(
"pre-commit owner hook imports live repository control code")
248 if HOOK_LAUNCHER
not in push_command
or 'git_hooks::pre-push "$@"' not in push_command:
249 failures.append(
"pre-push wrapper does not forward argv to git_hooks::pre-push")
250 if pre_push.count(HOOK_LAUNCHER) != 1:
251 failures.append(
"pre-push wrapper contains policy beyond one Just dispatch")
252 for label, command
in ((
"pre-push", push_command),):
254 '--justfile "$root/justfile"' not in command
255 or '--working-directory "$root"' not in command
257 failures.append(f
"{label} wrapper does not anchor the launcher at the repository root")
261def _check_pre_commit_flow(recipe: str, active: tuple[str, ...]) -> list[str]:
262 """Check candidate dispatch after immutable HEAD validation."""
263 failures: list[str] = []
264 forbidden = (
"RA8_STAGED_HOOK_PROOF",
"RA8_STAGED_GATE_PROOF",
"write-proof.py")
265 if any(token
in recipe
for token
in forbidden):
266 failures.append(
"candidate pre-commit policy can access owner proof capability")
267 if 'local gate="$1"' not in recipe:
268 failures.append(
"pre-commit gate declaration is not isolated")
269 if f
'{JUST_EXECUTABLE} quality::local::gate "$gate"' not in recipe:
270 failures.append(
"pre-commit lost its direct registered-gate dispatch")
271 if any(line ==
"exit 0" for line
in active):
272 failures.append(
"pre-commit contains an early-success exit")
276def _check_pre_commit(hooks: str) -> list[str]:
277 """Check staged semantics and the base hook's still-valid gate coverage."""
278 failures: list[str] = []
279 recipe = _recipe(hooks,
"pre-commit")
280 active = _active_lines(recipe)
282 return [
"hooks.just has no pre-commit recipe"]
283 if recipe.count(
"#!/bin/bash -p") != 1:
284 failures.append(
"pre-commit recipe lost its exact privileged Bash owner")
285 snapshot_requirements = (
286 '[[ "${RA8_STAGED_HOOK_SNAPSHOT:-0}" == "1" ]]',
287 "git diff --quiet --no-ext-diff",
288 "git ls-files --others --exclude-standard",
291 f
"pre-commit lost staged-snapshot assertion: {requirement}"
292 for requirement
in snapshot_requirements
293 if requirement
not in recipe
295 gate_positions = tuple(recipe.find(f
"\n {gate}\n")
for gate
in PRE_COMMIT_GATES)
296 if -1
in gate_positions:
297 failures.append(
"pre-commit lost a registered gate")
298 elif gate_positions != tuple(sorted(gate_positions)):
299 failures.append(
"pre-commit registered gates were reordered")
300 loop =
' for gate in "${gates[@]}"; do\n run_gate "$gate"\n done'
301 if loop
not in recipe:
302 failures.append(
"pre-commit gate loop is dead, wrapped, or reordered")
303 if f
'{JUST_EXECUTABLE} quality::local::gate "$gate"' not in recipe:
304 failures.append(
"pre-commit lost registered gate dispatch")
306 f
"pre-commit lost index-sensitive check: {check}"
307 for check
in STAGED_CHECKS
308 if check
not in recipe
310 if "git diff --cached --name-only --diff-filter=ACMR -z" not in recipe:
311 failures.append(
"pre-commit C trigger is not derived from the staged index")
313 f
"pre-commit lost staged-C {gate} trigger"
314 for gate
in (
"tidy",
"cppcheck")
315 if f
"run_gate {gate}" not in recipe
317 failures.extend(_check_pre_commit_flow(recipe, active))
320 f
"{JUST_EXECUTABLE} ci",
322 "checks::devcontainer",
324 if any(any(token
in line
for token
in prohibited)
for line
in active):
325 failures.append(
"pre-commit invokes full/push-only CI")
326 if any(line.startswith(
"just ")
for line
in active):
327 failures.append(
"pre-commit uses PATH lookup instead of just_executable()")
334BOOTSTRAP_REQUIREMENTS = (
335 'SOURCE_INDEX="$(active_index_path "$SOURCE_ROOT")"',
336 "install_strict_git_environment",
337 "validate_inherited_alternates",
338 "init_private_repository",
339 "GIT_CONFIG_GLOBAL=/dev/null",
340 "GIT_CONFIG_SYSTEM=/dev/null",
341 "GIT_CONFIG_KEY_0=core.hooksPath",
342 "GIT_CONFIG_KEY_1=core.fsmonitor",
343 "GIT_CONFIG_KEY_2=core.attributesFile",
344 'GIT_INDEX_FILE="$COPIED_INDEX"',
345 'GIT_OBJECT_DIRECTORY="$CAPTURE_OBJECTS"',
346 "start_new_session=True",
347 "preexec_fn=reset_child_signals",
349 "resolve_owner_tools",
354 "OWNER_PYTHON=/usr/bin/python3",
355 "OWNER_BASH=/bin/bash",
356 '"$SOURCE_ROOT/"*) die "owner Just resolves through the mutable source tree" ;;',
357 '[[ "$OWNER_PYTHON" == /* && "$OWNER_BASH" == /* && "$OWNER_JUST" == /* ]]',
358 "RA8_OWNER_PATH=/usr/bin:/bin:/usr/sbin:/sbin",
359 'PATH="$RA8_OWNER_PATH"',
360 '"$account_home/.local/bin/just"',
362 "prepare_head_control_plane",
363 "run_head_validator",
364 "verify_bootstrap_policy_population",
365 "head_supports_attribute_validation",
366 'RA8_HOOK_POLICY_ROOT="$SNAPSHOT_DIR"',
368 '--shell "$OWNER_BASH" --clear-shell-args --shell-arg -puc',
369 "activate_owner_signal_forwarding",
373 'OLDPWD="$SNAPSHOT_DIR"',
374 "verify_source_unchanged",
377 "verify_completion_proof",
381def _check_snapshot_dispatch(ci_script: str, pre_commit: str) -> list[str]:
382 """Pin immutable validation before trusted direct candidate dispatch."""
383 failures: list[str] = []
384 bootstrap_requirements = BOOTSTRAP_REQUIREMENTS
386 f
"pre-commit bootstrap lost exact behavior: {token}"
387 for token
in bootstrap_requirements
388 if token
not in pre_commit
390 if "--staged-hook" in ci_script
or "selftest-staged-runner" in ci_script:
391 failures.append(
"ci.sh retains an alternate live staged-hook front door")
392 if "set -euo pipefail\nexit 0\n" in ci_script:
393 failures.append(
"ci.sh contains an early-success exit before gate dispatch")
394 if ci_script.count(
'run_gate_capture "$gate"') != 1:
395 failures.append(
"ci.sh lost its single registered-gate dispatch")
396 if any(token
in ci_script
for token
in (
"RA8_STAGED_GATE_PROOF",
"write_staged_gate_proof")):
397 failures.append(
"candidate ci.sh retains a forgeable proof capability")
401def _check_pre_push(hooks: str) -> list[str]:
402 """Check LFS, pushed-commit policy, and the one full-CI invocation."""
403 failures: list[str] = []
404 recipe = _recipe(hooks,
"pre-push remote url")
406 return [
"hooks.just has no pre-push recipe"]
409 "/bin/bash -p scripts/git/commit-msg --selftest",
411 '/bin/bash -p scripts/git/commit-msg "$message"',
414 "--working-directory",
416 '[[ "$ci_rc" -eq 3 ]]',
419 f
"pre-push lost required behavior: {token}" for token
in required
if token
not in recipe
421 if recipe.count(
"#!/bin/bash -p") != 1:
422 failures.append(
"pre-push recipe lost its exact privileged Bash owner")
423 if "checks::devcontainer" in recipe
or "quality::fast" in recipe:
424 failures.append(
"pre-push runs a partial suite instead of root `just ci`")
425 if "mapfile" in recipe
or "readarray" in recipe:
426 failures.append(
"pre-push uses an array builtin absent from macOS Bash 3.2")
428 line
for line
in _active_lines(recipe)
if line.startswith(f
"{JUST_EXECUTABLE} --justfile")
430 if len(ci_command) != 1
or " ci" not in recipe:
431 failures.append(
"pre-push must invoke root `just ci` exactly once")
432 if any(line.startswith(
"just ")
for line
in _active_lines(recipe)):
433 failures.append(
"pre-push uses PATH lookup instead of just_executable()")
437def _source_reaches_runtime_proof(text: str) -> bool:
438 """Let Bash parse/source a gate fragment and require post-source proof."""
439 payload = f
"{text}\nprintf 'RA8-SOURCE-PROOF\\n'\n"
440 command =
"set -euo pipefail; source /dev/stdin"
441 result = subprocess.run(
442 [
"/bin/bash",
"--noprofile",
"--norc",
"-p",
"-c", command],
448 return result.returncode == 0
and result.stdout.endswith(
"RA8-SOURCE-PROOF\n")
451def _check_gate_sources(gate_sources: tuple[str, ...]) -> list[str]:
452 """Runtime-prove every sourced gate fragment returns to ci.sh dispatch."""
453 failures: list[str] = []
454 for number, text
in enumerate(gate_sources, start=1):
455 if not _source_reaches_runtime_proof(text):
456 failures.append(f
"gate source {number} bypasses its post-source runtime proof")
465 gate_sources: tuple[str, ...],
467 """Return every hook parity failure found in the supplied texts."""
468 failures = _check_installation_surfaces()
469 failures.extend(_check_wrappers(pre_commit, pre_push))
470 failures.extend(_check_snapshot_dispatch(ci_script, pre_commit))
471 failures.extend(_check_gate_sources(gate_sources))
472 if 'set working-directory := ".."' not in hooks:
473 failures.append(
"hooks.just does not anchor recipes at the repository root")
474 failures.extend(_check_pre_commit(hooks))
475 failures.extend(_check_pre_push(hooks))
479def _live_texts() -> tuple[str, str, str, str, tuple[str, ...]]:
480 """Read every executable hook-policy surface."""
481 gate_sources = tuple(
482 path.read_text(encoding=
"utf-8")
for path
in sorted(CI_GATES_DIR.glob(
"*.sh"))
485 PRE_COMMIT.read_text(encoding=
"utf-8"),
486 PRE_PUSH.read_text(encoding=
"utf-8"),
487 HOOKS_JUST.read_text(encoding=
"utf-8"),
488 CI_SCRIPT.read_text(encoding=
"utf-8"),
493def _structural_selftest(texts: tuple[str, str, str, str, tuple[str, ...]]) ->
None:
494 """Prove all named control-flow and nonce regressions are rejected."""
495 pre_commit, pre_push, hooks, ci_script, gate_sources = texts
497 _fail(
"live hook policy was rejected by its own baseline")
498 for number, case
in enumerate(
499 mutations.mutation_cases(pre_commit, hooks, ci_script, gate_sources), start=1
501 mutated_pre_commit, mutated_hooks, mutated_ci, mutated_gates = case
502 if not validate(mutated_pre_commit, pre_push, mutated_hooks, mutated_ci, mutated_gates):
503 _fail(f
"control-flow mutation {number} escaped")
509 "if true; then exit 0; fi\n",
510 "if true; then return 0; fi\n",
513 "# exit 0\ngate_fixture() { :; }\n",
514 "( exit 0 )\ngate_fixture() { :; }\n",
515 "( return 0 )\ngate_fixture() { :; }\n",
517 if any(
not _check_gate_sources((source,))
for source
in rejected_sources):
518 _fail(
"active sourced-gate exit mutation escaped Bash runtime proof")
519 if any(_check_gate_sources((source,))
for source
in accepted_sources):
520 _fail(
"comment or non-bypassing subshell was misclassified as active exit")
521 checker = TRUSTED_CHECKER.read_bytes()
522 runtime = TRUSTED_RUNTIME.read_bytes()
523 mutation_helper = TRUSTED_MUTATIONS.read_bytes()
524 run_just = RUN_JUST.read_text(encoding=
"utf-8")
525 control_mutations = (
526 (checker + b
"\n# candidate mutation\n", runtime, mutation_helper, run_just),
527 (checker, runtime + b
"\n# candidate mutation\n", mutation_helper, run_just),
528 (checker, runtime, mutation_helper + b
"\n# candidate mutation\n", run_just),
533 "if [[ -n ${RA8_STAGED_HOOK_PROOF-} ]]; then exit 0; fi\n",
536 if any(
not _check_candidate_control_plane(*mutation)
for mutation
in control_mutations):
537 _fail(
"candidate validator or run_just proof mutation escaped")
543 env: dict[str, str] |
None =
None,
544 input_data: bytes |
None =
None,
546 """Run one checked Git command in an isolated synthetic repository."""
547 proc = subprocess.run(
548 [trusted_git_executable(),
"-C", str(root), *args],
549 env=sanitized_git_environment()
if env
is None else env,
555 _fail(proc.stderr.decode(errors=
"replace").strip())
559def _write_fixture_file(path: Path, text: str, *, executable: bool =
False) ->
None:
560 """Write one synthetic fixture file and optionally make it executable."""
561 path.parent.mkdir(parents=
True, exist_ok=
True)
562 path.write_text(text, encoding=
"utf-8")
567POLICY_FIXTURE_FILES = (
568 "CMakeLists.txt justfile just/hooks.just scripts/checks/check_hook_parity.py "
569 "scripts/checks/hook_git_policy_selftest.py scripts/checks/hook_runtime_selftest.py "
570 "scripts/checks/hook_parity_mutations.py "
571 "scripts/checks/hook_transport_support.py "
572 "scripts/ci.sh scripts/dev/git_environment.py "
573 "scripts/dev/run_just.sh scripts/git/hook-launcher scripts/git/install-hooks.sh "
574 "scripts/git/pre-commit scripts/git/pre-push scripts/git/write-proof.py"
578def _make_transport_fixture(root: Path, staged: str, worktree: str) ->
None:
579 """Create a candidate index whose policy mode differs from its worktree."""
580 _git(root,
"init",
"--quiet")
581 _git(root,
"config",
"user.email",
"selftest@invalid")
582 _git(root,
"config",
"user.name",
"selftest")
583 for relative
in POLICY_FIXTURE_FILES:
584 source = REPO_ROOT / relative
585 destination = root / relative
586 destination.parent.mkdir(parents=
True, exist_ok=
True)
587 shutil.copy2(source, destination)
588 for source
in sorted((REPO_ROOT /
"scripts/ci/gates").glob(
"*.sh")):
589 destination = root /
"scripts/ci/gates" / source.name
590 destination.parent.mkdir(parents=
True, exist_ok=
True)
591 shutil.copy2(source, destination)
592 transport.write_transport_justfiles(root)
593 _write_fixture_file(root /
"policy-mode",
"success\n")
594 _write_fixture_file(root /
".gitignore",
".venv/\nignored-dir/*\n")
595 for index
in range(6):
596 _write_fixture_file(root / f
"policy-attributes/{index}/.gitattributes",
"* text\n")
597 for index
in range(26):
598 _write_fixture_file(root / f
"policy-ignores/{index}/.gitignore",
"scratch\n")
599 _write_fixture_file(root /
"delete-me",
"delete\n")
600 _write_fixture_file(root /
"resurrect-me",
"original\n")
601 _write_fixture_file(root /
"mode.sh",
"#!/usr/bin/env bash\n")
602 _write_fixture_file(root /
"link-target",
"target\n")
603 _write_fixture_file(root /
"conflict.txt",
"base\n")
604 _write_fixture_file(root /
"ignored-dir/tracked.txt",
"tracked despite ignore\n")
605 _git(root,
"add",
".")
606 _git(root,
"add",
"-f",
"ignored-dir/tracked.txt")
607 _git(root,
"commit",
"--quiet",
"-m",
"fixture")
608 _write_fixture_file(root /
"policy-mode", f
"{staged}\n")
609 _git(root,
"add",
"policy-mode")
610 _write_fixture_file(root /
"policy-mode", f
"{worktree}\n")
613def _source_state(root: Path, index: Path |
None =
None) -> tuple[str, tuple[tuple[str, str], ...]]:
614 """Hash the source index and every loose/packed object-store file."""
615 index_path = index
or (root /
".git/index")
616 index_digest = hashlib.sha256(index_path.read_bytes()).hexdigest()
617 objects = root /
".git/objects"
618 object_state = tuple(
619 (path.relative_to(objects).as_posix(), hashlib.sha256(path.read_bytes()).hexdigest())
620 for path
in sorted(objects.rglob(
"*"))
623 return index_digest, object_state
627 root: Path, temp_root: Path, extra_env: dict[str, str] |
None =
None
628) -> subprocess.CompletedProcess[str]:
629 """Run the audited owner hook against one synthetic active index."""
630 environment = os.environ.copy()
if extra_env
is None else extra_env.copy()
631 environment[
"TMPDIR"] = str(temp_root)
632 return subprocess.run(
633 [
"/bin/bash",
"-p", str(PRE_COMMIT)],
643def _transport_case(base: Path, name: str, staged: str, worktree: str, expected: int) ->
None:
644 """Prove the hook rules only on candidate-index policy bytes."""
646 temp_root = base / f
"{name}-tmp"
649 _make_transport_fixture(root, staged, worktree)
650 marker = base / f
"{name}.venv"
651 transport.install_venv_wrappers(root, marker)
652 before = _source_state(root)
653 environment = transport.transport_environment(base, root)
654 inherited = environment[
"PATH"]
656 PATH=f
"{root / '.venv/bin'}:{inherited}",
657 RA8_SELFTEST_VENV=str(marker),
664 if result.returncode != expected:
665 _fail(f
"{name}: expected {expected}, got {result.returncode}: {result.stderr}")
666 if tuple(temp_root.iterdir()):
667 _fail(f
"{name}: snapshot residue remained")
668 if _source_state(root) != before:
669 _fail(f
"{name}: source index or object store changed")
671 _fail(f
"{name}: an ignored source .venv wrapper became a trusted owner tool")
674def _wait_for_path(path: Path, process: subprocess.Popen[str]) ->
None:
675 """Wait briefly for the staged fixture child to report readiness."""
676 deadline = time.monotonic() + 10
677 while time.monotonic() < deadline:
680 if process.poll()
is not None:
681 _fail(f
"signal fixture exited before ready: {process.returncode}")
683 _fail(
"signal fixture did not become ready")
686def _kill_ready_group(ready: Path) ->
None:
687 """Kill one synthetic policy group recorded by its supervisor."""
689 pgid = int(ready.read_text(encoding=
"ascii").strip())
690 os.killpg(pgid, signal.SIGKILL)
691 except (OSError, ValueError):
695def _force_fixture_cleanup(process: subprocess.Popen[str], temp_root: Path) ->
None:
696 """Kill both synthetic owner and policy groups after a test timeout."""
697 for ready
in tuple(temp_root.rglob(
"policy-ready")):
698 _kill_ready_group(ready)
699 if process.poll()
is None:
700 with suppress(ProcessLookupError):
701 os.killpg(process.pid, signal.SIGKILL)
702 with suppress(subprocess.TimeoutExpired):
703 process.wait(timeout=5)
706def _signal_case(base: Path, sig: signal.Signals) ->
None:
707 """Signal only the owner PID and prove its policy group is reaped."""
708 root = base / f
"signal-{sig.name.lower()}"
709 temp_root = base / f
"signal-{sig.name.lower()}-tmp"
710 ready = base / f
"{sig.name}.ready"
711 continued = base / f
"{sig.name}.continued"
714 _make_transport_fixture(root,
"hang",
"success")
715 environment = transport.transport_environment(base, root)
717 RA8_SELFTEST_VENV=str(base /
"signal.venv"),
718 TMPDIR=str(temp_root),
719 RA8_SELFTEST_READY=str(ready),
720 RA8_SELFTEST_CONTINUED=str(continued),
722 process = subprocess.Popen(
723 default_signal_test_command(
"/bin/bash",
"-p", str(PRE_COMMIT)),
726 stdout=subprocess.PIPE,
727 stderr=subprocess.PIPE,
729 start_new_session=
True,
732 _wait_for_path(ready, process)
733 os.kill(process.pid, sig)
734 _stdout, stderr = process.communicate(
738 _force_fixture_cleanup(process, temp_root)
739 if process.returncode != ABORTED
or "ABORTED" not in stderr:
740 _fail(f
"{sig.name}: owner did not report UNKNOWN: {process.returncode}")
741 if continued.exists()
or tuple(temp_root.iterdir()):
742 _fail(f
"{sig.name}: child continued or snapshot residue remained")
745def _shape_case(base: Path) ->
None:
746 """Prove candidate add/delete/mode/link/ignore semantics and spaces."""
747 root, temp_root = base /
"shape", base /
"shape-tmp"
750 _make_transport_fixture(root,
"inspect",
"failure")
751 _write_fixture_file(root /
"path with spaces/added.txt",
"added\n")
752 _git(root,
"add",
"path with spaces/added.txt")
753 _git(root,
"rm",
"delete-me",
"resurrect-me")
754 _write_fixture_file(root /
"resurrect-me",
"worktree resurrection\n")
755 (root /
"mode.sh").chmod(0o755)
756 _git(root,
"add",
"mode.sh")
757 (root /
"alias").symlink_to(
"link-target")
758 _git(root,
"add",
"alias")
759 _write_fixture_file(root /
"untracked.txt",
"exclude me\n")
760 before = _source_state(root)
761 environment = transport.transport_environment(base, root)
762 environment[
"RA8_SELFTEST_VENV"] = str(base /
"shape.venv")
763 result = _run_owner(root, temp_root, environment)
764 if result.returncode
or _source_state(root) != before
or tuple(temp_root.iterdir()):
765 _fail(f
"candidate-shape fidelity failed: {result.returncode}: {result.stderr}")
768def _custom_index_case(base: Path) ->
None:
769 """Prove hostile hook routing selects an inherited index with spaces."""
770 root, temp_root = base /
"custom-index", base /
"custom-index-tmp"
773 _make_transport_fixture(root,
"failure",
"success")
774 custom = root /
".git/custom index"
775 shutil.copy2(root /
".git/index", custom)
776 _git(root,
"reset",
"--mixed",
"HEAD")
777 before_custom = _source_state(root, custom)
778 before_default = _source_state(root)
780 "GIT_DIR": str(root /
".git"),
781 "GIT_WORK_TREE": str(root),
782 "GIT_INDEX_FILE":
".git/custom index",
783 "GIT_PREFIX":
"hostile/",
785 environment.update(transport.transport_environment(base, root))
786 environment[
"GIT_INDEX_FILE"] =
".git/custom index"
787 environment[
"RA8_SELFTEST_VENV"] = str(base /
"custom.venv")
788 result = _run_owner(root, temp_root, environment)
789 if result.returncode != EXPECTED_POLICY_FAILURE:
790 _fail(f
"custom index was not authoritative: {result.returncode}: {result.stderr}")
791 if _source_state(root, custom) != before_custom
or _source_state(root) != before_default:
792 _fail(
"custom/default index or source objects changed")
795def _conflicted_index_case(base: Path) ->
None:
796 """Prove an unmerged active index fails closed without residue."""
797 root, temp_root = base /
"conflict", base /
"conflict-tmp"
800 _make_transport_fixture(root,
"success",
"success")
801 base_blob = os.fsdecode(_git(root,
"rev-parse",
"HEAD:conflict.txt")).strip()
802 _write_fixture_file(root /
"ours",
"ours\n")
803 _write_fixture_file(root /
"theirs",
"theirs\n")
804 ours = os.fsdecode(_git(root,
"hash-object",
"-w",
"ours")).strip()
805 theirs = os.fsdecode(_git(root,
"hash-object",
"-w",
"theirs")).strip()
807 f
"100644 {base_blob} 1\tconflict.txt\n"
808 f
"100644 {ours} 2\tconflict.txt\n"
809 f
"100644 {theirs} 3\tconflict.txt\n"
811 _git(root,
"update-index",
"--index-info", input_data=index_info.encode(
"ascii"))
812 before = _source_state(root)
813 environment = transport.transport_environment(base, root)
814 environment[
"RA8_SELFTEST_VENV"] = str(base /
"conflict.venv")
815 result = _run_owner(root, temp_root, environment)
816 if result.returncode == 0
or _source_state(root) != before
or tuple(temp_root.iterdir()):
817 _fail(
"conflicted active index did not fail closed")
820def _transport_selftest() -> None:
821 """Exercise staged-vs-worktree selection, exact shape, and signals."""
822 with tempfile.TemporaryDirectory(prefix=
"ra8-hook-parity-")
as temporary:
823 base = Path(temporary)
824 _transport_case(base,
"staged-wins",
"success",
"failure", 0)
825 _transport_case(base,
"failure-wins",
"failure",
"success", 42)
827 _custom_index_case(base)
828 _conflicted_index_case(base)
829 transport.run_bootstrap_validator_case(
832 _make_transport_fixture,
842 _make_transport_fixture,
846 transport.transport_environment,
849 _signal_case(base, signal.SIGTERM)
850 _signal_case(base, signal.SIGINT)
853def candidate_selftest() -> int:
854 """Run immutable structural mutation proofs against one candidate root."""
856 _structural_selftest(_live_texts())
857 except (OSError, ParityError, subprocess.TimeoutExpired)
as exc:
858 print(f
"check_hook_parity.py: candidate selftest failed: {exc}", file=sys.stderr)
860 print(
"check_hook_parity.py: candidate selftest passed")
864def selftest() -> int:
865 """Prove structural guards and the owner transport against regressions."""
867 _structural_selftest(_live_texts())
868 _transport_selftest()
869 run_runtime_selftests()
870 except (OSError, ParityError, subprocess.TimeoutExpired)
as exc:
871 print(f
"check_hook_parity.py: selftest failed: {exc}", file=sys.stderr)
873 print(
"check_hook_parity.py: selftest passed")
878 """Run the self-test or validate the live hook files."""
879 if sys.argv[1:] == [
"--selftest"]:
881 if sys.argv[1:] == [
"--candidate-selftest"]:
882 return candidate_selftest()
884 print(
"usage: check_hook_parity.py [--selftest|--candidate-selftest]", file=sys.stderr)
886 failures = validate(*_live_texts())
887 for failure
in failures:
888 print(f
"check_hook_parity.py: {failure}", file=sys.stderr)
891 print(
"check_hook_parity.py: hook wrappers and Just policy are in parity")
895if __name__ ==
"__main__":
896 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.