ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_policy.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Governed inputs and workflow policy for HIL convergence safety."""
4
5from __future__ import annotations
6
7import json
8import tempfile
9from pathlib import Path
10from typing import cast
11
12import yaml
13
14REPO_ROOT = Path(__file__).resolve().parents[2]
15DEV_ENTRY = "infra/ansible/roles/dev_box/tasks/hil_runner.yml"
16DEV_ROLE = "infra/ansible/roles/dev_box/tasks/hil_runner_transaction.yml"
17DEV_MAIN_ENTRY = "infra/ansible/roles/dev_box/tasks/main.yml"
18DEV_MAIN = "infra/ansible/roles/dev_box/tasks/transaction.yml"
19DEV_IMAGE_LOCK = "infra/ansible/roles/dev_box/tasks/image_lock.yml"
20DEV_GUARD = "infra/ansible/roles/dev_box/tasks/hil_mutation_guard.yml"
21DEV_HANDLER = "infra/ansible/roles/dev_box/handlers/main.yml"
22IDLE_HELPER = "infra/ansible/roles/dev_box/files/ra8-hil-runner-idle-stop.py"
23BENCH_ENTRY = "infra/ansible/roles/hil_bench/tasks/main.yml"
24BENCH_ROLE = "infra/ansible/roles/hil_bench/tasks/transaction.yml"
25BENCH_GUARD = "infra/ansible/roles/hil_bench/tasks/transaction_guard.yml"
26C6_ENTRY = "infra/ansible/roles/c6_toolchain/tasks/main.yml"
27C6_ROLE = "infra/ansible/roles/c6_toolchain/tasks/transaction.yml"
28AD2_ENTRY = "infra/ansible/roles/ad2_tools/tasks/main.yml"
29AD2_ROLE = "infra/ansible/roles/ad2_tools/tasks/transaction.yml"
30BENCH_DEFAULTS = "infra/ansible/roles/hil_bench/defaults/main.yml"
31FLEET = "scripts/dev/fleet.py"
32FLEET_BENCH = "scripts/dev/fleet_bench.py"
33FLEET_CAPACITY_CLIENT = "scripts/dev/fleet_capacity_client.py"
34FLEET_RECONCILE = "scripts/dev/fleet_reconcile.py"
35FLEET_RECONCILE_ARC_SELFTEST = "scripts/dev/fleet_reconcile_arc_selftest.py"
36FLEET_RECONCILE_PROCESS = "scripts/dev/fleet_reconcile_process.py"
37FLEET_RUNNER = "scripts/dev/fleet_runner_maintenance.py"
38FLEET_WSL = "scripts/dev/fleet_wsl.py"
39FLEET_WSL_STAGE = "scripts/dev/fleet_wsl_stage.py"
40FLEET_MODEL = "scripts/dev/fleet_model.py"
41FLEET_RUNNER_MODEL = "scripts/dev/fleet_runner_model.py"
42FLEET_REACH = "scripts/dev/fleet_reach.py"
43FLEET_PATH_AUTHORITY = "scripts/dev/fleet_path_authority.py"
44GATE = "scripts/ci/gates/checks.sh"
45WORKFLOW = ".github/workflows/hil.yml"
46HIL_JUST = "just/hil.just"
47DECLARATION = "infra/fleet.yml"
48PLAYBOOKS = (
49 "infra/ansible/playbooks/dev-box.yml",
50 "infra/ansible/playbooks/hil-bench.yml",
51)
52DIRECT_DEPENDENCIES = (
53 ".devcontainer/Dockerfile",
54 "scripts/ci/**",
55 "scripts/dev/**",
56 "scripts/checks/check_runner_image_deps.py",
57 "scripts/checks/check_tool_versions.py",
58)
59BASE_WORKFLOW_PATHS = (
60 "just/**",
61 "justfile",
62 "infra/bootstrap.sh",
63 "scripts/dev/fleet*.py",
64 "infra/ansible/ansible.cfg",
65 "infra/ansible/requirements.yml",
66 "scripts/checks/check_ansible_collections.py",
67 "scripts/checks/check_shebangs.py",
68 GATE,
69 DECLARATION,
70 "scripts/checks/check_hil_convergence_safety.py",
71 "scripts/checks/hil_convergence_check_mode.py",
72 "scripts/checks/hil_convergence_safety_*.py",
73 "scripts/checks/shell_entrypoint_policy*.py",
74 "scripts/hil/**",
75)
76
77
78class WorkflowPolicyError(ValueError):
79 """A playbook cannot provide a safe workflow dependency closure."""
80
81
82def load_bench_transaction(root: Path) -> object:
83 """Follow the public bench role into its one owned dynamic transaction."""
84 entry_path = root / BENCH_ENTRY
85 entry = yaml.safe_load(entry_path.read_text(encoding="utf-8"))
86 if not isinstance(entry, list) or len(entry) != 1 or not isinstance(entry[0], dict):
87 message = "HIL bench role entry is not one dynamic transaction"
88 raise WorkflowPolicyError(message)
89 include = entry[0].get("ansible.builtin.include_tasks")
90 if not isinstance(include, dict) or set(include) != {"file"}:
91 message = "HIL bench role entry does not own one task file"
92 raise WorkflowPolicyError(message)
93 relative = include["file"]
94 if relative != "transaction.yml":
95 message = "HIL bench role transaction authority drifted"
96 raise WorkflowPolicyError(message)
97 transaction = entry_path.with_name(relative)
98 if transaction.is_symlink() or not transaction.is_file():
99 message = "HIL bench role transaction is unavailable or linked"
100 raise WorkflowPolicyError(message)
101 return yaml.safe_load(transaction.read_text(encoding="utf-8"))
102
103
104def workflow_paths(root: Path) -> tuple[str, ...]:
105 """Derive playbook/role closure, then add direct command-source inputs."""
106 derived: set[str] = set(BASE_WORKFLOW_PATHS)
107 derived.update(DIRECT_DEPENDENCIES)
108 initial_roles: list[str] = []
109 for playbook in PLAYBOOKS:
110 document = yaml.safe_load((root / playbook).read_text(encoding="utf-8"))
111 if not isinstance(document, list):
112 message = f"workflow playbook is malformed: {playbook}"
113 raise WorkflowPolicyError(message)
114 derived.add(playbook)
115 for play in document:
116 if not isinstance(play, dict):
117 message = f"workflow playbook has a malformed play: {playbook}"
118 raise WorkflowPolicyError(message)
119 roles = list(play.get("roles") or [])
120 for section in ("pre_tasks", "tasks"):
121 tasks = play.get(section) or []
122 if not isinstance(tasks, list):
123 message = f"workflow playbook has malformed {section}: {playbook}"
124 raise WorkflowPolicyError(message)
125 for task in tasks:
126 include = (
127 task.get("ansible.builtin.include_role") if isinstance(task, dict) else None
128 )
129 if isinstance(include, dict):
130 roles.append(include.get("name"))
131 if not roles:
132 message = f"workflow playbook has no role closure: {playbook}"
133 raise WorkflowPolicyError(message)
134 initial_roles.extend(_role_name(role) for role in roles)
135 for role in _role_closure(root, initial_roles):
136 derived.add(f"infra/ansible/roles/{role}/**")
137 return tuple(sorted(derived))
138
139
140def _role_name(value: object) -> str:
141 """Return one confined static role name."""
142 if not isinstance(value, str) or "/" in value or value in {"", ".", ".."}:
143 msg = f"workflow contains unsafe role: {value!r}"
144 raise WorkflowPolicyError(msg)
145 return value
146
147
148def _role_dependencies(document: object, label: str) -> set[str]:
149 """Find nested include/import role names in one task or metadata tree."""
150 dependencies: set[str] = set()
151 if isinstance(document, list):
152 for item in document:
153 dependencies.update(_role_dependencies(item, label))
154 elif isinstance(document, dict):
155 for key in ("ansible.builtin.include_role", "ansible.builtin.import_role"):
156 include = document.get(key)
157 if isinstance(include, dict) and "name" in include:
158 dependencies.add(_role_name(include["name"]))
159 elif include is not None:
160 msg = f"{label}: malformed {key}"
161 raise WorkflowPolicyError(msg)
162 for key, value in document.items():
163 if key not in {
164 "ansible.builtin.include_role",
165 "ansible.builtin.import_role",
166 }:
167 dependencies.update(_role_dependencies(value, label))
168 return dependencies
169
170
171def _included_task_files(path: Path, role_root: Path, seen: set[Path]) -> set[Path]:
172 """Follow static task includes with cycle and real-path confinement checks."""
173 resolved = path.resolve(strict=True)
174 if resolved in seen:
175 return set()
176 if resolved != role_root and role_root not in resolved.parents:
177 msg = f"role task include escaped its owner: {path}"
178 raise WorkflowPolicyError(msg)
179 seen.add(resolved)
180 document = yaml.safe_load(resolved.read_text(encoding="utf-8"))
181 found = {resolved}
182 for task in document if isinstance(document, list) else []:
183 if not isinstance(task, dict):
184 msg = f"role task file is malformed: {path}"
185 raise WorkflowPolicyError(msg)
186 for key in ("ansible.builtin.include_tasks", "ansible.builtin.import_tasks"):
187 include = task.get(key)
188 value = include.get("file") if isinstance(include, dict) else include
189 if value is None:
190 continue
191 if not isinstance(value, str) or "{{" in value or Path(value).is_absolute():
192 msg = f"role task include is not static: {value!r}"
193 raise WorkflowPolicyError(msg)
194 found.update(_included_task_files(resolved.parent / value, role_root, seen))
195 return found
196
197
198def _one_role_dependencies(root: Path, role: str) -> set[str]:
199 """Return nested task/meta role dependencies for one exact role."""
200 roles_root = (root / "infra/ansible/roles").resolve(strict=True)
201 role_root = (roles_root / role).resolve(strict=True)
202 if role_root.parent != roles_root or (role_root / "tasks/main.yml").is_symlink():
203 msg = f"role path is linked or escaped: {role}"
204 raise WorkflowPolicyError(msg)
205 task_files = _included_task_files(role_root / "tasks/main.yml", role_root, set())
206 dependencies: set[str] = set()
207 for path in task_files:
208 dependencies.update(
209 _role_dependencies(yaml.safe_load(path.read_text(encoding="utf-8")), str(path))
210 )
211 meta = role_root / "meta/main.yml"
212 if meta.exists():
213 document = yaml.safe_load(meta.read_text(encoding="utf-8"))
214 raw = document.get("dependencies") if isinstance(document, dict) else None
215 for item in raw or []:
216 value = item.get("role") if isinstance(item, dict) else item
217 dependencies.add(_role_name(value))
218 return dependencies
219
220
221def _role_closure(root: Path, initial: list[str]) -> set[str]:
222 """Return complete recursive task/meta role closure with cycle handling."""
223 closure: set[str] = set()
224 pending = list(initial)
225 while pending:
226 role = _role_name(pending.pop())
227 if role in closure:
228 continue
229 closure.add(role)
230 pending.extend(sorted(_one_role_dependencies(root, role) - closure))
231 return closure
232
233
234def workflow_dependency_selftest() -> list[str]:
235 """Prove nested task includes and meta dependencies enter the trigger set."""
236 with tempfile.TemporaryDirectory(prefix="ra8-hil-workflow-") as raw:
237 root = Path(raw)
238 for playbook, role in zip(PLAYBOOKS, ("alpha", "beta"), strict=False):
239 target = root / playbook
240 target.parent.mkdir(parents=True, exist_ok=True)
241 target.write_text(f"- hosts: all\n roles: [{role}]\n", encoding="utf-8")
242 fixtures = {
243 "alpha/tasks/main.yml": "- ansible.builtin.include_tasks: nested.yml\n",
244 "alpha/tasks/nested.yml": "- ansible.builtin.include_role:\n name: gamma\n",
245 "beta/tasks/main.yml": "- ansible.builtin.debug:\n msg: beta\n",
246 "beta/meta/main.yml": "dependencies:\n - role: delta\n",
247 "gamma/tasks/main.yml": "- ansible.builtin.debug:\n msg: gamma\n",
248 "delta/tasks/main.yml": "- ansible.builtin.debug:\n msg: delta\n",
249 }
250 for relative, content in fixtures.items():
251 path = root / "infra/ansible/roles" / relative
252 path.parent.mkdir(parents=True, exist_ok=True)
253 path.write_text(content, encoding="utf-8")
254 found = set(workflow_paths(root))
255 required = {"infra/ansible/roles/gamma/**", "infra/ansible/roles/delta/**"}
256 return [] if required <= found else ["recursive task/meta role dependency escaped workflow"]
257
258
259def workflow_errors(workflow: str, declaration: str, root: Path = REPO_ROOT) -> list[str]:
260 """Bind both HIL parallelism variables and trusted path triggers to fleet."""
261 try:
262 doc = yaml.safe_load(workflow)
263 fleet = yaml.safe_load(declaration)
264 except yaml.YAMLError:
265 return ["hil workflow/fleet declaration: malformed YAML"]
266 if not isinstance(doc, dict) or not isinstance(fleet, dict):
267 return ["hil workflow/fleet declaration: expected mappings"]
268 expected = fleet.get("sizing", {}).get("build_parallelism")
269 environment = doc.get("env")
270 errors = []
271 if not isinstance(environment, dict) or any(
272 environment.get(name) != expected for name in ("RA8_MAX_JOBS", "CMAKE_BUILD_PARALLEL_LEVEL")
273 ):
274 errors.append("hil.yml: both build limits must equal fleet build_parallelism")
275 triggers = doc.get(True, doc.get("on"))
276 for event in ("push", "pull_request"):
277 config = triggers.get(event) if isinstance(triggers, dict) else None
278 paths = config.get("paths") if isinstance(config, dict) else None
279 if not isinstance(paths, list) or any(path not in paths for path in workflow_paths(root)):
280 errors.append(f"hil.yml: convergence safety paths missing from {event}")
281 return errors
282
283
284def _image_lock_selftest_process_tokens() -> tuple[str, ...]:
285 """Return bounded process, group, readiness, and worker-proof tokens."""
286 return (
287 '[[ "${BASH_SOURCE[0]}" != "$0" ]]',
288 ' exec /usr/bin/setsid /bin/bash -p -- "$SELFTEST_IMAGE_ENTRY"',
289 "write_fake_image_runtime() {",
290 'RA8_CONTAINER_RUNTIME="$fake_runtime" RA8_IMAGE_LOCK_DIR="$managed" \\\n'
291 " cmd_ensure --rebuild",
292 'SELFTEST_DEADLINE_STEPS="${SELFTEST_DEADLINE_STEPS:?}"',
293 "bounded_child_reap() {",
294 ' bounded_process_terminal "$pid" || return 1\n if wait "$pid"; then',
295 "bounded_process_terminal() {\n"
296 ' local pid="$1" steps="${2:-$SELFTEST_DEADLINE_STEPS}" attempt\n'
297 " for ((attempt = 0; attempt < steps; ++attempt)); do",
298 "bounded_group_empty() {\n"
299 ' local pgid="$1" steps="${2:-$SELFTEST_DEADLINE_STEPS}" attempt live\n'
300 " for ((attempt = 0; attempt < steps; ++attempt)); do",
301 "bounded_group_gone() {\n"
302 ' local pgid="$1" attempt members\n'
303 " for ((attempt = 0; attempt < SELFTEST_DEADLINE_STEPS; ++attempt)); do",
304 "bounded_process_absent() {\n"
305 ' local pid="$1" attempt\n'
306 " for ((attempt = 0; attempt < SELFTEST_DEADLINE_STEPS; ++attempt)); do",
307 "wait_for_status_file() {\n"
308 ' local path="$1" pid="$2" attempt\n'
309 " for ((attempt = 0; attempt < SELFTEST_DEADLINE_STEPS; ++attempt)); do",
310 ' if process_is_terminal "$pid"; then\n'
311 ' [[ -s "$path" ]] && return 0\n'
312 " return 1",
313 "cleanup_image_lock_case() {",
314 "force_signal_controller_cleanup() {",
315 "fresh_lock_probe() {\n"
316 ' local lock="$SELFTEST_MANAGED_DIR/devcontainer-image.lock"\n'
317 ' if ! exec 7<"$lock"; then\n'
318 " return 1\n"
319 " fi\n"
320 " if ! flock -n 7; then",
321 "worker_group_is_safe() {\n"
322 " local own_pgid\n"
323 ' own_pgid="$(ps -o pgid= -p "$$" | tr -d \' \')"',
324 )
325
326
327def _image_lock_selftest_cleanup_tokens() -> tuple[str, ...]:
328 """Return group termination, lock release, and trap-lifecycle tokens."""
329 return (
330 '[[ "$SELFTEST_WORKER_PGID" == "$SELFTEST_WORKER_PID" ]]',
331 '[[ "$SELFTEST_WORKER_PGID" != "$own_pgid" ]]',
332 "worker_group_signal_is_authorized() {\n"
333 ' worker_group_is_safe && [[ "$SELFTEST_WORKER_PGID" == "$SELFTEST_WORKER_PID" ]] &&\n'
334 ' shell_owns_live_child "$SELFTEST_WORKER_PID"',
335 "if worker_group_is_safe 2>/dev/null && ! bounded_group_empty "
336 '"$SELFTEST_WORKER_PGID"; then\n'
337 " if worker_group_signal_is_authorized 2>/dev/null; then\n"
338 " group_signal_authorized=1\n"
339 ' kill -TERM -- "-$SELFTEST_WORKER_PGID"',
340 'if [[ "$SELFTEST_WORKER_SHARED_GROUP" != "1" ]] &&\n'
341 " worker_group_is_safe 2>/dev/null && ! bounded_group_empty "
342 '"$SELFTEST_WORKER_PGID" 50; then\n'
343 ' if [[ "$group_signal_authorized" == "1" ]] &&\n'
344 " worker_group_signal_is_authorized 2>/dev/null; then\n"
345 ' kill -KILL -- "-$SELFTEST_WORKER_PGID"',
346 ' signal_owned_controller_group TERM "$controller" 2>/dev/null || return 1',
347 ' if ! bounded_process_terminal "$controller" "$SELFTEST_CONTROLLER_CLEANUP_STEPS"; then',
348 ' kill -KILL -- "-$controller" 2>/dev/null || return 1',
349 "release_parent_lock() {\n"
350 " local release_failed=0\n"
351 ' if [[ "$SELFTEST_PARENT_LOCK_OPEN" == "1" ]]; then\n'
352 " flock -u 8 || release_failed=1",
353 " trap image_lock_case_exit EXIT",
354 "clear_image_lock_case_traps() {\n restore_selftest_root_traps",
355 " trap 'image_lock_case_signal 129' HUP",
356 " trap 'image_lock_case_signal 130' INT",
357 " trap 'image_lock_case_signal 143' TERM",
358 )
359
360
361def _image_lock_selftest_scenario_tokens() -> tuple[str, ...]:
362 """Return attack dispatch, completion, and fallback-proof tokens."""
363 return (
364 'image_lock_selftest_worker() {\n local mode="$1" managed="$2" case_dir="$3" fake_runtime',
365 'record_worker_group "$case_dir" "$mode"\n'
366 ' wait_for_worker_ack "$case_dir" "$$" || exit 124',
367 " exec 8>&-\n trap '' HUP INT TERM\n"
368 ' exec /usr/bin/setsid /bin/bash -p -- "$SELFTEST_IMAGE_ENTRY"',
369 " begin_selftest_spawn_critical abort_bound_worker_spawn || return 1",
370 ' selftest_inject_bound_exit worker "$SELFTEST_WORKER_PID"',
371 " if ! read_worker_group; then\n abort_bound_worker_spawn 1\n fi",
372 "early-exit)",
373 "pre-ready-hang)",
374 "normal | post-ready-build-hang)",
375 "start_image_lock_worker post-ready-build-hang ||",
376 " signal-controller)",
377 'wait_for_status_file "$SELFTEST_CASE_DIR/build-entered.status" "$SELFTEST_WORKER_PID"',
378 "assert_no_surviving_descendants() {",
379 "selftest_forced_build_contention() {",
380 " if fresh_lock_probe; then\n"
381 ' die "selftest: fresh lock probe accepted a held lock"\n'
382 " fi",
383 ' fresh_lock_probe || die "selftest: fresh lock probe failed after worker completion"',
384 'reap_worker || die "selftest: normal child did not finish"',
385 'wait_for_status_file "$SELFTEST_CASE_DIR/done.status" '
386 '"$SELFTEST_WORKER_PID" ||\n'
387 ' die "selftest: forced rebuild did not complete after release"',
388 "assert_no_surviving_descendants || cleanup_failed=1",
389 "fresh_lock_probe || cleanup_failed=1",
390 'bounded_group_gone "$SELFTEST_WORKER_PGID" || cleanup_failed=1',
391 'bounded_process_absent "$pid" || return 1',
392 )
393
394
395def image_lock_cases_required_tokens() -> tuple[str, ...]:
396 """Return unique allocation and suite tokens owned by the cases helper."""
397 return (
398 '"${BASH_SOURCE[1]:-missing}" -ef "$SELFTEST_CASES_PARENT"',
399 " begin_selftest_spawn_critical allocation_bound_spawn_signal ||",
400 ' selftest_controller_launcher_refusals "$tmp"',
401 ' selftest_controller_persisted_ps_failure "$tmp" ||',
402 ' run_image_lock_scenario early-exit selftest_early_exit "$tmp"',
403 ' run_image_lock_scenario pre-ready-hang selftest_pre_ready_hang "$tmp"',
404 ' run_image_lock_scenario forced-build-contention selftest_forced_build_contention "$tmp"',
405 ' run_image_lock_scenario post-ready-hang selftest_post_ready_hang "$tmp"',
406 ' run_image_lock_scenario signal-ready-timeout selftest_signal_ready_timeout "$tmp"',
407 ' run_image_lock_scenario signal-cleanup selftest_signal_cleanup "$tmp"',
408 )
409
410
411def image_lock_receipt_required_tokens() -> tuple[str, ...]:
412 """Return source authority and interfaces owned by the receipt helper."""
413 return (
414 '[[ "${BASH_SOURCE[0]}" != "$0" ]] || {',
415 '"${BASH_SOURCE[1]:-missing}" -ef "$SELFTEST_LOCK_RECEIPT_PARENT"',
416 '"${DEVCONTAINER_SELFTEST_PARENT:-}" == '
417 '"$SELFTEST_LOCK_RECEIPT_PARENT_DIR/devcontainer_image.sh"',
418 "expected_image_lock_suite_receipts() {",
419 "scenario_receipt_value() {",
420 "validate_scenario_receipt_directory() {",
421 "write_scenario_receipt() {",
422 "require_scenario_receipt() {",
423 "verify_scenario_receipt_files() {",
424 "expected_cleanup_receipt() {",
425 "expected_force_cleanup_receipt() {",
426 "parent_lock_fd_is_closed() {",
427 "require_cleanup_receipt() {",
428 "require_force_cleanup_receipt() {",
429 "write_worker_cleanup_proof_file() {",
430 "require_worker_cleanup_proof_file() {",
431 "write_controller_cleanup_receipt_file() {",
432 "require_controller_cleanup_receipt_file() {",
433 )
434
435
436def image_lock_signal_required_tokens() -> tuple[str, ...]:
437 """Return unique signal-controller tokens owned by the signal helper."""
438 return (
439 '"${BASH_SOURCE[1]:-missing}" -ef "$SELFTEST_SIGNAL_PARENT"',
440 "selftest_signal_ready_timeout() {",
441 "selftest_signal_cleanup() {",
442 ' [[ ! -e "$ready" && ! -L "$ready" && ! -e "$ack" && ! -L "$ack" ]] || return 1\n'
443 " begin_selftest_spawn_critical controller_bound_spawn_signal || return 1",
444 ' [[ ! -e "$ready" && ! -L "$ready" ]] || return 1\n'
445 " begin_selftest_spawn_critical controller_bound_spawn_signal || return 1",
446 ' selftest_inject_bound_exit controller "$controller_pid"',
447 ' kill -KILL -- "-$controller" 2>/dev/null || '
448 'bounded_group_gone "$controller" || return 1',
449 'selftest_signal_cleanup() {\n local tmp="$1" signal expected '
450 "controller case_dir managed launcher_mode\n for signal in HUP INT TERM; do",
451 'bounded_process_terminal "$controller" "$SELFTEST_CONTROLLER_CLEANUP_STEPS"',
452 ' force_signal_controller_cleanup "$controller" "$case_dir" "$managed" ||\n'
453 ' die "selftest: handler-hang group fallback failed"',
454 'if ! wait_for_status_file "$case_dir/controller-ready.status" "$controller"; then\n'
455 ' SELFTEST_FORCE_CLEANUP_RECEIPT=""\n'
456 ' force_signal_controller_cleanup "$controller" "$case_dir" "$managed" ||\n'
457 ' die "selftest: $signal unready-controller cleanup failed"',
458 )
459
460
461def _image_lock_selftest_semantic_tokens() -> tuple[str, ...]:
462 """Return fail-closed managed-object attack tokens."""
463 return (
464 'if (RA8_IMAGE_LOCK_DIR="$tmp/missing-marker" resolve_image_lock ',
465 'if (RA8_IMAGE_LOCK_DIR="$directory" resolve_image_lock >/dev/null 2>&1); then\n'
466 ' die "selftest: symlinked managed image lock group marker passed"',
467 'if (RA8_IMAGE_LOCK_DIR="$directory" resolve_image_lock >/dev/null 2>&1); then\n'
468 ' die "selftest: multiply-linked managed image lock group marker passed"',
469 'die "selftest: wrong managed image lock directory group passed"',
470 'die "selftest: wrong managed image lock file group passed"',
471 'die "selftest: non-root managed image lock group marker passed"',
472 'die "selftest: writable managed image lock group marker passed"',
473 'die "selftest: root group in managed image lock group marker passed"',
474 'die "selftest: binary managed image lock group marker passed"',
475 )
476
477
478def image_lock_selftest_required_tokens() -> tuple[str, ...]:
479 """Return every unique token required from the image-lock selftest harness."""
480 return (
481 '"${BASH_SOURCE[1]:-missing}" -ef "$SELFTEST_LOCK_HELPER_PARENT"',
482 *_image_lock_selftest_process_tokens(),
483 *_image_lock_selftest_cleanup_tokens(),
484 *_image_lock_selftest_scenario_tokens(),
485 *_image_lock_selftest_semantic_tokens(),
486 )
487
488
489def _image_selftest_source_paths() -> dict[str, str]:
490 """Return the complete image-selftest source authority mapping."""
491 return {
492 "devcontainer_image": "scripts/ci/devcontainer_image.sh",
493 "image_lock_digest": "scripts/checks/hil_convergence_safety_image_lock_digest.py",
494 "image_harness_policy": ("scripts/checks/hil_convergence_safety_image_harness_policy.py"),
495 "image_process_analysis": (
496 "scripts/checks/hil_convergence_safety_image_process_analysis.py"
497 ),
498 "image_process_policy": ("scripts/checks/hil_convergence_safety_image_process_policy.py"),
499 "image_subreaper_policy": (
500 "scripts/checks/hil_convergence_safety_image_subreaper_policy.py"
501 ),
502 "process_source_fixtures": (
503 "scripts/checks/hil_convergence_safety_process_source_fixtures.py"
504 ),
505 "raw_digest_controls": "scripts/checks/hil_convergence_safety_raw_digest_controls.py",
506 "raw_digest_runtime": "scripts/checks/hil_convergence_safety_raw_digest_runtime.py",
507 "runtime_cleanup": "scripts/checks/hil_convergence_safety_runtime_cleanup.py",
508 "runtime_escape": "scripts/checks/hil_convergence_safety_runtime_escape.py",
509 "runtime_fixtures": "scripts/checks/hil_convergence_safety_runtime_fixtures.py",
510 "runtime_launcher": "scripts/checks/hil_convergence_safety_runtime_launcher.py",
511 "runtime_loader": "scripts/checks/hil_convergence_safety_runtime_loader.py",
512 "runtime_loader_harness": "scripts/checks/hil_convergence_safety_runtime_loader_harness.py",
513 "runtime_root_swap": "scripts/checks/hil_convergence_safety_runtime_root_swap.py",
514 "runtime_mutations": "scripts/checks/hil_convergence_safety_runtime_mutations.py",
515 "runtime_sources": "scripts/checks/hil_convergence_safety_runtime_sources.py",
516 "process_mutations": "scripts/checks/hil_convergence_safety_process_mutations.py",
517 "semantic_mutations": "scripts/checks/hil_convergence_safety_semantic_mutations.py",
518 "hil_convergence_entry": "scripts/checks/check_hil_convergence_safety.py",
519 "source_fixtures": "scripts/checks/hil_convergence_safety_source_fixtures.py",
520 "devcontainer_image_lock_receipts": "scripts/ci/devcontainer_image_lock_receipts.bash",
521 "devcontainer_image_lock_selftest": "scripts/ci/devcontainer_image_lock_selftest.bash",
522 "devcontainer_image_selftest": "scripts/ci/devcontainer_image_selftest.bash",
523 "devcontainer_image_bound_exit_selftest": (
524 "scripts/ci/devcontainer_image_bound_exit_selftest.bash"
525 ),
526 "devcontainer_image_selftest_cases": "scripts/ci/devcontainer_image_selftest_cases.bash",
527 "devcontainer_image_signal_selftest": "scripts/ci/devcontainer_image_signal_selftest.bash",
528 "devcontainer_image_selftest_supervisor": (
529 "scripts/ci/devcontainer_image_selftest_supervisor.py"
530 ),
531 "devcontainer_image_selftest_supervisor_cases": (
532 "scripts/ci/devcontainer_image_selftest_supervisor_cases.py"
533 ),
534 "devcontainer_image_selftest_process": (
535 "scripts/ci/devcontainer_image_selftest_process.py"
536 ),
537 }
538
539
540def _governed_source_paths() -> dict[str, str]:
541 """Return the complete governed source-name to repository-path mapping."""
542 return {
543 "dev_role": DEV_ROLE,
544 "dev_entry": DEV_ENTRY,
545 "dev_main": DEV_MAIN,
546 "dev_image_lock": DEV_IMAGE_LOCK,
547 "dev_defaults": "infra/ansible/roles/dev_box/defaults/main.yml",
548 "dev_main_entry": DEV_MAIN_ENTRY,
549 "dev_guard": DEV_GUARD,
550 "idle_helper": IDLE_HELPER,
551 "fleet": FLEET,
552 "fleet_bench": FLEET_BENCH,
553 "fleet_capacity_client": FLEET_CAPACITY_CLIENT,
554 "fleet_reconcile": FLEET_RECONCILE,
555 "fleet_reconcile_arc_selftest": FLEET_RECONCILE_ARC_SELFTEST,
556 "fleet_reconcile_process": FLEET_RECONCILE_PROCESS,
557 "fleet_runner": FLEET_RUNNER,
558 "fleet_wsl": FLEET_WSL,
559 "fleet_wsl_stage": FLEET_WSL_STAGE,
560 "fleet_model": FLEET_MODEL,
561 "fleet_runner_model": FLEET_RUNNER_MODEL,
562 "fleet_reach": FLEET_REACH,
563 "fleet_path_authority": FLEET_PATH_AUTHORITY,
564 "gate": GATE,
565 "bench_role": BENCH_ROLE,
566 "bench_entry": BENCH_ENTRY,
567 "bench_guard": BENCH_GUARD,
568 "c6_role": C6_ROLE,
569 "c6_entry": C6_ENTRY,
570 "ad2_role": AD2_ROLE,
571 "ad2_entry": AD2_ENTRY,
572 "bench_defaults": BENCH_DEFAULTS,
573 "workflow": WORKFLOW,
574 "declaration": DECLARATION,
575 "dev_playbook": PLAYBOOKS[0],
576 "bench_playbook": PLAYBOOKS[1],
577 "root_justfile": "justfile",
578 "infra_just": "just/infra.just",
579 "hil_just": HIL_JUST,
580 "infra_sh": "scripts/dev/infra.sh",
581 "infra_bootstrap": "infra/bootstrap.sh",
582 "dockerfile": ".devcontainer/Dockerfile",
583 **_image_selftest_source_paths(),
584 "dockerignore": ".dockerignore",
585 "ci_runner": "infra/ansible/roles/ci_runner/tasks/main.yml",
586 "wsl_role": "infra/ansible/roles/wsl_ci_host/tasks/main.yml",
587 "setup_ansible": "scripts/dev/setup_ansible.sh",
588 "provision_toolchain": "scripts/dev/provision_dev_box_toolchain.sh",
589 "bench_client": "scripts/hil/lib/bench_client.sh",
590 "bench_host": "scripts/hil/lib/bench_host.sh",
591 "bench_lock_verify": "scripts/hil/lib/bench_lock_verify.py",
592 "bench_lock_capability": "scripts/dev/bench_lock_capability.py",
593 "fleet_transaction_auth": "scripts/dev/fleet_transaction_auth.py",
594 }
595
596
597def load_inputs(root: Path) -> dict[str, str]:
598 """Read all governed sources, treating the removed restart handler as empty."""
599 paths = _governed_source_paths()
600 result = {key: (root / value).read_text(encoding="utf-8") for key, value in paths.items()}
601 result["dev_transaction"] = result["dev_main"]
602 _header, separator, image_lock_tasks = result["dev_image_lock"].partition("---\n")
603 include = (
604 "- name: Converge the managed devcontainer image lock authority\n"
605 " ansible.builtin.include_tasks: image_lock.yml\n"
606 )
607 if separator:
608 result["dev_main"] = result["dev_main"].replace(include, image_lock_tasks)
609 hil_shells = {
610 path.relative_to(root).as_posix(): path.read_text(encoding="utf-8")
611 for path in sorted((root / "scripts/hil").rglob("*.sh"))
612 if path.is_file()
613 }
614 monitor = root / "scripts/ci/monitor.sh"
615 hil_shells[monitor.relative_to(root).as_posix()] = monitor.read_text(encoding="utf-8")
616 result["hil_shells"] = json.dumps(hil_shells, sort_keys=True)
617 handler = root / DEV_HANDLER
618 result["dev_handler"] = handler.read_text(encoding="utf-8") if handler.exists() else ""
619 return result
620
621
622def remove_workflow_path(inputs: dict[str, str], event: str, path: str) -> dict[str, str]:
623 """Remove one exact governed path from one workflow event."""
624 document = cast(dict[object, object], yaml.safe_load(inputs["workflow"]))
625 triggers = cast(dict[str, object], document.get(True, document.get("on")))
626 config = cast(dict[str, object], triggers[event])
627 paths = cast(list[str], config["paths"])
628 if paths.count(path) != 1:
629 message = f"workflow {event} path fixture is not unique: {path}"
630 raise ValueError(message)
631 paths.remove(path)
632 changed = dict(inputs)
633 changed["workflow"] = yaml.safe_dump(document, sort_keys=False)
634 return changed