3"""Bind a mutating bench converge to the repository's bench lock."""
5from __future__
import annotations
16from collections.abc
import Callable, Mapping, Sequence
17from dataclasses
import dataclass, replace
18from pathlib
import Path
20import bench_lock_capability
as blc
22LOCK_ID_RE = re.compile(
r"[0-9a-f]{16}")
23MAINTENANCE_VAR =
"hil_bench_maintenance_lock_id"
24HOLDER_PID_VAR =
"hil_bench_maintenance_holder_pid"
25HOLDER_START_VAR =
"hil_bench_maintenance_holder_start_ticks"
26HOLDER_TARGET_VAR =
"hil_bench_maintenance_holder_target"
27INHERITED_LOCK_ERROR =
"inherited bench lock is not the live holder"
28ARGPARSE_USAGE_ERROR = 2
31class BenchGuardError(ValueError):
32 """A mutating bench converge lacks a canonical live-hold identity."""
35@dataclass(frozen=
True)
37 """Everything needed to wrap one converge without reaching a host."""
41 original_argv: Sequence[str]
45 environment: Mapping[str, str]
48@dataclass(frozen=True)
50 """The user-controlled selectors at the bench apply boundary."""
56 extra_vars: Sequence[str]
57 trusted_tags: bool =
False
60LockAuthenticator = Callable[[Path, blc.Capability], bool]
63def _live_lock_matches(repo_root: Path, capability: blc.Capability) -> bool:
64 """Authenticate local wrapper ancestry and the remote kernel-held lock."""
65 client = repo_root /
"scripts/hil/lib/bench_client.sh"
66 verifier = repo_root /
"scripts/hil/lib/bench_lock_verify.py"
67 broker = repo_root /
"scripts/hil/lib/bench_lock_broker.py"
68 host = repo_root /
"scripts/hil/lib/bench_host.sh"
70 resolved_client = client.resolve(strict=
True)
71 resolved_verifier = verifier.resolve(strict=
True)
72 resolved_broker = broker.resolve(strict=
True)
73 resolved_host = host.resolve(strict=
True)
74 blc.authenticate(capability, repo_root)
77 except blc.CapabilityError:
79 sources = (client, verifier, broker, host)
80 resolved = (resolved_client, resolved_verifier, resolved_broker, resolved_host)
82 path.absolute() != target
or path.is_symlink()
or not path.is_file()
83 for path, target
in zip(sources, resolved, strict=
False)
87 digest = hashlib.sha256(host.read_bytes()).hexdigest()
88 broker_digest = hashlib.sha256(broker.read_bytes()).hexdigest()
91 script =
'. "$1"; bench_verify_live "$2" wrapped "$3" "$4"'
93 home = Path(pwd.getpwuid(os.getuid()).pw_dir).resolve(strict=
True)
94 result = subprocess.run(
108 env={
"HOME": str(home),
"PATH":
"/usr/bin:/bin"},
112 except (OSError, subprocess.TimeoutExpired):
114 return result.returncode == 0
117def needs_guard(host_class: str, plays: Sequence[str], mode: str) -> bool:
118 """Return whether this converge can mutate the physical bench host."""
119 bench = host_class ==
"hil_bench" and "hil-bench" in plays
120 delegated = host_class ==
"dev_box" and "dev-box" in plays
121 return mode ==
"apply" and (bench
or delegated)
124def _lock_id(environment: Mapping[str, str]) -> str:
125 """Return the inherited live-hold identity, rejecting ambiguous values."""
126 lock_id = environment.get(
"RA8_BENCH_LOCK_ID",
"")
127 if lock_id
and LOCK_ID_RE.fullmatch(lock_id)
is None:
128 message =
"RA8_BENCH_LOCK_ID is not the canonical 16-hex identity"
129 raise BenchGuardError(message)
133def _capability(environment: Mapping[str, str]) -> blc.Capability:
134 """Parse every inherited capability field as one indivisible identity."""
136 return blc.from_environment(dict(environment))
137 except blc.CapabilityError
as exc:
138 raise BenchGuardError(str(exc))
from exc
142 request: GuardRequest,
143 authenticate: LockAuthenticator = _live_lock_matches,
145 """Return a bench-lock wrapper argv, or empty once already guarded."""
146 if not needs_guard(request.host_class, request.plays, request.mode):
148 lock_id = _lock_id(request.environment)
150 capability = _capability(request.environment)
151 if not authenticate(request.repo_root, capability):
152 raise BenchGuardError(INHERITED_LOCK_ERROR)
157 str(request.repo_root /
"scripts/hil/bench.sh"),
160 "Ansible bench-affecting converge",
166 str(request.fleet_script),
167 *request.original_argv,
173 plays: Sequence[str],
175 environment: Mapping[str, str],
177 """Bind the validated outer hold to the remote role transaction."""
178 if not needs_guard(host_class, plays, mode):
180 lock_id = _lock_id(environment)
182 message =
"mutating bench converge is outside the bench lock"
183 raise BenchGuardError(message)
184 capability = _capability(environment)
186 HOLDER_PID_VAR: capability.holder_pid,
187 HOLDER_START_VAR: capability.holder_start_ticks,
188 HOLDER_TARGET_VAR: capability.target,
189 MAINTENANCE_VAR: lock_id,
191 return [
"-e", json.dumps(values, sort_keys=
True)]
194def control_flow_refusal(request: FlowRequest) -> str:
195 """Reject user-controlled Ansible flow/variables before the outer lock."""
196 if not needs_guard(request.host_class, request.plays, request.mode):
198 if request.tags
and not request.trusted_tags:
199 return "bench-affecting applies do not accept --tags"
200 if request.extra_vars:
201 return "bench-affecting applies do not accept raw --extra-var"
205def _control_selftest() -> list[str]:
206 """Prove bench-affecting selectors are rejected before any wrapper."""
208 for tags, extra_vars, trusted, label
in (
209 (
"hil-runner", (),
False,
"user tag selector"),
212 (
"dev_box_hil_runner_service=harmless.service",),
216 (
"", (
"dev_box_hil_runner_bench_alias=other",),
False,
"bench override"),
217 (
"", (
"hil_bench_lock_dir=/tmp/fake",),
False,
"lock-path override"),
219 request = FlowRequest(
"dev_box", [
"dev-box"],
"apply", tags, extra_vars, trusted)
220 if not control_flow_refusal(request):
221 failures.append(f
"{label} was accepted before the bench wrapper")
222 trusted = FlowRequest(
"dev_box", [
"dev-box"],
"apply",
"hil-runner", (), trusted_tags=
True)
223 if control_flow_refusal(trusted):
224 failures.append(
"typed register-hil tag was refused")
228def parser_selftest(factory: Callable[[], argparse.ArgumentParser]) -> list[str]:
229 """Prove skip/start selectors are not public fleet arguments."""
230 failures: list[str] = []
231 for option
in (
"--skip-tags",
"--start-at-task"):
232 with contextlib.redirect_stderr(io.StringIO()):
234 factory().parse_args([
"apply",
"star", option,
"anything"])
235 except SystemExit
as exc:
236 if exc.code == ARGPARSE_USAGE_ERROR:
238 failures.append(f
"bench-affecting selector {option} was accepted")
242def _privileged_bash_selftest() -> list[str]:
243 """Execute the authentication shell prefix and require privileged mode."""
244 probe =
'case "$-" in *p*) printf "%s\\n" "$-" ;; *) exit 41 ;; esac'
246 result = subprocess.run(
255 env={
"HOME":
"/",
"PATH":
"/usr/bin:/bin"},
261 except (OSError, subprocess.TimeoutExpired)
as exc:
262 return [f
"privileged Bash execution probe failed: {exc}"]
263 if result.returncode != 0
or "p" not in result.stdout.strip():
264 return [
"live-lock Bash argv did not enter privileged mode"]
268def run_selftest() -> list[str]:
269 """Exercise both sides of the lock decision without contacting a host."""
271 script = root /
"scripts/dev/fleet.py"
272 bench = GuardRequest(root, script, [
"apply",
"star"],
"hil_bench", [
"hil-bench"],
"apply", {})
273 wrapped = guarded_argv(bench)
274 failures = _privileged_bash_selftest()
275 expected_wrapper = [
"/bin/bash",
"-p", str(root /
"scripts/hil/bench.sh"),
"run"]
276 if wrapped[:4] != expected_wrapper:
277 failures.append(
"unguarded bench apply did not use the fixed privileged Bash wrapper")
278 if wrapped[-3:] != [str(script),
"apply",
"star"]:
279 failures.append(
"wrapper did not preserve the exact fleet argv")
281 "RA8_BENCH_HOLDER_PID":
"41",
282 "RA8_BENCH_HOLDER_START_TICKS":
"1000",
283 "RA8_BENCH_HOLDER_TARGET":
"star.local",
284 "RA8_BENCH_LOCK_ID":
"0123456789abcdef",
286 if guarded_argv(replace(bench, environment=held),
lambda _root, _cap:
True):
287 failures.append(
"already-held bench apply recursed into a second lock")
289 guarded_argv(replace(bench, environment=held),
lambda _root, _cap:
False)
290 except BenchGuardError:
293 failures.append(
"well-formed forged or stale lock identity bypassed the wrapper")
296 '{"hil_bench_maintenance_holder_pid": 41, '
297 '"hil_bench_maintenance_holder_start_ticks": 1000, '
298 '"hil_bench_maintenance_holder_target": "star.local", '
299 '"hil_bench_maintenance_lock_id": "0123456789abcdef"}',
301 if ansible_extra(
"hil_bench", [
"hil-bench"],
"apply", held) != expected:
302 failures.append(
"live hold identity did not bind to Ansible")
303 if guarded_argv(replace(bench, original_argv=[
"check",
"star"], mode=
"check")):
304 failures.append(
"read-only check incorrectly required a live bench hold")
305 dev = GuardRequest(root, script, [
"apply",
"dev"],
"dev_box", [
"dev-box"],
"apply", {})
306 if not guarded_argv(dev):
307 failures.append(
"delegated dev-box bench mutation was not wrapped")
309 ansible_extra(
"hil_bench", [
"hil-bench"],
"apply", {
"RA8_BENCH_LOCK_ID":
"bad"})
310 except BenchGuardError:
313 failures.append(
"malformed inherited lock identity was accepted")
314 failures.extend(_control_selftest())