ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_hil_rig_contract.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Bind HIL rig consumers to one typed value and parser authority."""
5
6from __future__ import annotations
7
8import os
9import re
10import sys
11import tempfile
12from collections.abc import Callable
13from dataclasses import dataclass
14from pathlib import Path
15
16import yaml
17
18REPO_ROOT = Path(__file__).resolve().parents[2]
19RIG_ENV = "scripts/hil/lib/rig_env.sh"
20CONTRACT = "scripts/hil/lib/rig_contract.sh"
21PARSER = "scripts/hil/rig_env_parse.py"
22ANSIBLE = "infra/ansible/roles/dev_box/tasks/hil_runner_transaction.yml"
23BENCH_ANSIBLE = "infra/ansible/roles/hil_bench/tasks/transaction.yml"
24REMOTE_GDB_ARGS = "scripts/dev/remote_gdb_args.py"
25GATE = "scripts/ci/gates/checks.sh"
26EXAMPLE = ".env.example"
27ETH_SCRIPT = "scripts/hil/eth_tcp.sh"
28HIL_DOC = "docs/HIL_DEVELOPER_WORKFLOW.md"
29HIL_SUITE = "docs/HIL_SUITE.md"
30FIELDS = ("PI_HOST", "JLINK_SN", "JLINK_DEVICE", "PI_REPO")
31EXPANSION = re.compile(
32 r"\$(?:PI_HOST|JLINK_SN|JLINK_DEVICE|PI_REPO)\b"
33 r"|\$\{(?:PI_HOST|JLINK_SN|JLINK_DEVICE|PI_REPO)\}"
34)
35PI_HOST_EXPANSION = re.compile(r"\$PI_HOST\b|\$\{PI_HOST\}")
36SOURCE_RIG = re.compile(r"^\s*(?:source|[.])\s+[^#\n]*rig_env[.]sh", re.MULTILINE)
37
38
39@dataclass(frozen=True)
40class CommandResult:
41 """Captured status and streams from one fixed-argv test child."""
42
43 returncode: int
44 stdout: str
45 stderr: str
46
47
48def _read(root: Path, relative: str) -> str:
49 try:
50 return (root / relative).read_text(encoding="utf-8")
51 except (OSError, UnicodeError) as error:
52 return f"<UNREADABLE:{error}>"
53
54
55def _active_lines(text: str) -> list[str]:
56 return [
57 line for line in text.splitlines() if line.strip() and not line.lstrip().startswith("#")
58 ]
59
60
61def _require(condition: bool, message: str) -> None:
62 if not condition:
63 raise AssertionError(message)
64
65
66def _named_ansible_tasks(value: object, name: str) -> list[dict[str, object]]:
67 """Find every exact task through Ansible block/rescue/always nesting."""
68 if not isinstance(value, list):
69 return []
70 matches: list[dict[str, object]] = []
71 for item in value:
72 if not isinstance(item, dict):
73 continue
74 if item.get("name") == name:
75 matches.append(item)
76 for section in ("block", "rescue", "always"):
77 matches.extend(_named_ansible_tasks(item.get(section), name))
78 return matches
79
80
81def _audit_ansible_ephemeral_reporting(text: str) -> list[str]:
82 """Bind secure controller scratch tasks to truthful no-drift reporting."""
83 try:
84 document = yaml.safe_load(text)
85 except yaml.YAMLError as error:
86 return [f"{ANSIBLE}: cannot parse task structure: {error}"]
87 findings: list[str] = []
88 names = (
89 "Allocate a protected allowlist result file on the control node",
90 "Remove the protected allowlist result file",
91 )
92 for name in names:
93 tasks = _named_ansible_tasks(document, name)
94 if len(tasks) != 1:
95 findings.append(f"{ANSIBLE}: expected exactly one task {name!r}; found {len(tasks)}")
96 continue
97 task = tasks[0]
98 if task.get("changed_when") is not False:
99 findings.append(f"{ANSIBLE}: {name!r} reports same-run scratch as managed drift")
100 if task.get("check_mode") is not False:
101 findings.append(f"{ANSIBLE}: {name!r} no longer executes its secure check-mode path")
102 return findings
103
104
105def _run_fixed(arguments: tuple[str, ...], environment: dict[str, str]) -> CommandResult:
106 """Run fixed argv without a shell and capture both output streams."""
107 with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
108 actions = (
109 (os.POSIX_SPAWN_DUP2, stdout_file.fileno(), 1),
110 (os.POSIX_SPAWN_DUP2, stderr_file.fileno(), 2),
111 )
112 process = os.posix_spawn(arguments[0], arguments, environment, file_actions=actions)
113 _, status = os.waitpid(process, 0)
114 stdout_file.seek(0)
115 stderr_file.seek(0)
116 stdout = stdout_file.read().decode("utf-8", errors="replace")
117 stderr = stderr_file.read().decode("utf-8", errors="replace")
118 return CommandResult(os.waitstatus_to_exitcode(status), stdout, stderr)
119
120
121def _inside_double_quotes(line: str, offset: int) -> bool:
122 escaped = False
123 quoted = False
124 for character in line[:offset]:
125 if escaped:
126 escaped = False
127 elif character == "\\":
128 escaped = True
129 elif character == '"':
130 quoted = not quoted
131 return quoted
132
133
134def _quoted_expansion(line: str, match: re.Match[str]) -> bool:
135 """Accept a quote segment or an exact double-quoted argv expansion."""
136 adjacent_quotes = (
137 match.start() > 0
138 and match.end() < len(line)
139 and line[match.start() - 1] == '"'
140 and line[match.end()] == '"'
141 )
142 return adjacent_quotes or _inside_double_quotes(line, match.start())
143
144
145def audit_rig_env(text: str) -> list[str]:
146 """Report loader drift from the shared typed authority."""
147 findings: list[str] = []
148 required = (
149 'source "$_rig_lib_dir/rig_contract.sh"',
150 '"$_rig_python" -I "$_rig_parser"',
151 '--output "$_rig_result" --format nul --include-interactive',
152 "while IFS= read -r -d '' _rig_name",
153 "protected rig parser returned an unknown field",
154 "ra8_rig_contract_default JLINK_DEVICE",
155 "ra8_rig_contract_default PI_REPO",
156 "ra8_rig_validate_loaded true",
157 'ra8_rig_require "$@"',
158 "protected NUL-delimited pairs",
159 )
160 findings.extend(
161 f"{RIG_ENV}: missing shared-contract binding {token!r}"
162 for token in required
163 if token not in text
164 )
165 if re.search(r"PI_HOST.*=~|JLINK_(?:SN|DEVICE).*=~|PI_REPO.*=~", text):
166 findings.append(f"{RIG_ENV}: duplicated an inline rig grammar")
167 if re.search(r"(?:source|[.])\s+[\"']?\$\{?_rig_env_file", text):
168 findings.append(f"{RIG_ENV}: executes the selected rig environment")
169 if re.search(r"(^|[;(])\s*eval\b", text, re.MULTILINE):
170 findings.append(f"{RIG_ENV}: eval is forbidden for parsed rig data")
171 return findings
172
173
174def audit_contract(text: str) -> list[str]:
175 """Report missing value, startup, or source-mode contract defenses."""
176 findings: list[str] = []
177 required = (
178 'if [[ "$-" == *p* ]]; then',
179 "unset -v BASH_ENV ENV",
180 "BASH_FUNC_*%% | BASH_FUNC_*'()') ra8_startup_env_unset+=",
181 "if ((${#ra8_startup_env_unset[@]})); then",
182 'if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then',
183 "sourced rig contract refuses inherited Bash functions",
184 'exec /usr/bin/env "${ra8_startup_env_unset[@]}" -u BASH_ENV -u ENV',
185 "--descendant-selftest",
186 "PI_HOST ssh_target required",
187 "JLINK_SN identifier required",
188 "JLINK_DEVICE identifier optional R7KA8D2KF_CPU0",
189 "PI_REPO repo_path optional ra8-firmware",
190 "_ra8_rig_validate_ipv4",
191 "_ra8_rig_validate_dns",
192 "_ra8_rig_validate_repo_path",
193 )
194 findings.extend(
195 f"{CONTRACT}: missing authority token {token!r}" for token in required if token not in text
196 )
197 if re.search(r"(^|[;(])\s*eval\b", text, re.MULTILINE):
198 findings.append(f"{CONTRACT}: eval is forbidden in the value authority")
199 return findings
200
201
202def audit_ansible(text: str) -> list[str]:
203 """Report Ansible parser or exact-serialization authority drift."""
204 findings: list[str] = []
205 required = (
206 "{{ dev_box_context_src }}/scripts/hil/rig_env_parse.py",
207 'dev_box_hil_pi_repo: "{{ dev_box_hil_rig_env.PI_REPO }}"',
208 'dev_box_hil_jlink_device: "{{ dev_box_hil_rig_env.JLINK_DEVICE }}"',
209 "PI_REPO={{ dev_box_hil_pi_repo }}",
210 "PI_REPO={{ dev_box_hil_runner_bench_repo_dir }}",
211 "dev_box_hil_runner_bench_repo_dir",
212 "dev_box_hil_runner_bench_repo_dir | dirname",
213 "dev_box_hil_runner_bench_repo_dir }}/scripts/hil/run_direct.sh",
214 'service = ["PI_HOST", "JLINK_SN", "JLINK_DEVICE", "PI_REPO"]',
215 "interactive = service.copy()",
216 )
217 findings.extend(
218 f"{ANSIBLE}: missing parser/serialization binding {token!r}"
219 for token in required
220 if token not in text
221 )
222 forbidden = (
223 'allowed = {"PI_HOST"',
224 "import shlex",
225 "dev_box_hil_interactive_pi_host\n is match",
226 "dev_box_hil_rig_env.JLINK_SN is match",
227 "dev_box_hil_runner_jlink_device_default",
228 )
229 findings.extend(
230 f"{ANSIBLE}: retains duplicated parser/value grammar {token!r}"
231 for token in forbidden
232 if token in text
233 )
234 findings.extend(_audit_ansible_ephemeral_reporting(text))
235 return findings
236
237
238def audit_parser(text: str) -> list[str]:
239 """Report parser delegation, protected-I/O, or shell execution drift."""
240 findings: list[str] = []
241 required = (
242 'CONTRACT = Path(__file__).resolve().parent / "lib" / "rig_contract.sh"',
243 '("/bin/bash", "-p", str(CONTRACT), *arguments)',
244 'result = _run_contract("--validate", name, value)',
245 'result = _run_contract("--describe")',
246 "duplicate {key}",
247 "unsupported assignment syntax",
248 "never executes or expands",
249 "ASSIGNMENT_RE.fullmatch",
250 "LITERAL_RE.fullmatch",
251 "INTERACTIVE_FIELDS",
252 'output_format == "nul"',
253 )
254 findings.extend(
255 f"{PARSER}: missing authority/parser binding {token!r}"
256 for token in required
257 if token not in text
258 )
259 source_reader = text.partition("def _read_source")[2].partition("def _parse_allowlisted_line")[
260 0
261 ]
262 findings.extend(
263 f"{PARSER}: protected source reader lacks {token!r}"
264 for token in (
265 "flags |= os.O_NOFOLLOW",
266 "flags |= os.O_NONBLOCK",
267 "info.st_uid != os.getuid()",
268 "stat.S_IMODE(info.st_mode) != PROTECTED_MODE",
269 )
270 if token not in source_reader
271 )
272 if "shell=True" in text or re.search(r"\beval\s*\‍(", text):
273 findings.append(f"{PARSER}: shell/eval execution is forbidden")
274 return findings
275
276
277def audit_bench_ansible(text: str) -> list[str]:
278 """Bind the bench health check directly to the rig contract default."""
279 required = (
280 "/scripts/hil/lib/rig_contract.sh",
281 "--default JLINK_DEVICE",
282 'JLinkExe -device "$device"',
283 )
284 findings = [
285 f"{BENCH_ANSIBLE}: missing device-authority binding {token!r}"
286 for token in required
287 if token not in text
288 ]
289 if "hil_bench_jlink_device" in text or "R7KA8D2KF_CPU0" in text:
290 findings.append(f"{BENCH_ANSIBLE}: retains an independent J-Link device authority")
291 return findings
292
293
294def audit_remote_gdb_args(text: str) -> list[str]:
295 """Require start-only device input and a device-free cleanup interface."""
296 required = (
297 "def remote_command(serial: str, port: str, *, device: str)",
298 'remote.add_argument("--device", required=True)',
299 "print(remote_command(args.serial, args.port, device=args.device))",
300 )
301 findings = [
302 f"{REMOTE_GDB_ARGS}: missing start/cleanup split {token!r}"
303 for token in required
304 if token not in text
305 ]
306 if re.search(r'add_argument\‍("--device",\s*default=', text):
307 findings.append(f"{REMOTE_GDB_ARGS}: device still has an independent default")
308 if 'add_parser("cleanup")' in text or '"cleanup"' in text:
309 findings.append(f"{REMOTE_GDB_ARGS}: cleanup retains a device-command surface")
310 return findings
311
312
313def audit_hil_consumers(root: Path) -> list[str]:
314 """Report unsafe typed-field interpolation across HIL shell consumers."""
315 findings: list[str] = []
316 exempt = {RIG_ENV, CONTRACT, "scripts/hil/lib/tty_resolve.sh"}
317 for path in sorted((root / "scripts/hil").rglob("*.sh")):
318 relative = path.relative_to(root).as_posix()
319 text = path.read_text(encoding="utf-8")
320 active = "\n".join(_active_lines(text))
321 if relative != CONTRACT and "R7KA8D2KF_CPU0" in active:
322 findings.append(f"{relative}: hard-codes the contract-owned J-Link device")
323 if re.search(r"\b(?:source|[.])\s+[\"']?\.env\b", active):
324 findings.append(f"{relative}: executes a remote or local .env bypass")
325 if relative not in exempt and EXPANSION.search(active) and not SOURCE_RIG.search(text):
326 findings.append(f"{relative}: expands a rig field without sourcing rig_env.sh")
327 for line_number, line in enumerate(text.splitlines(), 1):
328 if line.lstrip().startswith("#"):
329 continue
330 findings.extend(
331 f"{relative}:{line_number}: PI_HOST is not one quoted argv/string value"
332 for match in PI_HOST_EXPANSION.finditer(line)
333 if not _quoted_expansion(line, match)
334 )
335 if relative not in {RIG_ENV, CONTRACT} and re.search(r"\$(?:\{)?PI_REPO\b", line):
336 parameter_guard = line.strip() == ': "${PI_REPO:?}"'
337 if not parameter_guard and ("printf -v" not in line or "%q" not in line):
338 findings.append(
339 f"{relative}:{line_number}: PI_REPO bypasses explicit %q serialization"
340 )
341 required_calls = {
342 "scripts/hil/run.sh": "rig_require PI_HOST JLINK_SN JLINK_DEVICE",
343 "scripts/hil/run_direct.sh": "rig_require PI_HOST JLINK_SN JLINK_DEVICE",
344 "scripts/hil/run_local.sh": "rig_require JLINK_SN JLINK_DEVICE",
345 "scripts/hil/rtt_scrape.sh": "rig_require PI_HOST JLINK_DEVICE",
346 "scripts/hil/camera_picture.sh": ("rig_require PI_HOST JLINK_SN JLINK_DEVICE PI_REPO"),
347 }
348 for relative, required in required_calls.items():
349 if (root / relative).is_file() and required not in _read(root, relative):
350 findings.append(f"{relative}: missing exact required-field call {required!r}")
351 return findings
352
353
354def audit_docs_and_ethernet(root: Path) -> list[str]:
355 """Report stale Ethernet semantics or missing contract documentation."""
356 findings: list[str] = []
357 document = _read(root, HIL_DOC)
358 suite = _read(root, HIL_SUITE)
359 ethernet = _read(root, ETH_SCRIPT)
360 defaults = _read(root, "infra/ansible/roles/hil_bench/defaults/main.yml")
361 findings.extend(
362 f"{HIL_DOC}: missing stable HIL semantic {token!r}"
363 for token in (
364 "rig_contract.sh",
365 "interactive loader never",
366 "non-symlink, mode-0600 regular file",
367 "hil_bench_eth_iface",
368 "hil_bench_eth_mac",
369 "hil_bench_eth_sysfs_device",
370 "hil_bench_eth_phc_index",
371 "USB-Ethernet",
372 "intentionally rejected",
373 "IPv6 targets, including bracketed literals, are intentionally rejected",
374 )
375 if token not in document
376 )
377 findings.extend(
378 f"{HIL_SUITE}: missing stable Ethernet semantic {token!r}"
379 for token in (
380 "fleet-declared built-in board-facing interface",
381 "installed policy verifies its MAC, sysfs device, PHC",
382 "USB adapters are rejected",
383 )
384 if token not in suite
385 )
386 findings.extend(
387 f"HIL Ethernet prose retains stale auto-detection claim {stale!r}"
388 for stale in ("detected automatically", "enxXX", "usbX device")
389 if stale in document or stale in suite or stale in ethernet
390 )
391 findings.extend(
392 f"{HIL_SUITE}: retains stale USB-interface claim {stale!r}"
393 for stale in ("USB-Ethernet adapter", "enxXX", "usbX")
394 if stale in suite
395 )
396 findings.extend(
397 f"{ETH_SCRIPT}: missing installed-policy binding {token!r}"
398 for token in (
399 '"$RA8_HIL_PRIVILEGED_HELPER" --policy-interface',
400 "verifies its permanent MAC, canonical sysfs device, PHC",
401 )
402 if token not in ethernet
403 )
404 findings.extend(
405 f"hil_bench defaults lost fleet-owned Ethernet field {token!r}"
406 for token in (
407 'hil_bench_eth_iface: ""',
408 'hil_bench_eth_mac: ""',
409 'hil_bench_eth_sysfs_device: ""',
410 "hil_bench_eth_phc_index: -1",
411 )
412 if token not in defaults
413 )
414 return findings
415
416
417def scan(root: Path = REPO_ROOT) -> list[str]:
418 """Run every typed rig, consumer, registration, and documentation audit."""
419 findings: list[str] = []
420 findings.extend(audit_rig_env(_read(root, RIG_ENV)))
421 findings.extend(audit_contract(_read(root, CONTRACT)))
422 findings.extend(audit_parser(_read(root, PARSER)))
423 findings.extend(audit_ansible(_read(root, ANSIBLE)))
424 findings.extend(audit_bench_ansible(_read(root, BENCH_ANSIBLE)))
425 findings.extend(audit_remote_gdb_args(_read(root, REMOTE_GDB_ARGS)))
426 findings.extend(audit_hil_consumers(root))
427 findings.extend(audit_docs_and_ethernet(root))
428 gate = _read(root, GATE)
429 findings.extend(
430 f"{GATE}: rig checker is not registered as {command!r}"
431 for command in (
432 "/bin/bash -p scripts/hil/lib/rig_contract.sh --selftest",
433 "python3 scripts/hil/rig_env_parse.py --selftest",
434 "python3 scripts/checks/check_hil_rig_contract.py --selftest",
435 "python3 scripts/checks/check_hil_rig_contract.py",
436 )
437 if command not in gate
438 )
439 if "PI_REPO=" not in _read(root, EXAMPLE):
440 findings.append(f"{EXAMPLE}: PI_REPO is absent from the documented contract")
441 return findings
442
443
444FAKE_HARNESS = r"""
445set -euo pipefail
446ssh() { [[ "$#" -eq 1 && "$1" == "sikar@10.0.40.103" ]]; printf 'SSH-FAKE\n'; }
447scp() { [[ "$#" -eq 2 && "$2" == "sikar@10.0.40.103:/tmp/fw.hex" ]]; printf 'SCP-FAKE\n'; }
448source "$1"
449rig_require PI_HOST JLINK_SN JLINK_DEVICE PI_REPO
450[[ -z "${TAPO_PASS+x}" ]]
451ssh "$PI_HOST"
452scp fw.hex "${PI_HOST}:/tmp/fw.hex"
453"""
454FAKE_VALID = (
455 "PI_HOST=sikar@10.0.40.103\n"
456 "JLINK_SN=123456789\n"
457 "JLINK_DEVICE=R7KA8D2KF_CPU0\n"
458 "PI_REPO=/home/ra8-hil/ra8-firmware\n"
459)
460FAKE_INVALID = (
461 "PI_HOST=-oProxyCommand=bad\nJLINK_SN=1\n",
462 "PI_HOST=user@@host\nJLINK_SN=1\n",
463 "PI_HOST=$'host\\ncommand'\nJLINK_SN=1\n",
464 "PI_HOST=host\nJLINK_SN=-1\n",
465 "PI_HOST=host\nJLINK_SN=1\nPI_REPO=../repo\n",
466 'PI_HOST=host\nJLINK_SN=1\nPI_REPO="repo\'bad"\n',
467)
468
469
470def _fake_value_cases(env_file: Path, command: tuple[str, ...]) -> None:
471 """Prove valid argv transport and reject hostile declared values."""
472 env_file.write_text(FAKE_VALID, encoding="utf-8")
473 env_file.chmod(0o600)
474 environment = {"PATH": "/usr/bin:/bin", "RA8_RIG_ENV": str(env_file)}
475 result = _run_fixed(command, environment)
476 _require(
477 not result.returncode and result.stdout == "SSH-FAKE\nSCP-FAKE\n",
478 f"valid fake transport failed: {result!r}",
479 )
480 for content in FAKE_INVALID:
481 env_file.write_text(content, encoding="utf-8")
482 result = _run_fixed(command, environment)
483 _require(
484 result.returncode != 0 and "-FAKE" not in result.stdout,
485 f"unsafe rig value reached fake transport: {content!r}",
486 )
487
488
489def _fake_loader_cases(env_file: Path, command: tuple[str, ...]) -> None:
490 """Prove commands/secrets stay data and protected-path checks fail closed."""
491 marker = env_file.parent / "executed"
492 env_file.write_text(
493 FAKE_VALID
494 + f"TAPO_PASS='$(touch {marker}); literal spaces | data'\n"
495 + f"touch {marker}\n",
496 encoding="utf-8",
497 )
498 environment = {"PATH": "/usr/bin:/bin", "RA8_RIG_ENV": str(env_file)}
499 result = _run_fixed(command, environment)
500 _require(
501 not result.returncode and not marker.exists(),
502 "declarative rig loader executed an unrelated command row",
503 )
504 env_file.chmod(0o644)
505 result = _run_fixed(command, environment)
506 _require(result.returncode != 0, "interactive loader accepted mode-0644 input")
507 target = env_file.parent / "target.env"
508 target.write_text(FAKE_VALID, encoding="utf-8")
509 target.chmod(0o600)
510 env_file.unlink()
511 env_file.symlink_to(target)
512 result = _run_fixed(command, environment)
513 _require(result.returncode != 0, "interactive loader accepted a symlink input")
514
515
516def fake_transport_selftest() -> None:
517 """Prove accepted values are one argv and rejected values never reach fakes."""
518 with tempfile.TemporaryDirectory(prefix="ra8-rig-transport-") as temporary:
519 env_file = Path(temporary) / "rig.env"
520 command = (
521 "/bin/bash",
522 "-p",
523 "-c",
524 FAKE_HARNESS,
525 "rig-harness",
526 str(REPO_ROOT / RIG_ENV),
527 )
528 _fake_value_cases(env_file, command)
529 _fake_loader_cases(env_file, command)
530
531
532def consumer_quoting_selftest() -> None:
533 """Prove the interpolation audit rejects bare option-position values."""
534 with tempfile.TemporaryDirectory(prefix="ra8-rig-consumer-") as temporary:
535 root = Path(temporary)
536 script = root / "scripts" / "hil" / "consumer.sh"
537 script.parent.mkdir(parents=True)
538 script.write_text(
539 '#!/bin/bash -p\nsource "$ROOT/scripts/hil/lib/rig_env.sh"\nssh $PI_HOST\n',
540 encoding="utf-8",
541 )
542 findings = audit_hil_consumers(root)
543 _require(
544 any("PI_HOST is not one quoted" in finding for finding in findings),
545 "consumer audit accepted an unquoted PI_HOST",
546 )
547 script.write_text(
548 '#!/bin/bash -p\nsource "$ROOT/scripts/hil/lib/rig_env.sh"\nssh "$PI_HOST"\n',
549 encoding="utf-8",
550 )
551 _require(not audit_hil_consumers(root), "consumer audit rejected quoted PI_HOST")
552 script.write_text(
553 '#!/bin/bash -p\nsource "$ROOT/scripts/hil/lib/rig_env.sh"\n'
554 "rig_require PI_HOST\ndevice R7KA8D2KF_CPU0\nsource .env\n",
555 encoding="utf-8",
556 )
557 findings = audit_hil_consumers(root)
558 _require(
559 any("hard-codes" in finding for finding in findings)
560 and any(".env bypass" in finding for finding in findings),
561 "consumer audit accepted literal-device and executable-env bypasses",
562 )
563
564
565Mutation = tuple[Callable[[str], list[str]], str]
566
567
568def _authority_mutations(sources: dict[str, str]) -> tuple[Mutation, ...]:
569 """Build must-fire mutations for the loader, contracts, and consumers."""
570 return (
571 (
572 audit_rig_env,
573 sources["rig"].replace("ra8_rig_validate_loaded true", ": # omitted", 1),
574 ),
575 (
576 audit_rig_env,
577 sources["rig"].replace('source "$_rig_lib_dir/rig_contract.sh"', ":", 1),
578 ),
579 (
580 audit_contract,
581 sources["contract"].replace(
582 "BASH_FUNC_*%% | BASH_FUNC_*'()') ra8_startup_env_unset+=",
583 "*) :",
584 1,
585 ),
586 ),
587 (audit_ansible, sources["ansible"].replace("rig_env_parse.py", "other_parser.py", 1)),
588 (
589 audit_ansible,
590 sources["ansible"].replace("PI_REPO={{ dev_box_hil_pi_repo }}", "", 1),
591 ),
592 (
593 audit_ansible,
594 sources["ansible"].replace("PI_REPO={{ dev_box_hil_runner_bench_repo_dir }}", "", 1),
595 ),
596 (
597 audit_ansible,
598 sources["ansible"].replace(
599 'dev_box_hil_jlink_device: "{{ dev_box_hil_rig_env.JLINK_DEVICE }}"',
600 "dev_box_hil_runner_jlink_device_default",
601 1,
602 ),
603 ),
604 (
605 audit_bench_ansible,
606 sources["bench"].replace('JLinkExe -device "$device"', "JLinkExe -device literal", 1),
607 ),
608 (
609 audit_remote_gdb_args,
610 sources["remote"].replace(
611 'remote.add_argument("--device", required=True)',
612 'remote.add_argument("--device", default="literal")',
613 1,
614 ),
615 ),
616 )
617
618
619def _ansible_reporting_mutations(sources: dict[str, str]) -> tuple[Mutation, ...]:
620 """Build must-fire mutations for same-run scratch reporting controls."""
621 allocation = " register: dev_box_hil_rig_env_result\n"
622 allocation += " changed_when: false\n check_mode: false"
623 cleanup = " - dev_box_hil_rig_env_result.path is defined\n"
624 cleanup += " changed_when: false\n check_mode: false"
625 return (
626 (
627 audit_ansible,
628 sources["ansible"].replace(
629 allocation,
630 allocation.replace(" changed_when: false\n", "", 1),
631 1,
632 ),
633 ),
634 (
635 audit_ansible,
636 sources["ansible"].replace(
637 cleanup,
638 cleanup.replace(" changed_when: false\n", "", 1),
639 1,
640 ),
641 ),
642 (
643 audit_ansible,
644 _append_duplicate_task(
645 sources["ansible"],
646 "Allocate a protected allowlist result file on the control node",
647 ),
648 ),
649 (
650 audit_ansible,
651 _append_duplicate_task(
652 sources["ansible"],
653 "Remove the protected allowlist result file",
654 ),
655 ),
656 )
657
658
659def _append_duplicate_task(text: str, name: str) -> str:
660 """Append one valid duplicate-name task for an exact-count mutation."""
661 return (
662 f"{text.rstrip()}\n\n- name: {name}\n"
663 " ansible.builtin.debug:\n"
664 " msg: duplicate exact-name fixture\n"
665 " changed_when: false\n"
666 " check_mode: false\n"
667 )
668
669
670def _parser_mutations(sources: dict[str, str]) -> tuple[Mutation, ...]:
671 """Build must-fire mutations for parser delegation and protected I/O."""
672 return (
673 (
674 audit_parser,
675 sources["parser"].replace(
676 'result = _run_contract("--validate", name, value)', "return", 1
677 ),
678 ),
679 (
680 audit_parser,
681 sources["parser"].replace('("/bin/bash", "-p", str(CONTRACT), *arguments)', "()", 1),
682 ),
683 (audit_parser, sources["parser"].replace("flags |= os.O_NOFOLLOW", "pass", 1)),
684 (audit_parser, sources["parser"].replace("ASSIGNMENT_RE.fullmatch", "re.match", 1)),
685 )
686
687
688def _static_mutations(sources: dict[str, str]) -> tuple[Mutation, ...]:
689 """Build exact must-fire mutations for every static authority seam."""
690 return (
691 _authority_mutations(sources)
692 + _ansible_reporting_mutations(sources)
693 + _parser_mutations(sources)
694 )
695
696
697def selftest() -> None:
698 """Exercise every static binding and fake transport in both directions."""
699 sources = {
700 "rig": _read(REPO_ROOT, RIG_ENV),
701 "contract": _read(REPO_ROOT, CONTRACT),
702 "ansible": _read(REPO_ROOT, ANSIBLE),
703 "bench": _read(REPO_ROOT, BENCH_ANSIBLE),
704 "parser": _read(REPO_ROOT, PARSER),
705 "remote": _read(REPO_ROOT, REMOTE_GDB_ARGS),
706 }
707 audits = (
708 audit_rig_env(sources["rig"]),
709 audit_contract(sources["contract"]),
710 audit_ansible(sources["ansible"]),
711 audit_bench_ansible(sources["bench"]),
712 audit_parser(sources["parser"]),
713 audit_remote_gdb_args(sources["remote"]),
714 )
715 _require(not any(audits), "live approved consumer fixtures are inconsistent")
716 mutations = _static_mutations(sources)
717 for audit, mutated in mutations:
718 _require(
719 bool(audit(mutated)),
720 f"{audit.__name__} stayed quiet on a must-fire mutation",
721 )
722 fake_transport_selftest()
723 consumer_quoting_selftest()
724 print(
725 "check_hil_rig_contract.py --selftest: PASS "
726 f"({len(mutations) + 7} must-fire, 3 valid paths must-pass)"
727 )
728
729
730def main() -> int:
731 """Run a selftest or scan the current repository."""
732 if sys.argv[1:] == ["--selftest"]:
733 selftest()
734 return 0
735 if sys.argv[1:]:
736 print("usage: check_hil_rig_contract.py [--selftest]", file=sys.stderr)
737 return 2
738 findings = scan()
739 if findings:
740 print("check_hil_rig_contract.py: findings:", file=sys.stderr)
741 for finding in findings:
742 print(f" {finding}", file=sys.stderr)
743 return 1
744 print("check_hil_rig_contract.py: shared contract, consumers, and HIL semantics agree")
745 return 0
746
747
748if __name__ == "__main__":
749 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298