From 828c18f8f98277ab6c8c712c1f4faaf781326d86 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 11 Oct 2026 23:28:36 +0200 Subject: [PATCH] Install generated-commit guard in existing shared hooks without replacing owner hooks --- scripts/install_shared_generated_guard.py | 59 +++++++++++++++++++ tests/test_shared_generated_guard.py | 55 +++++++++++++++++ ...UST-WP-0079-deterministic-security-flow.md | 7 +++ 3 files changed, 121 insertions(+) create mode 100644 scripts/install_shared_generated_guard.py create mode 100644 tests/test_shared_generated_guard.py diff --git a/scripts/install_shared_generated_guard.py b/scripts/install_shared_generated_guard.py new file mode 100644 index 00000000..dd106744 --- /dev/null +++ b/scripts/install_shared_generated_guard.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Add the generated-commit guard to an existing shared hook directory. + +Never changes core.hooksPath, other hooks, or a pre-existing different guard. +The repo argument discovers the already-configured hook path; it is not a new +fleet routing or authority configuration. +""" +import argparse +import os +from pathlib import Path +import shlex +import subprocess + + +CHECKER = Path(__file__).with_name("check_generated_only_commit.py").resolve() + + +def install(repo, execute=False): + repo = Path(repo).resolve() + configured = subprocess.check_output( + ["git", "-C", str(repo), "config", "--get", "core.hooksPath"], text=True).strip() + directory = Path(configured) + if not directory.is_absolute(): + directory = repo / directory + if not directory.is_dir() or directory.is_symlink(): + raise ValueError("existing hook directory must be real and present") + target = directory / "pre-commit" + content = ("#!/bin/sh\n# Custodian shared generated-commit guard; preserve other hooks.\n" + "set -eu\nexec python3 " + shlex.quote(str(CHECKER)) + " --staged\n") + if target.is_symlink(): + raise ValueError("refuse redirected existing pre-commit hook") + if target.exists(): + if target.read_text() != content: + raise ValueError("existing pre-commit hook differs; owner integration required") + return {"repo": str(repo), "hook": str(target), "status": "installed", "changed": False} + if execute: + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o755) + with os.fdopen(fd, "w") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + return {"repo": str(repo), "hook": str(target), + "status": "installed" if execute else "missing", "changed": execute} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=Path, required=True) + parser.add_argument("--install", action="store_true") + args = parser.parse_args() + import json + try: + print(json.dumps(install(args.repo, args.install), sort_keys=True)) + except (ValueError, OSError, subprocess.SubprocessError): + parser.exit(1, "Shared hook unverified; existing files and routing are not replaced.\n") + + +if __name__ == "__main__": + main() diff --git a/tests/test_shared_generated_guard.py b/tests/test_shared_generated_guard.py new file mode 100644 index 00000000..c8a284ef --- /dev/null +++ b/tests/test_shared_generated_guard.py @@ -0,0 +1,55 @@ +import importlib.util +from pathlib import Path +import subprocess + +import pytest + + +SCRIPT = Path(__file__).parents[1] / "scripts/install_shared_generated_guard.py" +spec = importlib.util.spec_from_file_location("shared_guard", SCRIPT) +guard = importlib.util.module_from_spec(spec) +spec.loader.exec_module(guard) + + +def repository(tmp_path): + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", str(repo)], check=True, capture_output=True) + hooks = tmp_path / "shared-hooks" + hooks.mkdir() + subprocess.run(["git", "-C", str(repo), "config", "core.hooksPath", str(hooks)], check=True) + return repo, hooks + + +def test_real_shared_hook_rejects_generated_only_and_retains_other_hooks(tmp_path): + repo, hooks = repository(tmp_path) + other = hooks / "prepare-commit-msg" + other.write_text("# existing owner hook\n") + before = other.read_bytes() + assert guard.install(repo)["status"] == "missing" + assert not (hooks / "pre-commit").exists() + assert guard.install(repo, True)["changed"] + assert not guard.install(repo, True)["changed"] + (repo / "WORK-RECORDS.md").write_text("derived\n") + subprocess.run(["git", "-C", str(repo), "add", "WORK-RECORDS.md"], check=True) + assert subprocess.run([str(hooks / "pre-commit")], cwd=repo, capture_output=True).returncode == 1 + (repo / "implementation.py").write_text("print('source')\n") + subprocess.run(["git", "-C", str(repo), "add", "implementation.py"], check=True) + assert subprocess.run([str(hooks / "pre-commit")], cwd=repo, capture_output=True).returncode == 0 + assert other.read_bytes() == before + + +def test_foreign_or_redirected_guard_is_never_replaced(tmp_path): + repo, hooks = repository(tmp_path) + hook = hooks / "pre-commit" + hook.write_text("owner implementation\n") + with pytest.raises(ValueError): + guard.install(repo, True) + assert hook.read_text() == "owner implementation\n" + hook.unlink() + private = tmp_path / "unrelated" + private.write_text("retained\n") + hook.symlink_to(private) + with pytest.raises(ValueError): + guard.install(repo, True) + assert private.read_text() == "retained\n" diff --git a/workplans/CUST-WP-0079-deterministic-security-flow.md b/workplans/CUST-WP-0079-deterministic-security-flow.md index cd17dd76..5df3e539 100644 --- a/workplans/CUST-WP-0079-deterministic-security-flow.md +++ b/workplans/CUST-WP-0079-deterministic-security-flow.md @@ -140,6 +140,13 @@ tests prove that owner completion admits the successor and that an unknown predecessor or stale progress label cannot produce readiness. This is queue qualification, not evidence that the corresponding live work has finished. +`scripts/install_shared_generated_guard.py` now checks or installs the guard in +the repository's already configured shared hook directory. It preserves existing +hook routing and other hooks, and refuses a differing or redirected pre-commit +hook. Real temporary-repository tests prove rejection and source-change acceptance. +The missing shared guard was installed for the existing repo-manager hook path; +the week's fleet evidence and generator adoption still remain open. + ## Attended window #1 ```task