ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_wsl.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Validate the WSL managed Ansible and Python environment boundary."""
4
5from __future__ import annotations
6
7import ast
8
9import yaml
10from hil_convergence_safety_ast import assignment as _assignment
11from hil_convergence_safety_ast import function as _function
12from hil_convergence_safety_ast import module_assignment as _module_assignment
13from hil_convergence_safety_ast import nested_assignment as _nested_assignment
14from hil_convergence_safety_ast import return_strings as _return_strings
15
16
17def _binary_errors(tree: ast.Module, render: ast.FunctionDef | None) -> list[str]:
18 """Require the exact managed WSL playbook binary and rendered argv."""
19 constants = {
20 "WSL_MANAGED_ROOT": "/opt/ra8-python-tools",
21 "WSL_MANAGED_CACHE": "/opt/ra8-python-tools-cache",
22 "WSL_ANSIBLE_PLAYBOOK": "/opt/ra8-python-tools/bin/ansible-playbook",
23 "WSL_SYSTEM_PYTHON": "/usr/bin/python3",
24 }
25 errors = []
26 for name, wanted in constants.items():
27 value = _module_assignment(tree, name)
28 if not isinstance(value, ast.Constant) or value.value != wanted:
29 errors.append(f"fleet WSL: {name} authority is not exact")
30 specs = [
31 node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "ConvergeSpec"
32 ]
33 field = None
34 if len(specs) == 1:
35 field = next(
36 (
37 node.value
38 for node in specs[0].body
39 if isinstance(node, ast.AnnAssign)
40 and isinstance(node.target, ast.Name)
41 and node.target.id == "ansible_playbook"
42 ),
43 None,
44 )
45 wanted_field = ast.parse("WSL_ANSIBLE_PLAYBOOK", mode="eval").body
46 argv = _nested_assignment(render, "argv") if render is not None else None
47 first = argv.elts[0] if isinstance(argv, ast.List) and argv.elts else None
48 wanted_first = ast.parse("ansible_playbook", mode="eval").body
49 if (
50 field is None
51 or ast.dump(field, include_attributes=False)
52 != ast.dump(wanted_field, include_attributes=False)
53 or first is None
54 or ast.dump(first, include_attributes=False)
55 != ast.dump(wanted_first, include_attributes=False)
56 ):
57 errors.append("fleet WSL: managed playbook executable binding is not exact")
58 return errors
59
60
61def _isolation_requirements() -> set[str]:
62 """Return required WSL environment-isolation commands."""
63 return {
64 ' case "$name" in ANSIBLE_*) unset "$name" ;; esac',
65 ' case "$name" in PYTHONHOME|PYTHONPATH|PYTHONNOUSERSITE) unset "$name" ;; esac',
66 ' case "$name" in UV_*) unset "$name" ;; esac',
67 "export PYTHONNOUSERSITE=1",
68 }
69
70
71def _path_proof_requirements() -> set[str]:
72 """Return required managed-path identity and publication commands."""
73 return {
74 ' [ -d "$1" ] && [ ! -L "$1" ] && [ "$(readlink -f -- "$1")" = "$1" ] || {',
75 ' [ -f "$1" ] && [ ! -L "$1" ] && [ "$(readlink -f -- "$1")" = "$1" ] || {',
76 "require_managed_python() {",
77 ' [ -L "$1" ] && [ "$(readlink -- "$1")" = python ] || {',
78 ' [ -L "$python_link" ] && [ "$(readlink -- "$python_link")" = "$2" ] &&',
79 ' [ "$(readlink -f -- "$1")" = "$(readlink -f -- "$2")" ] || {',
80 'require_real_dir "$(dirname "$managed_root")"',
81 'require_real_dir "$(dirname "$managed_cache")"',
82 ' ! /usr/bin/mountpoint -q -- "$1" || {',
83 "require_exact_file() {",
84 ' [ "$(stat -c %a -- "$1")" = "$2" ] || {',
85 ' file_digest="$(sha256sum -- "$1")"',
86 ' [ "${file_digest%% *}" = "$3" ] || {',
87 "/scripts/dev/bootstrap_uv.py",
88 "/scripts/dev/bootstrap_uv_exec.py",
89 " 755 ",
90 " 644 ",
91 "sync_file() {",
92 "sync_dir() {",
93 }
94
95
96def _sync_requirements() -> set[str]:
97 """Return required locked uv synchronization commands."""
98 return {
99 "uv_run() {",
100 ' --run "$@"',
101 'if [ "$mode" = apply ]; then',
102 ' install -d -m 0755 -- "$managed_root"',
103 ' install -d -m 0755 -- "$managed_cache"',
104 ' refuse_mount "$managed_root"',
105 ' refuse_mount "$managed_cache"',
106 ' UV_PROJECT_ENVIRONMENT="$managed_root" UV_PYTHON_DOWNLOADS=never '
107 'UV_CACHE_DIR="$managed_cache" uv_run ',
108 ' UV_PROJECT_ENVIRONMENT="$managed_root" UV_PYTHON_DOWNLOADS=never '
109 'UV_CACHE_DIR="$managed_cache" uv_run --offline --no-cache ',
110 " sync_status=0",
111 " --check || sync_status=$?",
112 ' if [ "$sync_status" -eq 1 ]; then',
113 ' [ "$sync_status" -eq 0 ] || exit "$sync_status"',
114 }
115
116
117def _verify_requirements() -> set[str]:
118 """Return required managed-environment verification commands."""
119 return {
120 'require_managed_python "$managed_root/bin/python3" ',
121 'require_real_file "$managed_root/bin/ansible-galaxy"',
122 ' mv -f -- "$marker" "$managed_root/.ra8-infra-lock.sha256"',
123 ' sync_file "$managed_root/.ra8-infra-lock.sha256"',
124 ' sync_dir "$managed_root"',
125 'export ANSIBLE_CONFIG="$PWD/ansible.cfg"',
126 'export ANSIBLE_COLLECTIONS_PATH="$PWD/../../.ansible/collections"',
127 "export ANSIBLE_COLLECTIONS_SCAN_SYS_PATH=false",
128 " pipeline_status=(0 0)",
129 ' /dev/stdin || pipeline_status=("${PIPESTATUS[@]}")',
130 ' if [ "${pipeline_status[1]}" -eq 1 ] && [ "$mode" = check ]; then',
131 ' [ "${pipeline_status[0]}" -eq 0 ] || exit "${pipeline_status[0]}"',
132 ' [ "${pipeline_status[1]}" -eq 0 ] || exit "${pipeline_status[1]}"',
133 }
134
135
136def _required_strings(tree: ast.Module) -> bool:
137 """Return whether executable render helpers contain every safety decision."""
138 requirements = (
139 (_function(tree, "_isolation_lines"), _isolation_requirements()),
140 (_function(tree, "_toolchain_verify_lines"), _verify_requirements()),
141 )
142 proof_strings = set().union(
143 _return_strings(_function(tree, "_proof_function_lines")),
144 _return_strings(_function(tree, "_path_proof_lines")),
145 )
146 sync_strings = set().union(
147 *(
148 _return_strings(_function(tree, name))
149 for name in (
150 "_toolchain_sync_lines",
151 "_apply_environment_lines",
152 "_check_environment_lines",
153 )
154 )
155 )
156 return (
157 all(wanted <= _return_strings(function) for function, wanted in requirements)
158 and _path_proof_requirements() <= proof_strings
159 and _sync_requirements() <= sync_strings
160 )
161
162
163def _exact_builder_contract(tree: ast.Module) -> bool:
164 """Require exact unmasked apply sync and executable-helper mode proofs."""
165 apply = _function(tree, "_apply_environment_lines")
166 proof = _function(tree, "_path_proof_lines")
167 apply_return = (
168 next((node.value for node in apply.body if isinstance(node, ast.Return)), None)
169 if apply is not None
170 else None
171 )
172 proof_return = (
173 next((node.value for node in proof.body if isinstance(node, ast.Return)), None)
174 if proof is not None
175 else None
176 )
177 if not isinstance(apply_return, ast.List) or not isinstance(proof_return, ast.List):
178 return False
179 expected_apply = ast.parse(
180 "' UV_PROJECT_ENVIRONMENT=\"$managed_root\" UV_PYTHON_DOWNLOADS=never ' "
181 "'UV_CACHE_DIR=\"$managed_cache\" uv_run ' "
182 'f"{sync_flags}"',
183 mode="eval",
184 ).body
185 expected_bootstrap = ast.parse(
186 "f\"require_exact_file {shlex.quote(stage + '/scripts/dev/bootstrap_uv.py')} \" "
187 'f"755 {bootstrap_digest}"',
188 mode="eval",
189 ).body
190 expected_helper = ast.parse(
191 "f\"require_exact_file {shlex.quote(stage + '/scripts/dev/bootstrap_uv_exec.py')} \" "
192 'f"644 {helper_digest}"',
193 mode="eval",
194 ).body
195 wanted = (expected_bootstrap, expected_helper)
196 apply_matches = sum(
197 ast.dump(item, include_attributes=False)
198 == ast.dump(expected_apply, include_attributes=False)
199 for item in apply_return.elts
200 )
201 proof_matches = [
202 sum(
203 ast.dump(item, include_attributes=False) == ast.dump(expected, include_attributes=False)
204 for item in proof_return.elts
205 )
206 for expected in wanted
207 ]
208 return apply_matches == 1 and proof_matches == [1, 1]
209
210
211def environment_errors(tree: ast.Module) -> list[str]:
212 """Require rendered WSL commands to scrub and bind environment controls."""
213 render = _function(tree, "render_converge")
214 combined = _function(tree, "_ansible_environment_lines")
215 runner = _function(tree, "_run_script")
216 environment = _assignment(runner, "env") if runner is not None else None
217 env_keys = (
218 {key.value for key in environment.keys if isinstance(key, ast.Constant)}
219 if isinstance(environment, ast.Dict)
220 else set()
221 )
222 hostile = {"ANSIBLE_CONFIG", "ANSIBLE_ROLES_PATH", "PYTHONHOME", "PYTHONPATH"}
223 render_lines = _assignment(render, "lines") if render is not None else None
224 expected = ast.parse("_ansible_environment_lines(spec)", mode="eval").body
225 combined_return = (
226 next((node.value for node in combined.body if isinstance(node, ast.Return)), None)
227 if combined is not None
228 else None
229 )
230 composition = ast.parse(
231 "[*_isolation_lines(), "
232 "*_path_proof_lines(spec.stage, spec.managed_root, spec.managed_cache), "
233 "*_toolchain_sync_lines(spec.stage, spec.mode, spec.system_python), "
234 "*_toolchain_verify_lines(spec.stage, spec.ansible_playbook, spec.system_python)]",
235 mode="eval",
236 ).body
237 sync = _function(tree, "_toolchain_sync_lines")
238 sync_strings = _return_strings(sync)
239 verify = _function(tree, "_toolchain_verify_lines")
240 verify_strings = _return_strings(verify)
241 sync_flags = _assignment(sync, "sync_flags") if sync is not None else None
242 wanted_flags = ast.parse(
243 'f"--no-config --directory {shlex.quote(stage)} sync --locked "'
244 'f"--only-group infra --no-install-project --python {shlex.quote(system_python)}"',
245 mode="eval",
246 ).body
247 errors = _binary_errors(tree, render)
248 if (
249 render_lines is None
250 or ast.dump(render_lines, include_attributes=False)
251 != ast.dump(expected, include_attributes=False)
252 or combined_return is None
253 or ast.dump(combined_return, include_attributes=False)
254 != ast.dump(composition, include_attributes=False)
255 or not _required_strings(tree)
256 or not _exact_builder_contract(tree)
257 or any(
258 "uv_bin" in value or "--verify-cache" in value or "|| true" in value
259 for value in sync_strings | verify_strings
260 )
261 or not any("uv_run --no-config --directory " in value for value in verify_strings)
262 or not any(
263 " export --locked --offline --only-group infra " in value for value in verify_strings
264 )
265 or sync_flags is None
266 or ast.dump(sync_flags, include_attributes=False)
267 != ast.dump(wanted_flags, include_attributes=False)
268 or not hostile <= env_keys
269 ):
270 errors.append("fleet WSL: rendered managed environment boundary is not exact")
271 return errors
272
273
274def _flatten_role_tasks(items: list[object]) -> list[dict[str, object]]:
275 """Return top-level and block-nested Ansible tasks in execution order."""
276 flattened: list[dict[str, object]] = []
277 for item in items:
278 if not isinstance(item, dict):
279 continue
280 flattened.append(item)
281 for section in ("block", "rescue", "always"):
282 children = item.get(section)
283 if isinstance(children, list):
284 flattened.extend(_flatten_role_tasks(children))
285 return flattened
286
287
288def _named_role_tasks(source: str) -> tuple[dict[str, tuple[int, dict[str, object]]], list[str]]:
289 """Parse uniquely named WSL role tasks for structural policy checks."""
290 try:
291 value = yaml.safe_load(source)
292 except yaml.YAMLError:
293 return {}, ["WSL clock: role task file is not valid YAML"]
294 if not isinstance(value, list):
295 return {}, ["WSL clock: role task file is not a list"]
296 grouped: dict[str, list[tuple[int, dict[str, object]]]] = {}
297 for index, item in enumerate(_flatten_role_tasks(value)):
298 if isinstance(item, dict) and isinstance(item.get("name"), str):
299 grouped.setdefault(item["name"], []).append((index, item))
300 duplicated = [name for name, matches in grouped.items() if len(matches) != 1]
301 if duplicated:
302 return {}, [f"WSL clock: duplicate task names: {', '.join(sorted(duplicated))}"]
303 return {name: matches[0] for name, matches in grouped.items()}, []
304
305
306def _required_role_task(
307 named: dict[str, tuple[int, dict[str, object]]], name: str, errors: list[str]
308) -> tuple[int, dict[str, object]] | None:
309 """Return one required role task while attributing absence."""
310 task = named.get(name)
311 if task is None:
312 errors.append(f"WSL clock: missing task {name!r}")
313 return task
314
315
316def clock_errors(source: str) -> list[str]:
317 """Require slew-safe chrony readiness and clean removal ownership."""
318 named, errors = _named_role_tasks(source)
319 if errors:
320 return errors
321 names = (
322 "Remove the managed chrony configuration",
323 "Ensure the chrony-wait override directory exists",
324 "Configure chrony-wait for slew-safe readiness",
325 "Enable chronyd and the unit that blocks until it has synchronised",
326 )
327 matches = [_required_role_task(named, name, errors) for name in names]
328 if any(match is None for match in matches):
329 return errors
330 removal, directory, override, enable = matches
331 if not removal[0] < directory[0] < override[0] < enable[0]:
332 errors.append("WSL clock: readiness task order is not exact")
333 removal_file = removal[1].get("ansible.builtin.file")
334 removed = [
335 "/etc/chrony/conf.d/10-ra8-slew-not-step.conf",
336 "/etc/systemd/system/chrony-wait.service.d/10-ra8-slew-readiness.conf",
337 ]
338 if (
339 not isinstance(removal_file, dict)
340 or removal_file.get("path") != "{{ item }}"
341 or removal_file.get("state") != "absent"
342 or removal[1].get("loop") != removed
343 ):
344 errors.append("WSL clock: removal does not retire both managed drop-ins")
345 directory_file = directory[1].get("ansible.builtin.file")
346 if not isinstance(directory_file, dict) or directory_file != {
347 "path": "/etc/systemd/system/chrony-wait.service.d",
348 "state": "directory",
349 "mode": "0755",
350 }:
351 errors.append("WSL clock: chrony-wait override directory is not exact")
352 override_copy = override[1].get("ansible.builtin.copy")
353 expected = (
354 "# Managed by the ra8-firmware wsl_ci_host Ansible role.\n"
355 "# A selected source is ready; remaining correction slews monotonically.\n"
356 "[Service]\nExecStart=\n"
357 "ExecStart=/usr/bin/chronyc -h 127.0.0.1,::1 waitsync 0 0 0 1\n"
358 )
359 if not isinstance(override_copy, dict) or override_copy != {
360 "dest": "/etc/systemd/system/chrony-wait.service.d/10-ra8-slew-readiness.conf",
361 "mode": "0644",
362 "content": expected,
363 }:
364 errors.append("WSL clock: slew-safe chrony-wait override is not exact")
365 return errors
366
367
368def autostart_errors(source: str) -> list[str]:
369 """Require the Windows keep-alive to run before WSL image work."""
370 named, errors = _named_role_tasks(source)
371 if errors:
372 return errors
373 names = (
374 "Register the Windows autostart task",
375 "Start the Windows autostart task now",
376 "Read back the task Windows actually stored",
377 "Assert the autostart task exists and is running after apply",
378 )
379 matches = [_required_role_task(named, name, errors) for name in names]
380 if any(match is None for match in matches):
381 return errors
382 create, start, query, assertion = matches
383 if not create[0] < start[0] < query[0] < assertion[0]:
384 errors.append("WSL autostart: keep-alive task order is not exact")
385 start_command = start[1].get("ansible.builtin.command")
386 start_argv = start_command.get("argv") if isinstance(start_command, dict) else None
387 if (
388 start_argv != ["{{ wsl_ci_host_schtasks }}", "/Run", "/TN", "{{ wsl_ci_host_task_name }}"]
389 or start[1].get("when") != "not ansible_check_mode"
390 or start[1].get("changed_when") is not False
391 ):
392 errors.append("WSL autostart: immediate keep-alive start is not exact")
393 query_command = query[1].get("ansible.builtin.command")
394 query_argv = query_command.get("argv") if isinstance(query_command, dict) else None
395 expected_query = [
396 "{{ wsl_ci_host_schtasks }}",
397 "/Query",
398 "/TN",
399 "{{ wsl_ci_host_task_name }}",
400 "/V",
401 "/FO",
402 "LIST",
403 ]
404 if (
405 query_argv != expected_query
406 or query[1].get("register") != "wsl_ci_host_task_query"
407 or query[1].get("changed_when") is not False
408 or query[1].get("failed_when") is not False
409 or query[1].get("check_mode") is not False
410 ):
411 errors.append("WSL autostart: verbose task readback is not exact")
412 assert_module = assertion[1].get("ansible.builtin.assert")
413 expected_conditions = [
414 "wsl_ci_host_task_name in wsl_ci_host_task_query.stdout",
415 "ansible_check_mode or 'Running' in wsl_ci_host_task_query.stdout",
416 ]
417 if not isinstance(assert_module, dict) or assert_module.get("that") != expected_conditions:
418 errors.append("WSL autostart: running-state assertion is not exact")
419 return errors