ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_cache_repair_rules.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Exact execution and raw-byte ownership contract for native-HIL cache repair."""
4
5from __future__ import annotations
6
7import hashlib
8import json
9import os
10import shutil
11import subprocess
12from collections.abc import Callable
13from copy import deepcopy
14from pathlib import Path
15from tempfile import TemporaryDirectory
16from typing import Any
17
18import authored_token_census as atc
19import hil_cache_isolation_rules as hci
20import yaml
21from check_shebangs import PRIVILEGED_BODY_PREFIX
22
23from scripts.dev.git_environment import trusted_git_executable
24
25RULES_SOURCE = "scripts/checks/hil_cache_repair_rules.py"
26PLAYBOOK = "infra/ansible/playbooks/hil-cache-repair.yml"
27DRIVER = "scripts/dev/hil_cache_repair.sh"
28JUSTFILE = "infra/hil-cache.just"
29DOCUMENTATION = "infra/README.md"
30JUST_REFERENCE_CHECKER = "scripts/checks/check_just_references.py"
31ENTRYPOINT_POLICY = "scripts/checks/shell_entrypoint_policy.py"
32SHEBANG_CHECKER = "scripts/checks/check_shebangs.py"
33DEV_BOX_DEFAULTS = "infra/ansible/roles/dev_box/defaults/main.yml"
34PRECOMMIT_HOOK = "just/hooks.just"
35FULL_GATE = "scripts/ci/gates/checks.sh"
36CORE_INPUT_FILES = (
37 PLAYBOOK,
38 DRIVER,
39 JUSTFILE,
40 DOCUMENTATION,
41 DEV_BOX_DEFAULTS,
42 hci.HIL_RUNNER_TASKS,
43 hci.HIL_RUNNER_SERVICE,
44 hci.SHARED_CACHE_TASKS,
45)
46SHARED_CACHE_ROOT = hci.SHARED_CACHE_ROOT
47HIL_CACHE_ROOT = hci.HIL_CACHE_ROOT
48CACHE_ROOT = HIL_CACHE_ROOT
49RUNNER_USER = "ra8-hil"
50SAFETY_VARS = frozenset({"dev_box_ccache_dir", "dev_box_hil_ccache_dir", "dev_box_hil_runner_user"})
51INVENTORY_SCOPE_PARTS = frozenset({"group_vars", "host_vars"})
52SELFTEST_OVERRIDE_COUNT = 9
53DEV_CONNECT_DIGEST = "cfeb5a1bcdf44c1fb822392bbc6dc985754d13562694797c0a8dd7b3bc01597f"
54ENTRYPOINT_TOKENS = (
55 "hil_cache_repair.sh",
56 "hil-cache-repair.yml",
57 "hil-cache.just",
58 "hil_cache_check",
59 "hil_cache_apply",
60)
61PINNED_ENTRYPOINT_OWNER_FILES = frozenset({RULES_SOURCE})
62PINNED_CHECKER_OCCURRENCES = {
63 JUST_REFERENCE_CHECKER: (
64 'STANDALONE_SURFACES = {"infra/hil-cache.just": frozenset({"check", "apply"})}',
65 'f"`{just_word} --justfile infra/hil-cache.just check`",',
66 'f"`{just_word} --justfile infra/hil-cache.just missing`",',
67 ),
68 SHEBANG_CHECKER: (),
69 ENTRYPOINT_POLICY: ('"scripts/dev/hil_cache_repair.sh": ShellPolicy(',),
70}
71AUTHORED_EXCLUDED_PARTS = frozenset(
72 {
73 ".cache",
74 ".git",
75 ".venv",
76 "__pycache__",
77 "build",
78 "generated",
79 "node_modules",
80 "third_party",
81 "vendor",
82 }
83)
84
85_repair_just_fixture = "just --justfile infra/hil-cache.just"
86_repair_driver_fixture = "bash scripts/dev/hil_cache_repair.sh"
87ENTRYPOINT_OWNERSHIP_SELFTEST_CASES = (
88 ("inline documentation", "docs/inline.md", f"Use `{_repair_just_fixture} apply`.\n"),
89 ("bulleted documentation", "docs/bullet.md", f"- {_repair_just_fixture} apply\n"),
90 (
91 "environment direct driver",
92 "scripts/direct.sh", # PATHREF-OK: throwaway ownership selftest fixture
93 f"env -i {_repair_driver_fixture} apply\n",
94 ),
95 (
96 "root recipe",
97 "justfile",
98 f"hil_cache_apply:\n {_repair_driver_fixture} apply\n",
99 ),
100 (
101 "module recipe",
102 "just/infra.just",
103 f"hil_cache_check:\n {_repair_driver_fixture} check\n",
104 ),
105 (
106 "renamed driver recipe",
107 "just/alternate.just",
108 f"cache-repair:\n {_repair_driver_fixture} apply\n",
109 ),
110 (
111 "renamed playbook recipe",
112 "just/alternate.just",
113 "cache-repair:\n ansible-playbook infra/ansible/playbooks/hil-cache-repair.yml\n",
114 ),
115 (
116 "renamed standalone recipe",
117 "just/alternate.just",
118 "cache-repair:\n just --justfile infra/hil-cache.just apply\n",
119 ),
120 ("bash login startup", ".bash_login", f"{_repair_driver_fixture} apply\n"),
121 ("bash logout startup", ".bash_logout", f"{_repair_driver_fixture} apply\n"),
122 ("zsh login startup", ".zlogin", f"{_repair_driver_fixture} apply\n"),
123 ("zsh logout startup", ".zlogout", f"{_repair_driver_fixture} apply\n"),
124 (
125 "GNU makefile",
126 "GNUmakefile",
127 f"repair:\n\t{_repair_driver_fixture} apply\n",
128 ),
129 (
130 "lowercase makefile",
131 "makefile",
132 f"repair:\n\t{_repair_driver_fixture} apply\n",
133 ),
134 (
135 "arbitrary extension",
136 "notes/repair.not-a-command-type",
137 f"{_repair_driver_fixture} apply\n",
138 ),
139 (
140 "arbitrary extensionless name",
141 "tools/repair",
142 f"{_repair_driver_fixture} apply\n",
143 ),
144)
145
146SAFE_DOCUMENTED_COMMANDS = (
147 "just --justfile infra/hil-cache.just check",
148 "just --justfile infra/hil-cache.just apply",
149)
150APPROVED_JUSTFILE = """# SPDX-License-Identifier: MIT
151# Copyright (c) 2026 Brighton Sikarskie
152
153set dotenv-load := false
154
155# Dry-run only the fixed private HIL compiler-cache repair on dev
156check:
157 /bin/bash -p "{{ justfile_directory() }}/../scripts/dev/hil_cache_repair.sh" check
158
159# Apply only the fixed private HIL compiler-cache repair on dev
160[confirm("Create or repair only ra8-hil's private compiler cache on dev?")]
161apply:
162 /bin/bash -p "{{ justfile_directory() }}/../scripts/dev/hil_cache_repair.sh" apply
163"""
164APPROVED_PLAYBOOK_DIGEST = "dafa425d2a2661d3c726e72117199c49a9debf603103347b18c151badf7d8bd0"
165
166APPROVED_DRIVER_LINES = (
167 *(line.strip() for line in PRIVILEGED_BODY_PREFIX),
168 "set -euo pipefail",
169 "export BASH_ENV=/dev/null ENV=/dev/null PYTHONNOUSERSITE=1",
170 "unset PYTHONHOME PYTHONPATH RA8_TOOL_VENV",
171 "PATH=/usr/bin:/bin",
172 "export PATH",
173 'ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"',
174 'PYTHON="${ROOT}/.venv/bin/python3"',
175 'ANSIBLE_PLAYBOOK="${ROOT}/.venv/bin/ansible-playbook"',
176 'mode="${1:-}"',
177 "reject_ansible_environment() {",
178 "local name",
179 "while IFS='=' read -r name _; do",
180 'if [[ "${name}" == ANSIBLE_* ]]; then',
181 'echo "error: inherited ANSIBLE_* environment is not allowed" >&2',
182 "return 2",
183 "fi",
184 "done < <(env)",
185 "}",
186 "selftest_rejection() {",
187 'local key="$1" output rc',
188 "set +e",
189 'output="$(env "${key}=unsafe" "${BASH_SOURCE[0]}" __probe_environment 2>&1)"',
190 "rc=$?",
191 "set -e",
192 'if [ "${rc}" -ne 2 ] || [ "${output}" != "error: inh'
193 'erited ANSIBLE_* environment is not allowed" ]; then',
194 'echo "hil_cache_repair.sh --selftest: ${key} was not rejected fail-closed" >&2',
195 "return 1",
196 "fi",
197 "}",
198 "selftest() {",
199 "selftest_rejection ANSIBLE_HOST_KEY_CHECKING",
200 "selftest_rejection ANSIBLE_ACTION_PLUGINS",
201 'echo "hil_cache_repair.sh --selftest: PASS"',
202 "}",
203 "reject_ansible_environment",
204 'if [ "$#" -eq 1 ] && [ "${mode}" = __probe_boundary ]; then',
205 '[ "${PATH}" = /usr/bin:/bin ] && [ "${BASH_ENV}" = /dev/null ] &&',
206 '[ "${ENV}" = /dev/null ] && [ -z "${PYTHONPATH:-}" ] &&',
207 '[ -z "${PYTHONHOME:-}" ] && [ -z "${RA8_TOOL_VENV:-}" ]',
208 "exit",
209 "fi",
210 'if [ "$#" -eq 1 ] && [ "${mode}" = __probe_environment ]; then',
211 'echo "environment accepted"',
212 "exit 0",
213 "fi",
214 'if [ "$#" -eq 1 ] && [ "${mode}" = --selftest ]; then',
215 "selftest",
216 "exit 0",
217 "fi",
218 'if [ "$#" -ne 1 ] || { [ "${mode}" != check ] && [ "${mode}" != apply ]; }; then',
219 'echo "usage: $0 check|apply" >&2',
220 "exit 2",
221 "fi",
222 'if [ ! -x "${PYTHON}" ] || [ ! -x "${ANSIBLE_PLAYBOOK}" ]; then',
223 "echo \"error: locked Ansible environment is absent; run 'just setup-python'\" >&2",
224 "exit 2",
225 "fi",
226 "umask 077",
227 'scratch="$(mktemp -d "${TMPDIR:-/tmp}/ra8-hil-cache-repair.XXXXXXXX")"',
228 'inventory="${scratch}/inventory.ini"',
229 'playbook="${scratch}/hil-cache-repair.yml"',
230 'config="${scratch}/ansible.cfg"',
231 "cleanup() {",
232 'rm -f -- "${inventory}" "${playbook}" "${config}"',
233 'rmdir -- "${scratch}"',
234 "}",
235 "trap cleanup EXIT",
236 "trap 'exit 129' HUP",
237 "trap 'exit 130' INT",
238 "trap 'exit 143' TERM",
239 '"${PYTHON}" "${ROOT}/scripts/checks/check_fleet_declaration.py" >/dev/null',
240 '"${PYTHON}" "${ROOT}/scripts/dev/fleet.py" inventory --stdout >"${inventory}"',
241 'cp "${ROOT}/infra/ansible/playbooks/hil-cache-repair.yml" "${playbook}"',
242 "printf '%s\\n' '[defaults]' 'host_key_checking = "
243 "True' 'retry_files_enabled = False' >\"${config}\"",
244 'args=("${ANSIBLE_PLAYBOOK}" -i "${inventory}" "${playbook}" --limit dev)',
245 'if [ "${mode}" = check ]; then',
246 "args+=(--check --diff)",
247 "fi",
248 'ANSIBLE_CONFIG="${config}" \\',
249 'ANSIBLE_COLLECTIONS_PATH="${ROOT}/.ansible/collections" \\',
250 "ANSIBLE_COLLECTIONS_SCAN_SYS_PATH=false \\",
251 "PYTHONNOUSERSITE=1 \\",
252 '"${args[@]}"',
253 "else",
254 '[[ "$-" == *p* ]]',
255 "fi",
256)
257
258
259def _read_yaml(path: Path) -> tuple[object, str | None]:
260 """Read one YAML document, returning a fail-closed error."""
261 try:
262 return yaml.safe_load(path.read_text(encoding="utf-8")), None
263 except (OSError, UnicodeError, yaml.YAMLError) as exc:
264 return None, str(exc)
265
266
267def _active_shell_lines(text: str) -> tuple[str, ...]:
268 """Strip comments and blanks from the small fixed driver."""
269 return tuple(
270 line.strip()
271 for line in text.splitlines()
272 if line.strip() and not line.lstrip().startswith("#")
273 )
274
275
276def _check_playbook_data(data: object) -> list[str]:
277 """Require the complete play document, not a tag-derived approximation."""
278 try:
279 encoded = json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
280 except (TypeError, ValueError):
281 encoded = b""
282 if hashlib.sha256(encoded).hexdigest() != APPROVED_PLAYBOOK_DIGEST:
283 return [f"{PLAYBOOK}: execution surface differs from the exact cache-only contract"]
284 return []
285
286
287def _check_driver_text(text: str) -> list[str]:
288 """Pin every executable line in the fixed no-argument driver."""
289 if _active_shell_lines(text) != APPROVED_DRIVER_LINES:
290 return [f"{DRIVER}: executable surface differs from the isolated driver contract"]
291 return []
292
293
294def _check_justfile_text(text: str) -> list[str]:
295 """Require the complete standalone file byte-for-byte."""
296 if text != APPROVED_JUSTFILE:
297 return [f"{JUSTFILE}: content differs from the exact standalone entry point"]
298 return []
299
300
301def _check_documentation_text(text: str) -> list[str]:
302 """Require the repair documentation to expose only the isolated entry point."""
303 repair_occurrences = tuple(
304 line for line in text.splitlines() if any(token in line for token in ENTRYPOINT_TOKENS)
305 )
306 if repair_occurrences != SAFE_DOCUMENTED_COMMANDS:
307 return [f"{DOCUMENTATION}: HIL cache repair commands are not the exact safe front door"]
308 return []
309
310
311def _authored_files(
312 repo_root: Path,
313 lstat_file: Callable[[Path], os.stat_result] = atc.path_lstat,
314) -> list[Path]:
315 """Return Git-authored regular files outside explicit excluded trees."""
316 return atc.authored_files(repo_root, AUTHORED_EXCLUDED_PARTS, lstat_file)
317
318
319def policy_input_files(repo_root: Path) -> tuple[str, ...]:
320 """Return core inputs plus every file in the authored-token census."""
321 discovered = (path.relative_to(repo_root).as_posix() for path in _authored_files(repo_root))
322 return tuple(dict.fromkeys((*CORE_INPUT_FILES, *discovered)))
323
324
325def _entrypoint_byte_hits(data: bytes) -> tuple[str, ...]:
326 """Find repair tokens encoded as UTF-8, UTF-16LE or UTF-16BE."""
327 return atc.token_hits(data, ENTRYPOINT_TOKENS)
328
329
330def _check_checker_occurrences(text: str, relative: str = JUST_REFERENCE_CHECKER) -> list[str]:
331 """Pin every allowed repair reference in one generic checker."""
332 actual = tuple(
333 line.strip()
334 for line in text.splitlines()
335 if any(token in line for token in ENTRYPOINT_TOKENS)
336 )
337 if actual != PINNED_CHECKER_OCCURRENCES[relative]:
338 return [f"{relative}: repair references differ from exact checker internals"]
339 return []
340
341
342def _check_exact_owner_source(source: atc.AuthoredSource) -> list[str] | None:
343 """Validate one exact front-door owner in either authored byte view."""
344 checks = {
345 DRIVER: _check_driver_text,
346 JUSTFILE: _check_justfile_text,
347 DOCUMENTATION: _check_documentation_text,
348 }
349 check = checks.get(source.relative)
350 if check is None:
351 return None
352 try:
353 text = source.data.decode("utf-8")
354 except UnicodeDecodeError:
355 return [f"{source.relative} [{source.view}]: exact front-door owner is not UTF-8"]
356 return [f"{problem} [{source.view} view]" for problem in check(text)]
357
358
359def _check_entrypoint_ownership(
360 repo_root: Path,
361 lstat_file: Callable[[Path], os.stat_result] = atc.path_lstat,
362 read_file: Callable[[Path], bytes] = atc.path_read_bytes,
363) -> list[str]:
364 """Reject every repair entrypoint occurrence outside its exact owners."""
365 problems = []
366 try:
367 authored = atc.authored_sources(repo_root, AUTHORED_EXCLUDED_PARTS, lstat_file, read_file)
368 except atc.CensusError as exc:
369 return [f"authored-file repair ownership cannot be proven: {exc}"]
370 for source in authored:
371 relative = source.relative
372 exact_owner_problems = _check_exact_owner_source(source)
373 if exact_owner_problems is not None:
374 problems.extend(exact_owner_problems)
375 continue
376 if relative in PINNED_ENTRYPOINT_OWNER_FILES:
377 continue
378 if relative in PINNED_CHECKER_OCCURRENCES:
379 try:
380 checker_text = source.data.decode("utf-8")
381 except UnicodeDecodeError:
382 problems.append(
383 f"{relative} [{source.view}]: exact checker internals are not UTF-8"
384 )
385 else:
386 problems.extend(_check_checker_occurrences(checker_text, relative))
387 continue
388 hits = _entrypoint_byte_hits(source.data)
389 if hits:
390 problems.append(
391 f"{relative} [{source.view}]: repair entrypoint token(s) "
392 f"{list(hits)!r} have no ownership"
393 )
394 return problems
395
396
397def _mapping_keys(value: object) -> set[str]:
398 """Collect mapping keys recursively from inventory variable documents."""
399 keys: set[str] = set()
400 if isinstance(value, dict):
401 for key, child in value.items():
402 keys.add(str(key))
403 keys.update(_mapping_keys(child))
404 elif isinstance(value, list):
405 for child in value:
406 keys.update(_mapping_keys(child))
407 return keys
408
409
410def _check_inventory_overrides(repo_root: Path) -> list[str]:
411 """Reject safety-owned path or identity variables in every inventory scope."""
412 ansible_root = repo_root / "infra" / "ansible"
413 problems = []
414 for path in sorted(ansible_root.rglob("*")):
415 if not path.is_file() or path.suffix not in {".json", ".yml", ".yaml"}:
416 continue
417 if not (INVENTORY_SCOPE_PARTS & set(path.parts)):
418 continue
419 loaded, error = _read_yaml(path)
420 relative = path.relative_to(repo_root).as_posix()
421 if error:
422 problems.append(f"{relative}: cannot audit safety-owned variables: {error}")
423 continue
424 problems.extend(
425 f"{relative}: may not override safety-owned variable {key!r}"
426 for key in sorted(_mapping_keys(loaded) & SAFETY_VARS)
427 )
428 return problems
429
430
431def _check_defaults(defaults: object) -> list[str]:
432 """Keep the full dev_box converge aligned with the standalone repair."""
433 if not isinstance(defaults, dict):
434 return [f"{DEV_BOX_DEFAULTS}: expected a YAML mapping"]
435 expected = {
436 "dev_box_ccache_dir": SHARED_CACHE_ROOT,
437 "dev_box_hil_ccache_dir": HIL_CACHE_ROOT,
438 "dev_box_hil_ccache_max_size": "10G",
439 "dev_box_hil_runner_user": RUNNER_USER,
440 }
441 return [
442 f"{DEV_BOX_DEFAULTS}: {key} is {defaults.get(key)!r}, expected {value!r}"
443 for key, value in expected.items()
444 if defaults.get(key) != value
445 ]
446
447
448def _connect_digest(connect: object) -> str:
449 """Hash a normalized connection mapping without reporting endpoint values."""
450 try:
451 encoded = json.dumps(connect, sort_keys=True, separators=(",", ":")).encode()
452 except (TypeError, ValueError):
453 return ""
454 return hashlib.sha256(encoded).hexdigest()
455
456
457def _check_dispatch(
458 fleet: dict[str, Any], expected_connect_digest: str = DEV_CONNECT_DIGEST
459) -> list[str]:
460 """Require the fixed dev target and its private SSH identity to remain exact."""
461 dev = fleet.get("hosts", {}).get("dev", {})
462 if not isinstance(dev, dict):
463 return ["infra/fleet.yml: fixed HIL cache target or private SSH identity changed"]
464 hil = dev.get("hil_runner", {})
465 connect = dev.get("connect", {})
466 if (
467 dev.get("class") != "dev_box"
468 or "dev-box" not in dev.get("provisions", [])
469 or not isinstance(hil, dict)
470 or not isinstance(connect, dict)
471 or set(connect) != {"address", "user"}
472 or _connect_digest(connect) != expected_connect_digest
473 ):
474 return ["infra/fleet.yml: fixed HIL cache target or private SSH identity changed"]
475 return []
476
477
478def _check_text_file(
479 repo_root: Path, relative: str, checker: Callable[[str], list[str]]
480) -> list[str]:
481 """Read and validate one textual execution-surface file."""
482 try:
483 return checker((repo_root / relative).read_text(encoding="utf-8"))
484 except (OSError, UnicodeError) as exc:
485 return [f"{relative}: cannot read execution surface: {exc}"]
486
487
488def _check_gate_wiring_texts(hook_text: str, gate_text: str) -> list[str]:
489 """Require unconditional pre-commit and full-gate repair validation."""
490 hook_line = " pre-commit-checks"
491 gate_pair = (
492 " python3 scripts/checks/check_fleet_declaration.py --selftest\n"
493 " python3 scripts/checks/check_fleet_declaration.py\n"
494 )
495 problems = []
496 if hook_text.splitlines().count(hook_line) != 1:
497 problems.append(f"{PRECOMMIT_HOOK}: pre-commit-checks is not wired exactly once")
498 if gate_text.count(gate_pair) != 1:
499 problems.append(f"{FULL_GATE}: fleet guard pair is not wired exactly once")
500 return problems
501
502
503def _check_gate_wiring(repo_root: Path) -> list[str]:
504 """Read the two unconditional gate dispatch surfaces fail-closed."""
505 try:
506 return _check_gate_wiring_texts(
507 (repo_root / PRECOMMIT_HOOK).read_text(encoding="utf-8"),
508 (repo_root / FULL_GATE).read_text(encoding="utf-8"),
509 )
510 except (OSError, UnicodeError) as exc:
511 return [f"HIL cache repair gate wiring cannot be read: {exc}"]
512
513
514def check(repo_root: Path, fleet: dict[str, Any]) -> list[str]:
515 """Validate the standalone playbook, driver, recipes, defaults and inventory."""
516 playbook, playbook_error = _read_yaml(repo_root / PLAYBOOK)
517 defaults, defaults_error = _read_yaml(repo_root / DEV_BOX_DEFAULTS)
518 problems = []
519 if playbook_error:
520 problems.append(f"{PLAYBOOK}: cannot read YAML: {playbook_error}")
521 else:
522 problems += _check_playbook_data(playbook)
523 if defaults_error:
524 problems.append(f"{DEV_BOX_DEFAULTS}: cannot read YAML: {defaults_error}")
525 else:
526 problems += _check_defaults(defaults)
527 problems += _check_text_file(repo_root, DRIVER, _check_driver_text)
528 problems += _check_text_file(repo_root, JUSTFILE, _check_justfile_text)
529 problems += _check_text_file(repo_root, DOCUMENTATION, _check_documentation_text)
530 return (
531 problems
532 + _check_entrypoint_ownership(repo_root)
533 + _check_inventory_overrides(repo_root)
534 + hci.check(repo_root)
535 + _check_gate_wiring(repo_root)
536 + _check_dispatch(fleet)
537 )
538
539
540def _playbook_mutations() -> dict[str, Any]:
541 """Return independently widened playbook documents for both-direction tests."""
542 return {
543 "rescue systemctl": lambda play: play[0]["tasks"][-1].update(
544 {"rescue": [{"name": "restart", "ansible.builtin.command": "systemctl restart x"}]}
545 ),
546 "pre-task always": lambda play: play[0].update(
547 {"pre_tasks": [{"name": "escape", "tags": "always", "ansible.builtin.command": "id"}]}
548 ),
549 "role always": lambda play: play[0].update(
550 {"roles": [{"role": "dev_box", "tags": "always"}]}
551 ),
552 "include apply delegate": lambda play: play[0]["tasks"].append(
553 {
554 "name": "escape include delegate",
555 "ansible.builtin.include_tasks": {
556 "file": "escape.yml",
557 "apply": {"delegate_to": "star"},
558 },
559 }
560 ),
561 "include apply check mode": lambda play: play[0]["tasks"].append(
562 {
563 "name": "escape include check mode",
564 "ansible.builtin.include_tasks": {
565 "file": "escape.yml",
566 "apply": {"check_mode": False},
567 },
568 }
569 ),
570 "include apply vars": lambda play: play[0]["tasks"].append(
571 {
572 "name": "escape include vars",
573 "ansible.builtin.include_tasks": {
574 "file": "escape.yml",
575 "apply": {"vars": {"dev_box_hil_ccache_dir": "/"}},
576 },
577 }
578 ),
579 "filesystem root": lambda play: play[0]["tasks"][4]["ansible.builtin.file"].update(
580 {"path": "/"}
581 ),
582 "root identity": lambda play: play[0]["tasks"][0]["ansible.builtin.getent"].update(
583 {"key": "root"}
584 ),
585 "all hosts": lambda play: play[0].update({"hosts": "all"}),
586 "task vars override": lambda play: play[0]["tasks"][4].update(
587 {"vars": {"dev_box_hil_ccache_dir": "/"}}
588 ),
589 "handler notify": lambda play: play[0]["tasks"][4].update({"notify": "restart runner"}),
590 "forced check mode": lambda play: play[0]["tasks"][5].update({"check_mode": False}),
591 "delegation": lambda play: play[0]["tasks"][5].update({"delegate_to": "star"}),
592 "connection override": lambda play: play[0].update({"connection": "local"}),
593 "shared cache crossing": lambda play: play[0]["tasks"][4]["ansible.builtin.file"].update(
594 {"path": SHARED_CACHE_ROOT}
595 ),
596 }
597
598
599def _selftest_playbook(repo_root: Path) -> list[str]:
600 """Prove the exact document accepts once and rejects every widening shape."""
601 failures = []
602 approved, error = _read_yaml(repo_root / PLAYBOOK)
603 if error:
604 return [f" cannot load approved standalone playbook: {error}"]
605 if _check_playbook_data(deepcopy(approved)):
606 failures.append(" the approved standalone playbook was rejected")
607 for name, mutate in _playbook_mutations().items():
608 broken = deepcopy(approved)
609 mutate(broken)
610 if not _check_playbook_data(broken):
611 failures.append(f" playbook widening was accepted: {name}")
612 return failures
613
614
615def _selftest_driver(repo_root: Path) -> list[str]:
616 """Prove exact driver lines and inherited-environment rejection."""
617 failures = []
618 good_driver = "\n".join(APPROVED_DRIVER_LINES)
619 if _check_driver_text(good_driver):
620 failures.append(" the approved driver execution lines were rejected")
621 if not _check_driver_text(good_driver + "\nsystemctl restart ra8-hil\n"):
622 failures.append(" an extra driver command was accepted")
623
624 clean_env = {key: value for key, value in os.environ.items() if not key.startswith("ANSIBLE_")}
625 driver_selftest = subprocess.run( # noqa: S603 -- exact repository-owned executable
626 [repo_root / DRIVER, "--selftest"],
627 check=False,
628 capture_output=True,
629 text=True,
630 env=clean_env,
631 )
632 if (
633 driver_selftest.returncode
634 or driver_selftest.stdout.strip() != "hil_cache_repair.sh --selftest: PASS"
635 or driver_selftest.stderr
636 ):
637 failures.append(" driver did not reject inherited Ansible control variables")
638 with TemporaryDirectory(prefix="ra8-hil-cache-boundary-") as raw:
639 poison = Path(raw) / "startup.sh"
640 poison.write_text("printf poisoned\n", encoding="utf-8")
641 hostile = clean_env.copy()
642 for key in ("PYTHONHOME", "PYTHONPATH", "RA8_TOOL_VENV"):
643 hostile[key] = "/unsafe"
644 hostile.update(BASH_ENV=str(poison), ENV=str(poison), PATH=str(Path(raw) / "absent-bin"))
645 boundary = subprocess.run( # noqa: S603 -- exact repository-owned executable
646 [repo_root / DRIVER, "__probe_boundary"],
647 check=False,
648 capture_output=True,
649 text=True,
650 env=hostile,
651 )
652 if boundary.returncode or boundary.stdout or boundary.stderr:
653 failures.append(" hostile startup state reached the cache-repair boundary")
654 return failures
655
656
657def _selftest_just_content() -> list[str]:
658 """Prove every byte of the standalone Justfile is pinned."""
659 failures = []
660 if _check_justfile_text(APPROVED_JUSTFILE):
661 failures.append(" approved standalone Justfile was rejected")
662 just_mutations = {
663 "caller-controlled host": APPROVED_JUSTFILE.replace("check:", "check host:", 1),
664 "removed confirmation": APPROVED_JUSTFILE.replace("[confirm", "[private", 1),
665 "dotenv enabled": APPROVED_JUSTFILE.replace("dotenv-load := false", "dotenv-load := true"),
666 "unexpected shell setting": APPROVED_JUSTFILE.replace(
667 "set dotenv-load := false",
668 'set dotenv-load := false\nset shell := ["sh", "-c"]',
669 ),
670 "import": APPROVED_JUSTFILE.replace(
671 "set dotenv-load := false", 'set dotenv-load := false\nimport "../just/infra.just"'
672 ),
673 "export backtick": APPROVED_JUSTFILE.replace(
674 "set dotenv-load := false", "set dotenv-load := false\nexport PATH := `printf /tmp`"
675 ),
676 "per-recipe working directory": APPROVED_JUSTFILE.replace(
677 "check:", '[working-directory("/tmp")]\ncheck:', 1
678 ),
679 "attribute before confirmation": APPROVED_JUSTFILE.replace(
680 "[confirm", "[private]\n[confirm", 1
681 ),
682 "changed wrapper": APPROVED_JUSTFILE.replace("hil_cache_repair.sh", "infra.sh", 1),
683 "extra body line": APPROVED_JUSTFILE.replace(
684 " check\n\n# Apply", " check\n true\n\n# Apply"
685 ),
686 "ssh star body": APPROVED_JUSTFILE.replace(
687 " check\n\n# Apply", " check\n ssh star true\n\n# Apply"
688 ),
689 "reboot body": APPROVED_JUSTFILE.replace(" apply\n", " apply\n reboot\n"),
690 }
691 # A mutation whose anchor stopped matching silently rewrites nothing, so the
692 # must-fire case passes while proving nothing. A `just --fmt` reflow of the
693 # standalone justfile did exactly that to the two body-injection cases.
694 failures.extend(
695 f" Just bypass mutation is vacuous: {name}"
696 for name, broken in just_mutations.items()
697 if broken == APPROVED_JUSTFILE
698 )
699 failures.extend(
700 f" Just bypass was accepted: {name}"
701 for name, broken in just_mutations.items()
702 if not _check_justfile_text(broken)
703 )
704 return failures
705
706
707def _selftest_documentation() -> list[str]:
708 """Prove repair docs cannot route through the root graph or direct driver."""
709 approved = "\n".join(SAFE_DOCUMENTED_COMMANDS)
710 failures = []
711 if _check_documentation_text(approved):
712 failures.append(" approved standalone repair documentation was rejected")
713 mutations = {
714 "root module": approved.replace(
715 SAFE_DOCUMENTED_COMMANDS[0], "jus" + "t infra::hil_cache_check"
716 ),
717 "inline Markdown": approved + "\nUse `just --justfile infra/hil-cache.just apply`.",
718 "bulleted Markdown": approved + "\n- just --justfile infra/hil-cache.just apply",
719 "environment-prefixed direct driver": approved
720 + "\nenv -i bash scripts/dev/hil_cache_repair.sh apply",
721 "direct driver": approved.replace(
722 SAFE_DOCUMENTED_COMMANDS[0], "bash scripts/dev/hil_cache_repair.sh check"
723 ),
724 "extra argument": approved.replace(
725 SAFE_DOCUMENTED_COMMANDS[0], f"{SAFE_DOCUMENTED_COMMANDS[0]} --limit star"
726 ),
727 }
728 failures.extend(
729 f" unsafe documented repair front door was accepted: {name}"
730 for name, broken in mutations.items()
731 if not _check_documentation_text(broken)
732 )
733 return failures
734
735
736def _selftest_entrypoint_ownership() -> list[str]:
737 """Prove every alternate documentation, script and Just owner fires."""
738 failures = []
739 for name, relative, content in ENTRYPOINT_OWNERSHIP_SELFTEST_CASES:
740 with TemporaryDirectory() as tmp, atc.isolated_git_environment():
741 root = Path(tmp)
742 path = root / relative
743 atc.init_test_repo(root)
744 path.parent.mkdir(parents=True, exist_ok=True)
745 path.write_text(content, encoding="utf-8")
746 if not _check_entrypoint_ownership(root):
747 failures.append(f" unowned repair entrypoint was accepted: {name}")
748 return failures
749
750
751def _selftest_exact_owner_views() -> list[str]:
752 """Prove exact owners are checked independently in index and worktree views."""
753 approved_driver = "\n".join(APPROVED_DRIVER_LINES) + "\n"
754 unsafe_driver = approved_driver + "systemctl restart ra8-hil\n"
755 approved_docs = "\n".join(SAFE_DOCUMENTED_COMMANDS) + "\n"
756 unsafe_docs = "bash scripts/dev/hil_cache_repair.sh apply\n"
757 unsafe_just = APPROVED_JUSTFILE + "\nrogue:\n reboot\n"
758 cases = (
759 ("documentation", DOCUMENTATION, approved_docs, unsafe_docs),
760 ("driver", DRIVER, approved_driver, unsafe_driver),
761 ("Justfile", JUSTFILE, APPROVED_JUSTFILE, unsafe_just),
762 )
763 failures = []
764 for name, relative, approved, unsafe in cases:
765 for unsafe_view in ("index", "worktree"):
766 with TemporaryDirectory() as tmp, atc.isolated_git_environment():
767 root = Path(tmp)
768 atc.init_test_repo(root)
769 path = root / relative
770 path.parent.mkdir(parents=True, exist_ok=True)
771 index_data = unsafe if unsafe_view == "index" else approved
772 path.write_text(index_data, encoding="utf-8")
773 subprocess.run( # noqa: S603 -- fixed throwaway fixture command
774 [trusted_git_executable(), "add", "--", relative],
775 cwd=root,
776 capture_output=True,
777 check=True,
778 )
779 worktree_data = unsafe if unsafe_view == "worktree" else approved
780 path.write_text(worktree_data, encoding="utf-8")
781 if not _check_entrypoint_ownership(root):
782 failures.append(f" unsafe {name} {unsafe_view} view was accepted")
783 return failures
784
785
786def _selftest_policy_inputs() -> list[str]:
787 """Prove Git-authored scope, encoding, alias and I/O behavior."""
788 return atc.selftest(AUTHORED_EXCLUDED_PARTS)
789
790
791def _selftest_checker_occurrences() -> list[str]:
792 """Prove generic-checker ownership accepts only its exact references."""
793 failures = []
794 for relative, expected in PINNED_CHECKER_OCCURRENCES.items():
795 approved = "\n".join(expected)
796 if _check_checker_occurrences(approved, relative):
797 failures.append(f" approved {relative} repair references were rejected")
798 widened = approved + '\nsubprocess.run(["bash", "hil_cache_repair.sh", "apply"])'
799 if not _check_checker_occurrences(widened, relative):
800 failures.append(f" executable repair reference in {relative} was accepted")
801 return failures
802
803
804def _selftest_gate_wiring() -> list[str]:
805 """Prove every census change reaches both unconditional repair gates."""
806 hook = "pre-commit:\n gates=(\n pre-commit-checks\n )\n"
807 gate = (
808 "_pcc_repository_structure() (\n"
809 " python3 scripts/checks/check_fleet_declaration.py --selftest\n"
810 " python3 scripts/checks/check_fleet_declaration.py\n"
811 ")\n"
812 )
813 failures = []
814 if _check_gate_wiring_texts(hook, gate):
815 failures.append(" approved unconditional repair-gate wiring was rejected")
816 if not _check_gate_wiring_texts(hook.replace("pre-commit-checks", "lint-just"), gate):
817 failures.append(" pre-commit repair-gate removal was accepted")
818 if not _check_gate_wiring_texts(hook, gate.replace(" --selftest", "")):
819 failures.append(" full-gate repair selftest removal was accepted")
820 if not _check_gate_wiring_texts(hook, gate.replace("declaration.py\n", "other.py\n")):
821 failures.append(" full-gate live repair guard removal was accepted")
822 return failures
823
824
825def _selftest_just_isolation() -> list[str]:
826 """Prove an explicit standalone invocation never parses root or modules."""
827 just = shutil.which("just")
828 if just is None:
829 return [" cannot prove explicit Justfile isolation without just"]
830 with TemporaryDirectory() as tmp:
831 root = Path(tmp)
832 standalone = root / JUSTFILE
833 module = root / "just" / "hostile.just"
834 standalone.parent.mkdir(parents=True)
835 module.parent.mkdir(parents=True)
836 standalone.write_text(APPROVED_JUSTFILE, encoding="utf-8")
837 (root / "justfile").write_text(
838 "export PATH := `touch root-assignment-fired; printf /usr/bin`\n"
839 'mod hostile "just/hostile.just"\n'
840 "this is invalid Just syntax\n"
841 "check:\n"
842 " @true\n",
843 encoding="utf-8",
844 )
845 module.write_text(
846 "export PYTHONPATH := `touch module-assignment-fired; printf /tmp`\n"
847 "this is invalid Just syntax\n"
848 "module-check:\n"
849 " @true\n",
850 encoding="utf-8",
851 )
852 markers = (root / "root-assignment-fired", root / "module-assignment-fired")
853 hostile_files = ((root / "justfile", "check"), (module, "module-check"))
854 for hostile_file, recipe in hostile_files:
855 hostile_result = subprocess.run( # noqa: S603 -- pinned throwaway fixture
856 [just, "--justfile", hostile_file, "--dry-run", recipe],
857 cwd=root,
858 check=False,
859 capture_output=True,
860 text=True,
861 )
862 if not hostile_result.returncode:
863 return [" hostile root/module fixture did not fail when parsed directly"]
864 for marker in markers:
865 marker.unlink(missing_ok=True)
866
867 result = subprocess.run( # noqa: S603 -- pinned tool and throwaway Justfile
868 [just, "--justfile", standalone, "--dry-run", "check"],
869 cwd=root,
870 check=False,
871 capture_output=True,
872 text=True,
873 )
874 if result.returncode:
875 return [" explicit standalone Justfile dry-run failed"]
876 if any(marker.exists() for marker in markers):
877 return [" explicit standalone invocation parsed a hostile root or module"]
878 return []
879
880
881def _selftest_inventory() -> list[str]:
882 """Prove inventory scopes cannot override either safety-owned value."""
883 failures = []
884 with TemporaryDirectory() as tmp:
885 root = Path(tmp)
886 if _check_inventory_overrides(root):
887 failures.append(" an empty inventory-variable scope was rejected")
888 for relative in (
889 "infra/ansible/inventory/host_vars/dev.yml",
890 "infra/ansible/group_vars/all.yaml",
891 "infra/ansible/inventory/host_vars/dev.json",
892 ):
893 path = root / relative
894 path.parent.mkdir(parents=True, exist_ok=True)
895 if path.suffix == ".json":
896 content = json.dumps(
897 {
898 "dev_box_ccache_dir": "/",
899 "dev_box_hil_ccache_dir": "/",
900 "dev_box_hil_runner_user": "root",
901 }
902 )
903 else:
904 content = (
905 "dev_box_ccache_dir: /\n"
906 "dev_box_hil_ccache_dir: /\n"
907 "dev_box_hil_runner_user: root\n"
908 )
909 path.write_text(content, encoding="utf-8")
910 if len(_check_inventory_overrides(root)) != SELFTEST_OVERRIDE_COUNT:
911 failures.append(" host_vars/group_vars safety overrides were accepted")
912 return failures
913
914
915def _selftest_files(repo_root: Path) -> list[str]:
916 """Prove driver, recipe and inventory override checks fire both ways."""
917 return (
918 _selftest_driver(repo_root)
919 + _selftest_just_content()
920 + _selftest_documentation()
921 + _selftest_entrypoint_ownership()
922 + _selftest_exact_owner_views()
923 + _selftest_policy_inputs()
924 + _selftest_checker_occurrences()
925 + _selftest_gate_wiring()
926 + _selftest_just_isolation()
927 + _selftest_inventory()
928 + hci.selftest()
929 )
930
931
932def selftest(repo_root: Path) -> list[str]:
933 """Prove the standalone execution contract has both acceptance directions."""
934 failures = _selftest_playbook(repo_root) + _selftest_files(repo_root)
935 approved_defaults = {
936 "dev_box_ccache_dir": SHARED_CACHE_ROOT,
937 "dev_box_hil_ccache_dir": HIL_CACHE_ROOT,
938 "dev_box_hil_ccache_max_size": "10G",
939 "dev_box_hil_runner_user": RUNNER_USER,
940 }
941 if _check_defaults(approved_defaults):
942 failures.append(" aligned full-role defaults were rejected")
943 unsafe_defaults = dict(approved_defaults)
944 unsafe_defaults.update({"dev_box_hil_ccache_dir": "/", "dev_box_hil_runner_user": "root"})
945 if not _check_defaults(unsafe_defaults):
946 failures.append(" unsafe full-role defaults were accepted")
947 good_connect = {"address": "192.0.2.10", "user": "dev-user"}
948 expected_digest = _connect_digest(good_connect)
949 good_fleet = {
950 "hosts": {
951 "dev": {
952 "class": "dev_box",
953 "provisions": ["dev-box"],
954 "hil_runner": {},
955 "connect": good_connect,
956 },
957 "star": {"connect": {"address": "192.0.2.20", "user": "bench-user"}},
958 }
959 }
960 if _check_dispatch(good_fleet, expected_digest):
961 failures.append(" fixed synthetic dev transport was rejected")
962 repointed = deepcopy(good_fleet)
963 repointed["hosts"]["dev"]["connect"] = deepcopy(repointed["hosts"]["star"]["connect"])
964 jumped = deepcopy(good_fleet)
965 jumped["hosts"]["dev"]["connect"]["jump"] = "star"
966 if not _check_dispatch(repointed, expected_digest):
967 failures.append(" dev repointed to the bench transport was accepted")
968 if not _check_dispatch(jumped, expected_digest):
969 failures.append(" dev ProxyJump through the bench was accepted")
970 if not _check_dispatch({"hosts": {}}, expected_digest):
971 failures.append(" missing fixed dev dispatcher target was accepted")
972 return failures
-copyright